From 470a8c6130314428c4a4dc0d1981e409ca8abb06 Mon Sep 17 00:00:00 2001 From: bobbyonmagic <237182227+bobbyonmagic@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:28:02 +0000 Subject: [PATCH] Sync with the API spec --- spec/openapi.json | 8436 +++++++++++++++++++++++------------- src/smtpfast/_resources.py | 168 +- src/smtpfast/types.py | 433 ++ 3 files changed, 6020 insertions(+), 3017 deletions(-) diff --git a/spec/openapi.json b/spec/openapi.json index 18bd9e7..e29941f 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -993,7 +993,7 @@ "get": { "operationId": "getEmailMetrics", "summary": "Retrieve email metrics", - "description": "Resend-compatible counts and rates over a date range, for transactional and broadcast email together. Leave dimensions out for one totals row; add period, domain, email or broadcast (email and broadcast cannot be combined) to break the numbers down. Each lifecycle timestamp counts in its own period. Sends where every recipient is the SES mailbox simulator are left out. Rates are percentages with one decimal. Open and click metrics need tracking on. Requires the email:read scope.", + "description": "Resend-compatible counts and rates over a date range, for transactional and broadcast email together. Leave dimensions out for one totals row; add period, domain, email or broadcast (email and broadcast cannot be combined) to break the numbers down. Each lifecycle timestamp counts in its own period. Sends where every recipient is the SES mailbox simulator are left out. Rates are percentages with one decimal. Open and click metrics need tracking on. Add include=providers for a breakdown by mailbox provider (Gmail, Outlook, iCloud, Yahoo, Proton, other). Requires the email:read scope.", "tags": [ "Emails" ], @@ -1077,6 +1077,17 @@ "schema": { "type": "string" } + }, + { + "name": "include", + "in": "query", + "description": "providers adds a providers array: sent email by the recipient's mailbox provider over the range and the id filters, each email counted once by its first To address, by the time it was created and its current status. Not part of Resend's API.", + "schema": { + "type": "string", + "enum": [ + "providers" + ] + } } ], "responses": { @@ -1218,6 +1229,13 @@ "open_rate": 49.7 } } + }, + "providers": { + "type": "array", + "description": "With include=providers: one row per mailbox provider with at least one sent email, most sent first. A recipient domain not classified yet counts as other for that request (up to 50 new domains are looked up per request).", + "items": { + "$ref": "#/components/schemas/MailboxProviderMetrics" + } } } } @@ -7290,7 +7308,8 @@ "properties": { "tier": { "type": "string", - "example": "pro" + "example": "growth", + "description": "The plan: free, starter, growth or scale." }, "status": { "type": "string", @@ -7423,41 +7442,59 @@ "x-sdk-method": "usage" } }, - "/v1/team/members": { + "/v1/account/activation": { "get": { - "operationId": "listTeamMembers", - "summary": "List team members", - "description": "Everyone on the key's team, oldest first, with their role. Any member can list. Requires the team:read scope.", + "operationId": "getAccountActivation", + "summary": "Whether the account is active", + "description": "A new account is pending until one of three things happens: an invite code is redeemed, a paid plan is bought, or a free-trial request is approved. This says which state the account is in; for an active account it gives the plan and nothing personal. Works with the key smtpfast signup returns while the account is still pending; every other endpoint answers 403 until the account is active. Needs no scope. Not for connected apps.", "tags": [ - "Team" + "Account" ], "responses": { "200": { - "description": "Members", + "description": "The account's activation state", "content": { "application/json": { "schema": { "type": "object", "required": [ - "object", - "has_more", - "data" + "status", + "plan", + "email_verified", + "free_trial_requested_at", + "rejection_reason" ], "properties": { - "object": { + "status": { "type": "string", "enum": [ - "list" - ] + "pending", + "active", + "rejected" + ], + "example": "pending" }, - "has_more": { + "plan": { + "type": "string", + "example": "free" + }, + "email_verified": { "type": "boolean" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamMember" - } + "free_trial_requested_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Set once a free-trial request is waiting for review" + }, + "rejection_reason": { + "type": [ + "string", + "null" + ], + "description": "Why a request was turned down, when it was" } } } @@ -7484,6 +7521,16 @@ } } }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -7506,30 +7553,18 @@ } }, "x-sdk-group": [ - "team", - "members" + "account" ], - "x-sdk-method": "list" + "x-sdk-method": "activation" } }, - "/v1/team/members/{id}": { - "patch": { - "operationId": "updateTeamMember", - "summary": "Change a member's role or billing access", - "description": "Send role, can_manage_billing, or both. Owners and admins can change roles under the same rules as the Members page (an admin cannot touch an owner, a team keeps at least one owner). Only an owner can change billing access. The member gets an email when their role changes. Requires the team:manage scope.", + "/v1/account/activation/free-trial": { + "post": { + "operationId": "requestFreeTrial", + "summary": "Ask for the free plan", + "description": "Sends a free-trial request with the use case, as the activation page does. A person reviews it and the account is emailed when it is approved. A second request changes nothing. Works with the key smtpfast signup returns while the account is still pending; every other endpoint answers 403 until the account is active. Needs no scope. Not for connected apps.", "tags": [ - "Team" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The membership ID from List team members", - "schema": { - "type": "string" - } - } + "Account" ], "requestBody": { "required": true, @@ -7537,17 +7572,23 @@ "application/json": { "schema": { "type": "object", + "required": [ + "use_case" + ], "properties": { - "role": { + "use_case": { "type": "string", - "enum": [ - "owner", - "admin", - "member" - ] + "minLength": 20, + "maxLength": 1000, + "example": "Password resets and receipts for our app at example.com." }, - "can_manage_billing": { - "type": "boolean" + "source": { + "type": "string", + "description": "How you heard about SMTPfast: one of the options on the activation page, or other" + }, + "source_other": { + "type": "string", + "maxLength": 100 } } } @@ -7556,12 +7597,23 @@ }, "responses": { "200": { - "description": "The changed fields", + "description": "Request received", "content": { "application/json": { "schema": { "type": "object", - "additionalProperties": true + "required": [ + "requested", + "message" + ], + "properties": { + "requested": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } } } } @@ -7606,6 +7658,19 @@ } } }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The body is larger than 16 KB" + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -7628,37 +7693,63 @@ } }, "x-sdk-group": [ - "team", - "members" + "account" ], - "x-sdk-method": "update" - }, - "delete": { - "operationId": "removeTeamMember", - "summary": "Remove a member", - "description": "Removes the person from the team and revokes their API keys for it. Team resources stay. You cannot remove yourself this way, and the last owner cannot be removed. Owner or admin only; requires the team:manage scope.", + "x-sdk-method": "requestFreeTrial" + } + }, + "/v1/account/activation/invite": { + "post": { + "operationId": "redeemActivationInvite", + "summary": "Activate with an invite code", + "description": "Activates the account on the free plan with an invite code. Limited to 10 tries a minute. Works with the key smtpfast signup returns while the account is still pending; every other endpoint answers 403 until the account is active. Needs no scope. Not for connected apps.", "tags": [ - "Team" + "Account" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The membership ID", - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "example": "SF-AB12-CD34" + } + } + } } } - ], + }, "responses": { "200": { - "description": "Removed", + "description": "The account is active", "content": { "application/json": { "schema": { "type": "object", - "additionalProperties": true + "required": [ + "status", + "plan" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "active" + ] + }, + "plan": { + "type": "string", + "enum": [ + "free" + ] + } + } } } } @@ -7725,53 +7816,80 @@ } }, "x-sdk-group": [ - "team", - "members" + "account" ], - "x-sdk-method": "remove" + "x-sdk-method": "redeemInvite" } }, - "/v1/team/invites": { - "get": { - "operationId": "listTeamInvites", - "summary": "List pending invitations", - "description": "Invitations that are not accepted and not expired, newest first. Owner or admin only; requires the team:read scope.", + "/v1/account/activation/checkout": { + "post": { + "operationId": "createActivationCheckout", + "summary": "Activate with a paid plan", + "description": "Returns a Stripe Checkout link for a paid plan. A person opens it and pays; the account is active as soon as Stripe confirms the payment, so poll GET /v1/account/activation. For an account that is already active, use POST /v1/billing/checkout. Limited to 10 links an hour. Works with the key smtpfast signup returns while the account is still pending; every other endpoint answers 403 until the account is active. Needs no scope. Not for connected apps.", "tags": [ - "Team" + "Account" ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "plan" + ], + "properties": { + "plan": { + "type": "string", + "enum": [ + "starter", + "growth", + "scale" + ] + }, + "interval": { + "type": "string", + "enum": [ + "monthly", + "yearly" + ], + "default": "monthly" + } + } + } + } + } + }, "responses": { "200": { - "description": "Invitations", + "description": "The link to open", "content": { "application/json": { "schema": { "type": "object", "required": [ - "object", - "has_more", - "data" + "url" ], "properties": { - "object": { + "url": { "type": "string", - "enum": [ - "list" - ] - }, - "has_more": { - "type": "boolean" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamInvite" - } + "format": "uri" } } } } } }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "401": { "description": "Invalid or missing API key", "content": { @@ -7792,6 +7910,26 @@ } } }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -7811,65 +7949,61 @@ } } } - } - }, - "x-sdk-group": [ - "team", - "invites" - ], - "x-sdk-method": "list" - }, - "post": { - "operationId": "createTeamInvite", - "summary": "Invite someone to the team", - "description": "Emails an invitation link that is valid for 7 days. The person signs in with the invited address to accept. Inviting the same address again sends a fresh link. Needs a paid plan and a free seat. Limited to 20 invitations an hour per inviter and 3 an hour per address. Owner or admin only; requires the team:manage scope.", - "tags": [ - "Team" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "email": { - "type": "string", - "format": "email" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "member" - ], - "default": "member" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "The invitation", + }, + "503": { + "description": "Stripe could not be reached - retry shortly", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamInvite" + "$ref": "#/components/schemas/Error" } } } - }, - "400": { - "description": "Bad request - check your payload", + } + }, + "x-sdk-group": [ + "account" + ], + "x-sdk-method": "activationCheckout" + } + }, + "/v1/team/members": { + "get": { + "operationId": "listTeamMembers", + "summary": "List team members", + "description": "Everyone on the key's team, oldest first, with their role. Any member can list. Requires the team:read scope.", + "tags": [ + "Team" + ], + "responses": { + "200": { + "description": "Members", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "type": "object", + "required": [ + "object", + "has_more", + "data" + ], + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "has_more": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamMember" + } + } + } } } } @@ -7894,16 +8028,6 @@ } } }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -7927,16 +8051,16 @@ }, "x-sdk-group": [ "team", - "invites" + "members" ], - "x-sdk-method": "create" + "x-sdk-method": "list" } }, - "/v1/team/invites/{id}": { - "get": { - "operationId": "getTeamInvite", - "summary": "Retrieve a pending invitation", - "description": "One invitation, in the same shape as the list items. 404 once it is accepted, expired or revoked, or when it belongs to another team. Owner or admin only; requires the team:read scope.", + "/v1/team/members/{id}": { + "patch": { + "operationId": "updateTeamMember", + "summary": "Change a member's role or billing access", + "description": "Send role, can_manage_billing, or both. Owners and admins can change roles under the same rules as the Members page (an admin cannot touch an owner, a team keeps at least one owner). Only an owner can change billing access. The member gets an email when their role changes. Requires the team:manage scope.", "tags": [ "Team" ], @@ -7945,19 +8069,53 @@ "name": "id", "in": "path", "required": true, - "description": "The invitation ID", + "description": "The membership ID from List team members", "schema": { "type": "string" } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "owner", + "admin", + "member" + ] + }, + "can_manage_billing": { + "type": "boolean" + } + } + } + } + } + }, "responses": { "200": { - "description": "The invitation", + "description": "The changed fields", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamInvite" + "type": "object", + "additionalProperties": true + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } } @@ -8015,14 +8173,14 @@ }, "x-sdk-group": [ "team", - "invites" + "members" ], - "x-sdk-method": "get" + "x-sdk-method": "update" }, "delete": { - "operationId": "revokeTeamInvite", - "summary": "Revoke an invitation", - "description": "The link stops working at once. Owner or admin only; requires the team:manage scope.", + "operationId": "removeTeamMember", + "summary": "Remove a member", + "description": "Removes the person from the team and revokes their API keys for it. Team resources stay. You cannot remove yourself this way, and the last owner cannot be removed. Owner or admin only; requires the team:manage scope.", "tags": [ "Team" ], @@ -8031,7 +8189,7 @@ "name": "id", "in": "path", "required": true, - "description": "The invitation ID", + "description": "The membership ID", "schema": { "type": "string" } @@ -8039,7 +8197,7 @@ ], "responses": { "200": { - "description": "Revoked", + "description": "Removed", "content": { "application/json": { "schema": { @@ -8049,6 +8207,16 @@ } } }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "401": { "description": "Invalid or missing API key", "content": { @@ -8102,38 +8270,48 @@ }, "x-sdk-group": [ "team", - "invites" + "members" ], - "x-sdk-method": "revoke" + "x-sdk-method": "remove" } }, - "/v1/contacts/{id}/segments": { + "/v1/team/invites": { "get": { - "operationId": "listContactSegments", - "summary": "List a contact's segments", - "description": "The segments one contact belongs to. The contact can be named by id or email address, like Resend. Requires the contact:read scope.", + "operationId": "listTeamInvites", + "summary": "List pending invitations", + "description": "Invitations that are not accepted and not expired, newest first. Owner or admin only; requires the team:read scope.", "tags": [ - "Contacts" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The contact ID or email address", - "schema": { - "type": "string" - } - } + "Team" ], "responses": { "200": { - "description": "Segments", + "description": "Invitations", "content": { "application/json": { "schema": { "type": "object", - "additionalProperties": true + "required": [ + "object", + "has_more", + "data" + ], + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "has_more": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamInvite" + } + } + } } } } @@ -8158,16 +8336,6 @@ } } }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -8190,35 +8358,138 @@ } }, "x-sdk-group": [ - "contacts", - "segments" + "team", + "invites" ], "x-sdk-method": "list" - } - }, - "/v1/contacts/{id}/segments/{segment_id}": { + }, "post": { - "operationId": "addContactToSegment", - "summary": "Add a contact to a segment", - "description": "Resend-compatible. Adding a contact that is already in the segment is not an error. Requires the contact:write scope.", + "operationId": "createTeamInvite", + "summary": "Invite someone to the team", + "description": "Emails an invitation link that is valid for 7 days. The person signs in with the invited address to accept. Inviting the same address again sends a fresh link. Needs a paid plan and a free seat. Limited to 20 invitations an hour per inviter and 3 an hour per address. Owner or admin only; requires the team:manage scope.", "tags": [ - "Contacts" + "Team" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The contact ID or email address", - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "member" + ], + "default": "member" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The invitation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamInvite" + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } } }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "team", + "invites" + ], + "x-sdk-method": "create" + } + }, + "/v1/team/invites/{id}": { + "get": { + "operationId": "getTeamInvite", + "summary": "Retrieve a pending invitation", + "description": "One invitation, in the same shape as the list items. 404 once it is accepted, expired or revoked, or when it belongs to another team. Owner or admin only; requires the team:read scope.", + "tags": [ + "Team" + ], + "parameters": [ { - "name": "segment_id", + "name": "id", "in": "path", "required": true, - "description": "The segment ID", + "description": "The invitation ID", "schema": { "type": "string" } @@ -8226,12 +8497,11 @@ ], "responses": { "200": { - "description": "The segment id", + "description": "The invitation", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/TeamInvite" } } } @@ -8288,33 +8558,24 @@ } }, "x-sdk-group": [ - "contacts", - "segments" + "team", + "invites" ], - "x-sdk-method": "add" + "x-sdk-method": "get" }, "delete": { - "operationId": "removeContactFromSegment", - "summary": "Remove a contact from a segment", - "description": "Resend-compatible. The contact stays in the audience. Requires the contact:write scope.", + "operationId": "revokeTeamInvite", + "summary": "Revoke an invitation", + "description": "The link stops working at once. Owner or admin only; requires the team:manage scope.", "tags": [ - "Contacts" + "Team" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The contact ID or email address", - "schema": { - "type": "string" - } - }, - { - "name": "segment_id", - "in": "path", - "required": true, - "description": "The segment ID", + "description": "The invitation ID", "schema": { "type": "string" } @@ -8322,7 +8583,7 @@ ], "responses": { "200": { - "description": "Removed", + "description": "Revoked", "content": { "application/json": { "schema": { @@ -8384,58 +8645,33 @@ } }, "x-sdk-group": [ - "contacts", - "segments" + "team", + "invites" ], - "x-sdk-method": "remove" + "x-sdk-method": "revoke" } }, - "/v1/suppressions/batch/add": { - "post": { - "operationId": "addSuppressionsBatch", - "summary": "Suppress addresses in bulk", - "description": "Resend-compatible: add 1 to 100 addresses as manual suppressions of every send (scope all). An address already suppressed keeps its record, except that a marketing-only one becomes a manual suppression of every send. Requires the contact:write scope.", + "/v1/billing": { + "get": { + "operationId": "getBilling", + "summary": "Get the plan and usage", + "description": "The plan the team runs on, its state in Stripe (billing interval, status, renewal or end date, a scheduled move to a smaller plan, a failed payment), usage this month against the plan's limits, and the price and limits of the next plan up. Only keys created by the team owner can call it, because billing is the owner's, as on the billing page. A team with more than one owner gets a 409 with code multiple_owners. Requires the billing:read scope.", "tags": [ - "Suppressions" + "Billing" ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "emails" - ], - "properties": { - "emails": { - "type": "array", - "minItems": 1, - "maxItems": 100, - "items": { - "type": "string", - "format": "email" - } - } - } - } - } - } - }, "responses": { - "201": { - "description": "The suppressions", + "200": { + "description": "Plan and usage", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/Billing" } } } }, - "400": { - "description": "Bad request - check your payload", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -8444,8 +8680,8 @@ } } }, - "401": { - "description": "Invalid or missing API key", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { @@ -8454,8 +8690,8 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", "content": { "application/json": { "schema": { @@ -8483,21 +8719,31 @@ } } } + }, + "503": { + "description": "Stripe could not be reached - retry shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ - "suppressions" + "billing" ], - "x-sdk-method": "addBatch" + "x-sdk-method": "get" } }, - "/v1/suppressions/batch/remove": { + "/v1/billing/checkout": { "post": { - "operationId": "removeSuppressionsBatch", - "summary": "Remove suppressions in bulk", - "description": "Resend-compatible: remove 1 to 100 suppressions by emails or by ids, not both. Hard bounces and complaints are released from the provider's account list too. Requires the contact:write scope and an owner or admin.", + "operationId": "createBillingCheckout", + "summary": "Get a link to start or change a plan", + "description": "Nothing is charged and no plan changes here: the response is a link for a person to open. Without a subscription it is a Stripe Checkout page. With one it is the billing page with the change open in its confirmation dialog, which shows the price before anything is charged. A subscription with an unpaid invoice, one that is set to end, or one with a smaller plan scheduled before an upgrade is refused with the reason. Only keys created by the team owner can call it, because billing is the owner's, as on the billing page. A team with more than one owner gets a 409 with code multiple_owners. Requires the billing:manage scope.", "tags": [ - "Suppressions" + "Billing" ], "requestBody": { "required": true, @@ -8505,23 +8751,26 @@ "application/json": { "schema": { "type": "object", + "required": [ + "plan" + ], "properties": { - "emails": { - "type": "array", - "minItems": 1, - "maxItems": 100, - "items": { - "type": "string", - "format": "email" - } + "plan": { + "type": "string", + "enum": [ + "starter", + "growth", + "scale" + ], + "description": "The plan to move to" }, - "ids": { - "type": "array", - "minItems": 1, - "maxItems": 100, - "items": { - "type": "string" - } + "interval": { + "type": "string", + "enum": [ + "monthly", + "yearly" + ], + "description": "Defaults to the subscription's current interval, or monthly without one. Required when the subscription is on a price with no known interval." } } } @@ -8530,12 +8779,11 @@ }, "responses": { "200": { - "description": "The removed suppressions", + "description": "The link", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/BillingCheckout" } } } @@ -8570,8 +8818,18 @@ } } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { @@ -8589,66 +8847,69 @@ } } } + }, + "503": { + "description": "Stripe could not be reached - retry shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ - "suppressions" + "billing", + "checkout" ], - "x-sdk-method": "removeBatch" + "x-sdk-method": "create" } }, - "/v1/suppressions": { - "get": { - "operationId": "listSuppressions", - "summary": "List suppressions", - "description": "Return suppressed addresses for the current account. Suppressed recipients are blocked before sending. A suppression with scope marketing blocks marketing email only; scope all blocks every send.", + "/v1/billing/portal": { + "post": { + "operationId": "createBillingPortalSession", + "summary": "Get a link to the billing portal", + "description": "A Stripe billing portal link, for cards, invoices, billing details and cancelling. Plan changes are not offered there. With flow set to cancel it opens on the cancel page. Only keys created by the team owner can call it, because billing is the owner's, as on the billing page. A team with more than one owner gets a 409 with code multiple_owners. Requires the billing:manage scope.", "tags": [ - "Suppressions" + "Billing" ], - "parameters": [ - { - "name": "q", - "in": "query", - "description": "Search by suppressed email address", - "schema": { - "type": "string" - } - }, - { - "name": "reason", - "in": "query", - "description": "Filter by suppression reason", - "schema": { - "type": "string", - "enum": [ - "all", - "hard_bounce", - "complaint", - "manual", - "unsubscribe" - ], - "default": "all" - } - }, - { - "name": "limit", - "in": "query", - "description": "Number of suppressions to return", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "flow": { + "type": "string", + "enum": [ + "cancel" + ], + "description": "Open the portal on the page that cancels the subscription" + } + } + } } } - ], + }, "responses": { "200": { - "description": "List of suppressions", + "description": "The link", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionList" + "$ref": "#/components/schemas/BillingPortalSession" + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } } @@ -8673,6 +8934,16 @@ } } }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -8692,37 +8963,94 @@ } } } + }, + "503": { + "description": "Stripe could not be reached - retry shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ - "suppressions" + "billing", + "portal" ], - "x-sdk-method": "list" - }, - "post": { - "operationId": "createSuppression", - "summary": "Create a manual suppression", - "description": "Manually suppress an address so future sends to it are blocked: every send by default, or marketing email only with scope marketing.", + "x-sdk-method": "create" + } + }, + "/v1/billing/invoices": { + "get": { + "operationId": "listBillingInvoices", + "summary": "List invoices", + "description": "Invoices from Stripe, newest first. Page with after or before, set to an invoice id from the list. Only keys created by the team owner can call it, because billing is the owner's, as on the billing page. A team with more than one owner gets a 409 with code multiple_owners. Requires the billing:read scope.", "tags": [ - "Suppressions" + "Billing" ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSuppressionRequest" - } + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "description": "How many to return, 1 to 100 (default 10)", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 10 + } + }, + { + "name": "after", + "in": "query", + "required": false, + "description": "Return invoices older than this invoice id", + "schema": { + "type": "string" + } + }, + { + "name": "before", + "in": "query", + "required": false, + "description": "Return invoices newer than this invoice id", + "schema": { + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Suppression created", + "200": { + "description": "Invoices", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Suppression" + "type": "object", + "required": [ + "object", + "has_more", + "data" + ], + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "has_more": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Invoice" + } + } + } } } } @@ -8757,6 +9085,16 @@ } } }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -8776,28 +9114,39 @@ } } } + }, + "503": { + "description": "Stripe could not be reached - retry shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ - "suppressions" + "billing", + "invoices" ], - "x-sdk-method": "create" + "x-sdk-method": "list" } }, - "/v1/suppressions/{id}": { + "/v1/contacts/{id}/segments": { "get": { - "operationId": "getSuppression", - "summary": "Retrieve a suppression", - "description": "Retrieve a suppression by ID or URL-encoded email address.", + "operationId": "listContactSegments", + "summary": "List a contact's segments", + "description": "The segments one contact belongs to. The contact can be named by id or email address, like Resend. Requires the contact:read scope.", "tags": [ - "Suppressions" + "Contacts" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The suppression ID or URL-encoded email address", + "description": "The contact ID or email address", "schema": { "type": "string" } @@ -8805,11 +9154,12 @@ ], "responses": { "200": { - "description": "Suppression details", + "description": "Segments", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Suppression" + "type": "object", + "additionalProperties": true } } } @@ -8866,23 +9216,35 @@ } }, "x-sdk-group": [ - "suppressions" + "contacts", + "segments" ], - "x-sdk-method": "get" - }, - "delete": { - "operationId": "deleteSuppression", - "summary": "Delete a suppression", - "description": "Remove a suppression. The address becomes eligible for future sends unless it is suppressed again.", + "x-sdk-method": "list" + } + }, + "/v1/contacts/{id}/segments/{segment_id}": { + "post": { + "operationId": "addContactToSegment", + "summary": "Add a contact to a segment", + "description": "Resend-compatible. Adding a contact that is already in the segment is not an error. Requires the contact:write scope.", "tags": [ - "Suppressions" + "Contacts" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The suppression ID or URL-encoded email address", + "description": "The contact ID or email address", + "schema": { + "type": "string" + } + }, + { + "name": "segment_id", + "in": "path", + "required": true, + "description": "The segment ID", "schema": { "type": "string" } @@ -8890,11 +9252,12 @@ ], "responses": { "200": { - "description": "Suppression deleted", + "description": "The segment id", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionDeleted" + "type": "object", + "additionalProperties": true } } } @@ -8951,68 +9314,46 @@ } }, "x-sdk-group": [ - "suppressions" + "contacts", + "segments" ], - "x-sdk-method": "delete" - } - }, - "/v1/broadcasts": { - "get": { - "operationId": "listBroadcasts", - "summary": "List broadcasts", - "description": "Return broadcast campaigns for the current account with recipient rollup metrics.", + "x-sdk-method": "add" + }, + "delete": { + "operationId": "removeContactFromSegment", + "summary": "Remove a contact from a segment", + "description": "Resend-compatible. The contact stays in the audience. Requires the contact:write scope.", "tags": [ - "Broadcasts" + "Contacts" ], "parameters": [ { - "name": "status", - "in": "query", - "description": "Filter by campaign status", - "schema": { - "type": "string", - "enum": [ - "all", - "draft", - "scheduled", - "queued", - "sent", - "paused", - "canceled" - ], - "default": "all" - } - }, - { - "name": "q", - "in": "query", - "description": "Search by campaign name, subject, or sender", + "name": "id", + "in": "path", + "required": true, + "description": "The contact ID or email address", "schema": { "type": "string" } }, { - "name": "archived", - "in": "query", - "description": "false leaves archived broadcasts out, true lists only archived ones, all lists both", + "name": "segment_id", + "in": "path", + "required": true, + "description": "The segment ID", "schema": { - "type": "string", - "enum": [ - "false", - "true", - "all" - ], - "default": "false" + "type": "string" } } ], "responses": { "200": { - "description": "List of broadcasts", + "description": "Removed", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BroadcastList" + "type": "object", + "additionalProperties": true } } } @@ -9037,6 +9378,16 @@ } } }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -9059,34 +9410,52 @@ } }, "x-sdk-group": [ - "broadcasts" + "contacts", + "segments" ], - "x-sdk-method": "list" - }, + "x-sdk-method": "remove" + } + }, + "/v1/suppressions/batch/add": { "post": { - "operationId": "createBroadcast", - "summary": "Create a broadcast draft", - "description": "Create a broadcast campaign draft. Broadcasts can be edited while draft or scheduled, and must include an unsubscribe token before sending.", + "operationId": "addSuppressionsBatch", + "summary": "Suppress addresses in bulk", + "description": "Resend-compatible: add 1 to 100 addresses as manual suppressions of every send (scope all). An address already suppressed keeps its record, except that a marketing-only one becomes a manual suppression of every send. Requires the contact:write scope.", "tags": [ - "Broadcasts" + "Suppressions" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBroadcastRequest" + "type": "object", + "required": [ + "emails" + ], + "properties": { + "emails": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "string", + "format": "email" + } + } + } } } } }, "responses": { "201": { - "description": "Broadcast created", + "description": "The suppressions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Broadcast" + "type": "object", + "additionalProperties": true } } } @@ -9143,43 +9512,62 @@ } }, "x-sdk-group": [ - "broadcasts" + "suppressions" ], - "x-sdk-method": "create" + "x-sdk-method": "addBatch" } }, - "/v1/broadcasts/{id}": { - "get": { - "operationId": "getBroadcast", - "summary": "Get broadcast details", - "description": "Retrieve a broadcast with metrics and the latest recipient rows.", + "/v1/suppressions/batch/remove": { + "post": { + "operationId": "removeSuppressionsBatch", + "summary": "Remove suppressions in bulk", + "description": "Resend-compatible: remove 1 to 100 suppressions by emails or by ids, not both. Hard bounces and complaints are released from the provider's account list too. Requires the contact:write scope and an owner or admin.", "tags": [ - "Broadcasts" + "Suppressions" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The broadcast ID", - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "string", + "format": "email" + } + }, + "ids": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "string" + } + } + } + } } } - ], + }, "responses": { "200": { - "description": "Broadcast details", + "description": "The removed suppressions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BroadcastDetail" + "type": "object", + "additionalProperties": true } } } }, - "401": { - "description": "Invalid or missing API key", + "400": { + "description": "Bad request - check your payload", "content": { "application/json": { "schema": { @@ -9188,8 +9576,8 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -9198,8 +9586,8 @@ } } }, - "404": { - "description": "Resource not found", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { @@ -9230,55 +9618,63 @@ } }, "x-sdk-group": [ - "broadcasts" + "suppressions" ], - "x-sdk-method": "get" - }, - "patch": { - "operationId": "updateBroadcast", - "summary": "Update a draft or scheduled broadcast", - "description": "Update broadcast content. Updating a scheduled broadcast cancels the previous schedule and returns it to draft.", + "x-sdk-method": "removeBatch" + } + }, + "/v1/suppressions": { + "get": { + "operationId": "listSuppressions", + "summary": "List suppressions", + "description": "Return suppressed addresses for the current account. Suppressed recipients are blocked before sending. A suppression with scope marketing blocks marketing email only; scope all blocks every send.", "tags": [ - "Broadcasts" + "Suppressions" ], "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "description": "The broadcast ID", + "name": "q", + "in": "query", + "description": "Search by suppressed email address", "schema": { "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateBroadcastRequest" - } + }, + { + "name": "reason", + "in": "query", + "description": "Filter by suppression reason", + "schema": { + "type": "string", + "enum": [ + "all", + "hard_bounce", + "complaint", + "manual", + "unsubscribe" + ], + "default": "all" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of suppressions to return", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 } } - }, + ], "responses": { "200": { - "description": "Broadcast updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Broadcast" - } - } - } - }, - "400": { - "description": "Bad request - check your payload", + "description": "List of suppressions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/SuppressionList" } } } @@ -9303,16 +9699,6 @@ } } }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -9335,35 +9721,34 @@ } }, "x-sdk-group": [ - "broadcasts" + "suppressions" ], - "x-sdk-method": "update" + "x-sdk-method": "list" }, - "delete": { - "operationId": "deleteBroadcast", - "summary": "Delete a draft broadcast", - "description": "Delete a broadcast that has not been queued or scheduled.", + "post": { + "operationId": "createSuppression", + "summary": "Create a manual suppression", + "description": "Manually suppress an address so future sends to it are blocked: every send by default, or marketing email only with scope marketing.", "tags": [ - "Broadcasts" + "Suppressions" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The broadcast ID", - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSuppressionRequest" + } } } - ], + }, "responses": { - "200": { - "description": "Broadcast deleted", + "201": { + "description": "Suppression created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedObject" + "$ref": "#/components/schemas/Suppression" } } } @@ -9398,16 +9783,6 @@ } } }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -9430,24 +9805,25 @@ } }, "x-sdk-group": [ - "broadcasts" + "suppressions" ], - "x-sdk-method": "delete" + "x-sdk-method": "create" } }, - "/v1/broadcasts/audience": { + "/v1/suppressions/{id}": { "get": { - "operationId": "getBroadcastAudience", - "summary": "Preview a broadcast audience", - "description": "How many contacts a broadcast to all contacts, or to one segment, would reach after unsubscribes, suppressions and invalid addresses are skipped, plus the plan's broadcast allowance this month. Not in Resend. Requires the email:read scope.", + "operationId": "getSuppression", + "summary": "Retrieve a suppression", + "description": "Retrieve a suppression by ID or URL-encoded email address.", "tags": [ - "Broadcasts" + "Suppressions" ], "parameters": [ { - "name": "segment_id", - "in": "query", - "description": "Preview one segment instead of all contacts", + "name": "id", + "in": "path", + "required": true, + "description": "The suppression ID or URL-encoded email address", "schema": { "type": "string" } @@ -9455,12 +9831,11 @@ ], "responses": { "200": { - "description": "The audience counts", + "description": "Suppression details", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/Suppression" } } } @@ -9517,89 +9892,23 @@ } }, "x-sdk-group": [ - "broadcasts" + "suppressions" ], - "x-sdk-method": "getAudience" - } - }, - "/v1/broadcasts/{id}/recipients": { - "get": { - "operationId": "listBroadcastRecipients", - "summary": "List broadcast recipients", - "description": "Resend-compatible: recipients of a broadcast for one event type (sent, delivered, opened, clicked, bounced, complained, unsubscribed, suppressed), newest first, with after/before cursors. Each has contact_id; opened and clicked add count, clicked adds clicked_links, bounced adds bounce_type. Without type, every recipient (not in Resend). Requires the email:read scope.", + "x-sdk-method": "get" + }, + "delete": { + "operationId": "deleteSuppression", + "summary": "Delete a suppression", + "description": "Remove a suppression. The address becomes eligible for future sends unless it is suppressed again.", "tags": [ - "Broadcasts" + "Suppressions" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The broadcast ID", - "schema": { - "type": "string" - } - }, - { - "name": "type", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "sent", - "delivered", - "opened", - "clicked", - "bounced", - "complained", - "unsubscribed", - "suppressed" - ] - } - }, - { - "name": "bounce_type", - "in": "query", - "description": "With type=bounced only", - "schema": { - "type": "string", - "enum": [ - "permanent", - "transient", - "undetermined" - ] - } - }, - { - "name": "email", - "in": "query", - "description": "Recipients whose address contains this text", - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - } - }, - { - "name": "after", - "in": "query", - "description": "Return recipients after this recipient ID", - "schema": { - "type": "string" - } - }, - { - "name": "before", - "in": "query", - "description": "Return recipients before this recipient ID", + "description": "The suppression ID or URL-encoded email address", "schema": { "type": "string" } @@ -9607,22 +9916,11 @@ ], "responses": { "200": { - "description": "Recipients", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "400": { - "description": "Bad request - check your payload", + "description": "Suppression deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/SuppressionDeleted" } } } @@ -9679,188 +9977,142 @@ } }, "x-sdk-group": [ - "broadcasts", - "recipients" + "suppressions" ], - "x-sdk-method": "list" + "x-sdk-method": "delete" } }, - "/v1/broadcasts/{id}/clicked-links": { + "/v1/broadcasts": { "get": { - "operationId": "listBroadcastClickedLinks", - "summary": "List clicked links", - "description": "Every link clicked in this broadcast, with total clicks and the number of distinct recipients who clicked it. Ordered by total clicks.", + "operationId": "listBroadcasts", + "summary": "List broadcasts", + "description": "Return broadcast campaigns for the current account with recipient rollup metrics.", "tags": [ "Broadcasts" ], "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "description": "The broadcast ID", + "name": "status", + "in": "query", + "description": "Filter by campaign status", + "schema": { + "type": "string", + "enum": [ + "all", + "draft", + "scheduled", + "queued", + "sent", + "paused", + "canceled" + ], + "default": "all" + } + }, + { + "name": "q", + "in": "query", + "description": "Search by campaign name, subject, or sender", "schema": { "type": "string" } + }, + { + "name": "archived", + "in": "query", + "description": "false leaves archived broadcasts out, true lists only archived ones, all lists both", + "schema": { + "type": "string", + "enum": [ + "false", + "true", + "all" + ], + "default": "false" + } } ], "responses": { "200": { - "description": "The links clicked in this broadcast", + "description": "List of broadcasts", "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "broadcast_id", - "links", - "total_clicks" - ], - "properties": { - "broadcast_id": { - "type": "string" - }, - "links": { - "type": "array", - "items": { - "type": "object", - "required": [ - "url", - "clicks", - "unique_clicks" - ], - "properties": { - "url": { - "type": "string" - }, - "clicks": { - "type": "integer", - "example": 12 - }, - "unique_clicks": { - "type": "integer", - "example": 9, - "description": "Distinct recipients who clicked" - } - } - } - }, - "total_clicks": { - "type": "integer", - "example": 30 - } - } + "$ref": "#/components/schemas/BroadcastList" } } } }, - "404": { - "description": "Broadcast not found" - } - }, - "x-sdk-group": [ - "broadcasts" - ], - "x-sdk-method": "listClickedLinks" - } - }, - "/v1/broadcasts/{id}/duplicate": { - "post": { - "operationId": "duplicateBroadcast", - "summary": "Duplicate broadcast", - "description": "Copy a broadcast into a new draft. Content and targeting carry over; the schedule, recipients and send history do not. Pass `name` to set the copy's name, otherwise it is the original's with \"(copy)\" appended.", - "tags": [ - "Broadcasts" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The broadcast ID to copy", - "schema": { - "type": "string" + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } } - } - ], - "responses": { - "201": { - "description": "The new draft broadcast", + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "broadcast" - ], - "properties": { - "broadcast": { - "$ref": "#/components/schemas/Broadcast" - } - } + "$ref": "#/components/schemas/Error" } } } }, - "404": { - "description": "Broadcast not found" + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ "broadcasts" ], - "x-sdk-method": "duplicate" - } - }, - "/v1/broadcasts/{id}/send": { + "x-sdk-method": "list" + }, "post": { - "operationId": "sendBroadcast", - "summary": "Send or schedule a broadcast", - "description": "Snapshot eligible subscribed contacts, create one queued email per recipient, and send now or schedule within 30 days. With send_over_minutes the recipients go out evenly spread over that window instead of all at once; a canceled broadcast stops the sends that have not gone out yet.", + "operationId": "createBroadcast", + "summary": "Create a broadcast draft", + "description": "Create a broadcast campaign draft. Broadcasts can be edited while draft or scheduled, and must include an unsubscribe token before sending.", "tags": [ "Broadcasts" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The broadcast ID", - "schema": { - "type": "string" - } - } - ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "scheduled_at": { - "type": "string", - "format": "date-time" - }, - "send_over_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 4320, - "description": "Not in Resend. Spread the send evenly over this many minutes from the send time (or scheduled_at): recipient i of N goes out at start + i * window / N. 0 sends to everyone at once. 15 to 4320 (72 hours). Useful on a new sending domain or during the first week's warm-up, where each hour of the window must fit the hourly cap; the daily cap still counts the whole send. Left out, the broadcast's saved send_over_minutes applies; given, it is saved on the broadcast. The window must end within 30 days.", - "example": 240 - } - } + "$ref": "#/components/schemas/CreateBroadcastRequest" } } } }, "responses": { - "200": { - "description": "Broadcast queued or scheduled", + "201": { + "description": "Broadcast created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BroadcastSendResult" + "$ref": "#/components/schemas/Broadcast" } } } @@ -9895,16 +10147,6 @@ } } }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -9929,14 +10171,14 @@ "x-sdk-group": [ "broadcasts" ], - "x-sdk-method": "send" + "x-sdk-method": "create" } }, - "/v1/broadcasts/{id}/cancel": { - "post": { - "operationId": "cancelBroadcast", - "summary": "Cancel a scheduled broadcast", - "description": "Cancel a broadcast. A scheduled broadcast is stopped before anything is sent. A queued or sending broadcast stops where it is: emails already accepted are delivered, the rest are dropped and their credits refunded", + "/v1/broadcasts/{id}": { + "get": { + "operationId": "getBroadcast", + "summary": "Get broadcast details", + "description": "Retrieve a broadcast with metrics and the latest recipient rows.", "tags": [ "Broadcasts" ], @@ -9953,21 +10195,11 @@ ], "responses": { "200": { - "description": "Broadcast canceled", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Broadcast" - } - } - } - }, - "400": { - "description": "Bad request - check your payload", + "description": "Broadcast details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/BroadcastDetail" } } } @@ -10026,14 +10258,12 @@ "x-sdk-group": [ "broadcasts" ], - "x-sdk-method": "cancel" - } - }, - "/v1/broadcasts/{id}/archive": { - "post": { - "operationId": "archiveBroadcast", - "summary": "Archive a broadcast", - "description": "Hide a sent, canceled or failed broadcast from the default list. The broadcast, its recipients and its stats stay available, and the list shows it with archived=true. Archiving an archived broadcast changes nothing.", + "x-sdk-method": "get" + }, + "patch": { + "operationId": "updateBroadcast", + "summary": "Update a draft or scheduled broadcast", + "description": "Update broadcast content. Updating a scheduled broadcast cancels the previous schedule and returns it to draft.", "tags": [ "Broadcasts" ], @@ -10048,9 +10278,19 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateBroadcastRequest" + } + } + } + }, "responses": { "200": { - "description": "Broadcast archived", + "description": "Broadcast updated", "content": { "application/json": { "schema": { @@ -10123,14 +10363,12 @@ "x-sdk-group": [ "broadcasts" ], - "x-sdk-method": "archive" - } - }, - "/v1/broadcasts/{id}/unarchive": { - "post": { - "operationId": "unarchiveBroadcast", - "summary": "Unarchive a broadcast", - "description": "Return an archived broadcast to the default list. Unarchiving a broadcast that is not archived changes nothing.", + "x-sdk-method": "update" + }, + "delete": { + "operationId": "deleteBroadcast", + "summary": "Delete a draft broadcast", + "description": "Delete a broadcast that has not been queued or scheduled.", "tags": [ "Broadcasts" ], @@ -10147,11 +10385,21 @@ ], "responses": { "200": { - "description": "Broadcast unarchived", + "description": "Broadcast deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Broadcast" + "$ref": "#/components/schemas/DeletedObject" + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } } @@ -10210,85 +10458,35 @@ "x-sdk-group": [ "broadcasts" ], - "x-sdk-method": "unarchive" + "x-sdk-method": "delete" } }, - "/v1/broadcasts/{id}/test": { - "post": { - "operationId": "sendBroadcastTest", - "summary": "Send a test broadcast email", - "description": "Queue a test email to a single recipient using the broadcast content and personalization sample data.", + "/v1/broadcasts/audience": { + "get": { + "operationId": "getBroadcastAudience", + "summary": "Preview a broadcast audience", + "description": "How many contacts a broadcast to all contacts, or to one segment, would reach after unsubscribes, suppressions and invalid addresses are skipped, plus the plan's broadcast allowance this month. Not in Resend. Requires the email:read scope.", "tags": [ "Broadcasts" ], "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "description": "The broadcast ID", + "name": "segment_id", + "in": "query", + "description": "Preview one segment instead of all contacts", "schema": { "type": "string" } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "to" - ], - "properties": { - "to": { - "type": "string", - "format": "email", - "example": "you@example.com" - } - } - } - } - } - }, "responses": { "200": { - "description": "Test email queued", + "description": "The audience counts", "content": { "application/json": { "schema": { "type": "object", - "required": [ - "object", - "id", - "status" - ], - "properties": { - "object": { - "type": "string", - "example": "email" - }, - "id": { - "type": "string", - "example": "email_abc123", - "description": "The test email, in GET /v1/emails/{id}" - }, - "status": { - "type": "string", - "example": "queued" - } - } - } - } - } - }, - "400": { - "description": "Bad request - check your payload", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" + "additionalProperties": true } } } @@ -10323,16 +10521,6 @@ } } }, - "422": { - "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -10357,65 +10545,100 @@ "x-sdk-group": [ "broadcasts" ], - "x-sdk-method": "sendTest" + "x-sdk-method": "getAudience" } }, - "/v1/templates": { + "/v1/broadcasts/{id}/recipients": { "get": { - "operationId": "listTemplates", - "summary": "List templates", - "description": "List the team's templates, newest first, in Resend's list shape. Page with limit and an after or before id (not both). Rows carry no content; get one template for that. Requires the email:read scope.", + "operationId": "listBroadcastRecipients", + "summary": "List broadcast recipients", + "description": "Resend-compatible: recipients of a broadcast for one event type (sent, delivered, opened, clicked, bounced, complained, unsubscribed, suppressed), newest first, with after/before cursors. Each has contact_id; opened and clicked add count, clicked adds clicked_links, bounced adds bounce_type. Without type, every recipient (not in Resend). Requires the email:read scope.", "tags": [ - "Templates" + "Broadcasts" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The broadcast ID", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "sent", + "delivered", + "opened", + "clicked", + "bounced", + "complained", + "unsubscribed", + "suppressed" + ] + } + }, + { + "name": "bounce_type", + "in": "query", + "description": "With type=bounced only", + "schema": { + "type": "string", + "enum": [ + "permanent", + "transient", + "undetermined" + ] + } + }, + { + "name": "email", + "in": "query", + "description": "Recipients whose address contains this text", + "schema": { + "type": "string" + } + }, { "name": "limit", "in": "query", - "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 - }, - "description": "Number of templates to return." + } }, { "name": "after", "in": "query", - "required": false, + "description": "Return recipients after this recipient ID", "schema": { "type": "string" - }, - "description": "Return templates created before this template id (older). Not included in the result. Cannot be used with before." + } }, { "name": "before", "in": "query", - "required": false, - "schema": { - "type": "string" - }, - "description": "Return templates created after this template id (newer). Cannot be used with after." - }, - { - "name": "q", - "in": "query", - "required": false, + "description": "Return recipients before this recipient ID", "schema": { "type": "string" - }, - "description": "Not in Resend. Search name, alias and subject." + } } ], "responses": { "200": { - "description": "A page of templates", + "description": "Recipients", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateList" + "type": "object", + "additionalProperties": true } } } @@ -10425,7 +10648,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10435,7 +10658,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10445,7 +10668,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10455,7 +10678,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10465,7 +10688,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10475,110 +10698,235 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } } }, "x-sdk-group": [ - "templates" + "broadcasts", + "recipients" ], "x-sdk-method": "list" - }, - "post": { - "operationId": "createTemplate", - "summary": "Create a template", - "description": "Create a template as a draft. Publish it with POST /v1/templates/{id}/publish before sending with it. Same body as Resend's create template; markdown, preview_text and category are SMTPfast additions. Requires the email:send scope.", + } + }, + "/v1/broadcasts/{id}/clicked-links": { + "get": { + "operationId": "listBroadcastClickedLinks", + "summary": "List clicked links", + "description": "Every link clicked in this broadcast, with total clicks and the number of distinct recipients who clicked it. Ordered by total clicks.", "tags": [ - "Templates" + "Broadcasts" ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTemplateRequest" - }, - "example": { - "name": "order-confirmation", - "alias": "order-confirmation", - "subject": "Your order {{{ORDER_ID}}}", - "from": "Acme ", - "html": "

Name: {{{PRODUCT}}}

Total: {{{PRICE}}}

", - "variables": [ - { - "key": "PRODUCT", - "type": "string", - "fallback_value": "item" - }, - { - "key": "PRICE", - "type": "number", - "fallback_value": 25 - } - ] - } + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The broadcast ID", + "schema": { + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Template created as a draft", + "200": { + "description": "The links clicked in this broadcast", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateRef" + "type": "object", + "required": [ + "broadcast_id", + "links", + "total_clicks" + ], + "properties": { + "broadcast_id": { + "type": "string" + }, + "links": { + "type": "array", + "items": { + "type": "object", + "required": [ + "url", + "clicks", + "unique_clicks" + ], + "properties": { + "url": { + "type": "string" + }, + "clicks": { + "type": "integer", + "example": 12 + }, + "unique_clicks": { + "type": "integer", + "example": 9, + "description": "Distinct recipients who clicked" + } + } + } + }, + "total_clicks": { + "type": "integer", + "example": 30 + } + } } } } }, - "400": { - "description": "Bad request - check your payload", - "content": { + "404": { + "description": "Broadcast not found" + } + }, + "x-sdk-group": [ + "broadcasts" + ], + "x-sdk-method": "listClickedLinks" + } + }, + "/v1/broadcasts/{id}/duplicate": { + "post": { + "operationId": "duplicateBroadcast", + "summary": "Duplicate broadcast", + "description": "Copy a broadcast into a new draft. Content and targeting carry over; the schedule, recipients and send history do not. Pass `name` to set the copy's name, otherwise it is the original's with \"(copy)\" appended.", + "tags": [ + "Broadcasts" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The broadcast ID to copy", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "The new draft broadcast", + "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "type": "object", + "required": [ + "broadcast" + ], + "properties": { + "broadcast": { + "$ref": "#/components/schemas/Broadcast" + } + } } } } }, - "401": { - "description": "Invalid or missing API key", + "404": { + "description": "Broadcast not found" + } + }, + "x-sdk-group": [ + "broadcasts" + ], + "x-sdk-method": "duplicate" + } + }, + "/v1/broadcasts/{id}/send": { + "post": { + "operationId": "sendBroadcast", + "summary": "Send or schedule a broadcast", + "description": "Snapshot eligible subscribed contacts, create one queued email per recipient, and send now or schedule within 30 days. With send_over_minutes the recipients go out evenly spread over that window instead of all at once; a canceled broadcast stops the sends that have not gone out yet.", + "tags": [ + "Broadcasts" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The broadcast ID", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "scheduled_at": { + "type": "string", + "format": "date-time" + }, + "send_over_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 4320, + "description": "Not in Resend. Spread the send evenly over this many minutes from the send time (or scheduled_at): recipient i of N goes out at start + i * window / N. 0 sends to everyone at once. 15 to 4320 (72 hours). Useful on a new sending domain or during the first week's warm-up, where each hour of the window must fit the hourly cap; the daily cap still counts the whole send. Left out, the broadcast's saved send_over_minutes applies; given, it is saved on the broadcast. The window must end within 30 days.", + "example": 240 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Broadcast queued or scheduled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/BroadcastSendResult" } } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "400": { + "description": "Bad request - check your payload", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } }, - "422": { - "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } } @@ -10588,7 +10936,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10598,32 +10946,32 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } } }, "x-sdk-group": [ - "templates" + "broadcasts" ], - "x-sdk-method": "create" + "x-sdk-method": "send" } }, - "/v1/templates/{id}": { - "get": { - "operationId": "getTemplate", - "summary": "Get a template", - "description": "Get one template by id or alias. The content fields are the current draft; has_unpublished_versions says whether it differs from the published version that sends use. Requires the email:read scope.", + "/v1/broadcasts/{id}/cancel": { + "post": { + "operationId": "cancelBroadcast", + "summary": "Cancel a scheduled broadcast", + "description": "Cancel a broadcast. A scheduled broadcast is stopped before anything is sent. A queued or sending broadcast stops where it is: emails already accepted are delivered, the rest are dropped and their credits refunded", "tags": [ - "Templates" + "Broadcasts" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The template id or alias", + "description": "The broadcast ID", "schema": { "type": "string" } @@ -10631,11 +10979,21 @@ ], "responses": { "200": { - "description": "The template", + "description": "Broadcast canceled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Template" + "$ref": "#/components/schemas/Broadcast" + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } } @@ -10645,7 +11003,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10655,7 +11013,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10665,7 +11023,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10675,7 +11033,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10685,55 +11043,44 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } } }, "x-sdk-group": [ - "templates" + "broadcasts" ], - "x-sdk-method": "get" - }, - "patch": { - "operationId": "updateTemplate", - "summary": "Update a template", - "description": "Change the draft. Only the fields you send change. Sends keep using the published version until you publish again. Pass expected_updated_at to refuse the change if someone else edited the template since you loaded it. Requires the email:send scope.", + "x-sdk-method": "cancel" + } + }, + "/v1/broadcasts/{id}/archive": { + "post": { + "operationId": "archiveBroadcast", + "summary": "Archive a broadcast", + "description": "Hide a sent, canceled or failed broadcast from the default list. The broadcast, its recipients and its stats stay available, and the list shows it with archived=true. Archiving an archived broadcast changes nothing.", "tags": [ - "Templates" + "Broadcasts" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The template id or alias", + "description": "The broadcast ID", "schema": { "type": "string" } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTemplateRequest" - }, - "example": { - "html": "

Total: {{{PRICE}}}

Name: {{{PRODUCT}}}

" - } - } - } - }, "responses": { "200": { - "description": "Draft updated", + "description": "Broadcast archived", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateRef" + "$ref": "#/components/schemas/Broadcast" } } } @@ -10743,7 +11090,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10753,7 +11100,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10763,7 +11110,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10773,17 +11120,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" - } - } - } - }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10793,7 +11130,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10803,30 +11140,32 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } } }, "x-sdk-group": [ - "templates" + "broadcasts" ], - "x-sdk-method": "update" - }, - "delete": { - "operationId": "deleteTemplate", - "summary": "Delete a template", - "description": "Delete a template by id or alias. Emails already sent with it are not affected. Requires the email:send scope.", + "x-sdk-method": "archive" + } + }, + "/v1/broadcasts/{id}/unarchive": { + "post": { + "operationId": "unarchiveBroadcast", + "summary": "Unarchive a broadcast", + "description": "Return an archived broadcast to the default list. Unarchiving a broadcast that is not archived changes nothing.", "tags": [ - "Templates" + "Broadcasts" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The template id or alias", + "description": "The broadcast ID", "schema": { "type": "string" } @@ -10834,11 +11173,11 @@ ], "responses": { "200": { - "description": "Template deleted", + "description": "Broadcast unarchived", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateDeleted" + "$ref": "#/components/schemas/Broadcast" } } } @@ -10848,7 +11187,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10858,7 +11197,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10868,7 +11207,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10878,7 +11217,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10888,48 +11227,51 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } } }, "x-sdk-group": [ - "templates" + "broadcasts" ], - "x-sdk-method": "delete" + "x-sdk-method": "unarchive" } }, - "/v1/templates/{id}/publish": { + "/v1/broadcasts/{id}/test": { "post": { - "operationId": "publishTemplate", - "summary": "Publish a template", - "description": "Publish the current draft. From then on, sends that name this template render this version; later edits change only the draft until the next publish. Pass the updated_at you loaded as expected_updated_at to publish only that version: if the template changed since, it answers 409 revision_conflict. Requires the email:send scope.", + "operationId": "sendBroadcastTest", + "summary": "Send a test broadcast email", + "description": "Queue a test email to a single recipient using the broadcast content and personalization sample data.", "tags": [ - "Templates" + "Broadcasts" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The template id or alias", + "description": "The broadcast ID", "schema": { "type": "string" } } ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "to" + ], "properties": { - "expected_updated_at": { + "to": { "type": "string", - "format": "date-time", - "description": "Not in Resend. The updated_at you loaded." + "format": "email", + "example": "you@example.com" } } } @@ -10938,51 +11280,71 @@ }, "responses": { "200": { - "description": "Template published", + "description": "Test email queued", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateRef" + "type": "object", + "required": [ + "object", + "id", + "status" + ], + "properties": { + "object": { + "type": "string", + "example": "email" + }, + "id": { + "type": "string", + "example": "email_abc123", + "description": "The test email, in GET /v1/emails/{id}" + }, + "status": { + "type": "string", + "example": "queued" + } + } } } } }, - "401": { - "description": "Invalid or missing API key", + "400": { + "description": "Bad request - check your payload", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } }, - "404": { - "description": "Resource not found", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", + "404": { + "description": "Resource not found", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -10992,7 +11354,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -11002,7 +11364,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } @@ -11012,50 +11374,80 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateError" + "$ref": "#/components/schemas/Error" } } } } }, "x-sdk-group": [ - "templates" + "broadcasts" ], - "x-sdk-method": "publish" + "x-sdk-method": "sendTest" } }, - "/v1/templates/{id}/duplicate": { - "post": { - "operationId": "duplicateTemplate", - "summary": "Duplicate a template", - "description": "Copy a template's draft into a new draft template named with \"(copy)\" appended. The alias is not copied and nothing is published. Requires the email:send scope.", + "/v1/templates": { + "get": { + "operationId": "listTemplates", + "summary": "List templates", + "description": "List the team's templates, newest first, in Resend's list shape. Page with limit and an after or before id (not both). Rows carry no content; get one template for that. Requires the email:read scope.", "tags": [ "Templates" ], "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "description": "The template id or alias", + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "Number of templates to return." + }, + { + "name": "after", + "in": "query", + "required": false, "schema": { "type": "string" - } + }, + "description": "Return templates created before this template id (older). Not included in the result. Cannot be used with before." + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Return templates created after this template id (newer). Cannot be used with after." + }, + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Not in Resend. Search name, alias and subject." } ], "responses": { - "201": { - "description": "The new template", + "200": { + "description": "A page of templates", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateRef" + "$ref": "#/components/schemas/TemplateList" } } } }, - "401": { - "description": "Invalid or missing API key", + "400": { + "description": "Bad request - check your payload", "content": { "application/json": { "schema": { @@ -11064,8 +11456,8 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -11074,8 +11466,8 @@ } } }, - "404": { - "description": "Resource not found", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { @@ -11084,8 +11476,8 @@ } } }, - "422": { - "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", + "404": { + "description": "Resource not found", "content": { "application/json": { "schema": { @@ -11118,34 +11510,51 @@ "x-sdk-group": [ "templates" ], - "x-sdk-method": "duplicate" - } - }, - "/v1/domains": { + "x-sdk-method": "list" + }, "post": { - "operationId": "addDomain", - "summary": "Add a domain", - "description": "Register a new sending domain. Returns the DNS records that must be configured before the domain can be verified.", + "operationId": "createTemplate", + "summary": "Create a template", + "description": "Create a template as a draft. Publish it with POST /v1/templates/{id}/publish before sending with it. Same body as Resend's create template; markdown, preview_text and category are SMTPfast additions. Requires the email:send scope.", "tags": [ - "Domains" + "Templates" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddDomainRequest" + "$ref": "#/components/schemas/CreateTemplateRequest" + }, + "example": { + "name": "order-confirmation", + "alias": "order-confirmation", + "subject": "Your order {{{ORDER_ID}}}", + "from": "Acme ", + "html": "

Name: {{{PRODUCT}}}

Total: {{{PRICE}}}

", + "variables": [ + { + "key": "PRODUCT", + "type": "string", + "fallback_value": "item" + }, + { + "key": "PRICE", + "type": "number", + "fallback_value": 25 + } + ] } } } }, "responses": { - "200": { - "description": "Domain created with DNS records to configure", + "201": { + "description": "Template created as a draft", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/TemplateRef" } } } @@ -11155,7 +11564,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11165,37 +11574,37 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "402": { - "description": "Plan limit reached - upgrade the plan to add more", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", + "422": { + "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11205,7 +11614,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11215,52 +11624,44 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } } }, "x-sdk-group": [ - "domains" + "templates" ], "x-sdk-method": "create" - }, + } + }, + "/v1/templates/{id}": { "get": { - "operationId": "listDomains", - "summary": "List domains", - "description": "Return all sending domains associated with the current account.", + "operationId": "getTemplate", + "summary": "Get a template", + "description": "Get one template by id or alias. The content fields are the current draft; has_unpublished_versions says whether it differs from the published version that sends use. Requires the email:read scope.", "tags": [ - "Domains" + "Templates" ], - "responses": { - "200": { - "description": "List of domains", - "content": { - "application/json": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The template id or alias", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The template", + "content": { + "application/json": { "schema": { - "type": "object", - "required": [ - "object", - "has_more", - "data" - ], - "properties": { - "object": { - "type": "string", - "example": "list" - }, - "has_more": { - "type": "boolean", - "example": false - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DomainListItem" - } - } - } + "$ref": "#/components/schemas/Template" } } } @@ -11270,7 +11671,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateError" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateError" } } } @@ -11280,7 +11701,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11290,45 +11711,65 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } } }, "x-sdk-group": [ - "domains" + "templates" ], - "x-sdk-method": "list" - } - }, - "/v1/domains/{id}": { - "delete": { - "operationId": "deleteDomain", - "summary": "Delete a domain", - "description": "Resend-compatible. Removes the domain and its sending identity at the provider. Mail from it stops at once. Requires the domain:write scope and an owner or admin.", + "x-sdk-method": "get" + }, + "patch": { + "operationId": "updateTemplate", + "summary": "Update a template", + "description": "Change the draft. Only the fields you send change. Sends keep using the published version until you publish again. Pass expected_updated_at to refuse the change if someone else edited the template since you loaded it. Requires the email:send scope.", "tags": [ - "Domains" + "Templates" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The domain ID", + "description": "The template id or alias", "schema": { "type": "string" } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTemplateRequest" + }, + "example": { + "html": "

Total: {{{PRICE}}}

Name: {{{PRODUCT}}}

" + } + } + } + }, "responses": { "200": { - "description": "Deleted", + "description": "Draft updated", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/TemplateRef" + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateError" } } } @@ -11338,7 +11779,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11348,7 +11789,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11358,60 +11799,60 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "500": { - "description": "Internal server error", + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "502": { - "description": "Error", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } } }, "x-sdk-group": [ - "domains" + "templates" ], - "x-sdk-method": "delete" + "x-sdk-method": "update" }, - "get": { - "operationId": "getDomain", - "summary": "Get domain details", - "description": "Retrieve details for a single domain including its current verification status and DNS records.", + "delete": { + "operationId": "deleteTemplate", + "summary": "Delete a template", + "description": "Delete a template by id or alias. Emails already sent with it are not affected. Requires the email:send scope.", "tags": [ - "Domains" + "Templates" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The domain ID (e.g. dom_xyz789)", + "description": "The template id or alias", "schema": { "type": "string" } @@ -11419,11 +11860,11 @@ ], "responses": { "200": { - "description": "Domain details", + "description": "Template deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/TemplateDeleted" } } } @@ -11433,7 +11874,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateError" } } } @@ -11443,7 +11894,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11453,7 +11904,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11463,48 +11914,48 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } } }, "x-sdk-group": [ - "domains" + "templates" ], - "x-sdk-method": "get" - }, - "patch": { - "operationId": "updateDomainReceiving", - "summary": "Turn inbound receiving on or off", - "description": "Enable or disable inbound email for a verified domain (paid plans). Returns the MX record to publish and the receiving status: disabled, pending, active or failed. Requires the domain:write scope.", + "x-sdk-method": "delete" + } + }, + "/v1/templates/{id}/publish": { + "post": { + "operationId": "publishTemplate", + "summary": "Publish a template", + "description": "Publish the current draft. From then on, sends that name this template render this version; later edits change only the draft until the next publish. Pass the updated_at you loaded as expected_updated_at to publish only that version: if the template changed since, it answers 409 revision_conflict. Requires the email:send scope.", "tags": [ - "Domains", - "Receiving" + "Templates" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The domain ID (e.g. dom_xyz789)", + "description": "The template id or alias", "schema": { "type": "string" } } ], "requestBody": { - "required": true, + "required": false, "content": { "application/json": { "schema": { "type": "object", - "required": [ - "receiving_enabled" - ], "properties": { - "receiving_enabled": { - "type": "boolean" + "expected_updated_at": { + "type": "string", + "format": "date-time", + "description": "Not in Resend. The updated_at you loaded." } } } @@ -11513,29 +11964,11 @@ }, "responses": { "200": { - "description": "Receiving settings", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "receiving": { - "$ref": "#/components/schemas/DomainReceiving" - } - } - } - } - } - }, - "400": { - "description": "Bad request - check your payload", + "description": "Template published", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateRef" } } } @@ -11545,17 +11978,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "402": { - "description": "Plan limit reached - upgrade the plan to add more", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11565,7 +11988,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11575,7 +11998,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11585,146 +12008,124 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "422": { + "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "500": { - "description": "Internal server error", - "content": { + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "503": { - "description": "Error", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } } }, "x-sdk-group": [ - "domains" + "templates" ], - "x-sdk-method": "update" + "x-sdk-method": "publish" } }, - "/v1/domains/claim": { - "get": { - "operationId": "getDomainClaimRecord", - "summary": "Get the claim record for a domain", - "description": "The TXT record a team publishes to prove DNS control before claiming a domain another account added but never verified. 404 when nobody holds the domain, 409 with code already_yours or verified_elsewhere when it cannot be claimed. Requires the domain:read scope.", + "/v1/templates/{id}/duplicate": { + "post": { + "operationId": "duplicateTemplate", + "summary": "Duplicate a template", + "description": "Copy a template's draft into a new draft template named with \"(copy)\" appended. The alias is not copied and nothing is published. Requires the email:send scope.", "tags": [ - "Domains" + "Templates" ], "parameters": [ { - "name": "domain", - "in": "query", + "name": "id", + "in": "path", "required": true, + "description": "The template id or alias", "schema": { "type": "string" } } ], "responses": { - "200": { - "description": "Record to publish", + "201": { + "description": "The new template", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "domain": { - "type": "string" - }, - "record": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "TXT" - }, - "name": { - "type": "string" - }, - "value": { - "type": "string" - }, - "ttl": { - "type": "string" - } - } - } - } + "$ref": "#/components/schemas/TemplateRef" } } } }, - "400": { - "description": "Bad request - check your payload", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "401": { - "description": "Invalid or missing API key", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "404": { + "description": "Resource not found", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "404": { - "description": "Resource not found", + "422": { + "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } @@ -11734,21 +12135,23 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/TemplateError" } } } } }, "x-sdk-group": [ - "domains" + "templates" ], - "x-sdk-method": "getClaimRecord" - }, + "x-sdk-method": "duplicate" + } + }, + "/v1/domains": { "post": { - "operationId": "claimDomain", - "summary": "Claim a domain held by another account", - "description": "Checks the claim TXT record and moves a never-verified domain from the account that holds it to the caller's team, with its verification reset and any mail the previous holder still had queued through it cancelled. POST /v1/domains answers 409 with code domain_claimable and the record when this applies. 422 claim_record_missing when the record is not visible, 503 dns_unavailable when the lookup itself failed. The previous holder is notified. Requires the domain:write scope; 20 checks per hour per team.", + "operationId": "addDomain", + "summary": "Add a domain", + "description": "Register a new sending domain. Returns the DNS records that must be configured before the domain can be verified.", "tags": [ "Domains" ], @@ -11757,46 +12160,18 @@ "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "domain" - ], - "properties": { - "domain": { - "type": "string", - "example": "acme.com" - } - } + "$ref": "#/components/schemas/AddDomainRequest" } } } }, "responses": { "200": { - "description": "Domain moved to the caller's team, unverified", + "description": "Domain created with DNS records to configure", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "domain": { - "type": "string" - }, - "status": { - "type": "string", - "example": "pending" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "message": { - "type": "string" - } - } + "$ref": "#/components/schemas/Domain" } } } @@ -11841,8 +12216,8 @@ } } }, - "404": { - "description": "Resource not found", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", "content": { "application/json": { "schema": { @@ -11851,8 +12226,8 @@ } } }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { @@ -11861,8 +12236,8 @@ } } }, - "422": { - "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -11870,9 +12245,54 @@ } } } + } + }, + "x-sdk-group": [ + "domains" + ], + "x-sdk-method": "create" + }, + "get": { + "operationId": "listDomains", + "summary": "List domains", + "description": "Return all sending domains associated with the current account.", + "tags": [ + "Domains" + ], + "responses": { + "200": { + "description": "List of domains", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "object", + "has_more", + "data" + ], + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean", + "example": false + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DomainListItem" + } + } + } + } + } + } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -11881,8 +12301,8 @@ } } }, - "500": { - "description": "Internal server error", + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { @@ -11891,8 +12311,8 @@ } } }, - "503": { - "description": "Error", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -11905,14 +12325,14 @@ "x-sdk-group": [ "domains" ], - "x-sdk-method": "claim" + "x-sdk-method": "list" } }, - "/v1/domains/{id}/verify": { - "post": { - "operationId": "verifyDomain", - "summary": "Verify a domain", - "description": "Check whether the required DNS records have been configured correctly. If verification succeeds, the domain status changes to verified and it can be used for sending.", + "/v1/domains/{id}": { + "delete": { + "operationId": "deleteDomain", + "summary": "Delete a domain", + "description": "Resend-compatible. Removes the domain and its sending identity at the provider. Mail from it stops at once. Requires the domain:write scope and an owner or admin.", "tags": [ "Domains" ], @@ -11921,7 +12341,7 @@ "name": "id", "in": "path", "required": true, - "description": "The domain ID (e.g. dom_xyz789)", + "description": "The domain ID", "schema": { "type": "string" } @@ -11929,57 +12349,12 @@ ], "responses": { "200": { - "description": "Verification result", + "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", - "properties": { - "domain": { - "type": "string", - "example": "mail.example.com" - }, - "status": { - "type": "string", - "example": "verified" - }, - "verification_phase": { - "type": "string" - }, - "checks": { - "type": "object", - "properties": { - "dkim": { - "type": "string" - }, - "spf": { - "type": "string" - }, - "dmarc": { - "type": "string" - }, - "mail_from": { - "type": "string" - } - } - }, - "mail_from_domain": { - "type": [ - "string", - "null" - ] - }, - "ses_configured": { - "type": "boolean" - }, - "dkim_check_restarted": { - "type": "boolean", - "description": "True when the email provider had stopped checking DKIM and this request restarted the check." - }, - "message": { - "type": "string" - } - } + "additionalProperties": true } } } @@ -11994,28 +12369,8 @@ } } }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "500": { - "description": "Internal server error", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { @@ -12023,45 +12378,9 @@ } } } - } - }, - "x-sdk-group": [ - "domains" - ], - "x-sdk-method": "verify" - } - }, - "/v1/api-keys": { - "post": { - "operationId": "createApiKey", - "summary": "Create API key", - "description": "Generate a new API key. The full key value is returned only once in the response. Store it securely.", - "tags": [ - "API Keys" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateApiKeyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "API key created (includes the full key value)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyCreated" - } - } - } }, - "400": { - "description": "Bad request - check your payload", + "404": { + "description": "Resource not found", "content": { "application/json": { "schema": { @@ -12070,8 +12389,8 @@ } } }, - "401": { - "description": "Invalid or missing API key", + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { @@ -12080,8 +12399,8 @@ } } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -12090,8 +12409,8 @@ } } }, - "500": { - "description": "Internal server error", + "502": { + "description": "Error", "content": { "application/json": { "schema": { @@ -12102,45 +12421,35 @@ } }, "x-sdk-group": [ - "apiKeys" + "domains" ], - "x-sdk-method": "create" + "x-sdk-method": "delete" }, "get": { - "operationId": "listApiKeys", - "summary": "List API keys", - "description": "Return all API keys for the current account. The full key value is never included; only the prefix is shown.", + "operationId": "getDomain", + "summary": "Get domain details", + "description": "Retrieve details for a single domain including its current verification status and DNS records.", "tags": [ - "API Keys" + "Domains" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The domain ID (e.g. dom_xyz789)", + "schema": { + "type": "string" + } + } ], "responses": { "200": { - "description": "List of API keys", + "description": "Domain details", "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "object", - "has_more", - "data" - ], - "properties": { - "object": { - "type": "string", - "example": "list" - }, - "has_more": { - "type": "boolean", - "example": false - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApiKey" - } - } - } + "$ref": "#/components/schemas/Domain" } } } @@ -12155,6 +12464,16 @@ } } }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -12177,25 +12496,24 @@ } }, "x-sdk-group": [ - "apiKeys" + "domains" ], - "x-sdk-method": "list" - } - }, - "/v1/api-keys/{id}": { + "x-sdk-method": "get" + }, "patch": { - "operationId": "updateApiKey", - "summary": "Update API key", - "description": "Rename a key or change its scopes. The secret is unchanged; scope changes apply on the key's next request. Requires the apikey:manage scope when called with an API key.", + "operationId": "updateDomainReceiving", + "summary": "Turn inbound receiving on or off", + "description": "Enable or disable inbound email for a verified domain (paid plans). Returns the MX record to publish and the receiving status: disabled, pending, active or failed. Requires the domain:write scope.", "tags": [ - "API Keys" + "Domains", + "Receiving" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The API key ID", + "description": "The domain ID (e.g. dom_xyz789)", "schema": { "type": "string" } @@ -12207,17 +12525,12 @@ "application/json": { "schema": { "type": "object", + "required": [ + "receiving_enabled" + ], "properties": { - "name": { - "type": "string", - "maxLength": 100 - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Replaces the key's scopes" + "receiving_enabled": { + "type": "boolean" } } } @@ -12226,7 +12539,7 @@ }, "responses": { "200": { - "description": "The updated key", + "description": "Receiving settings", "content": { "application/json": { "schema": { @@ -12235,17 +12548,8 @@ "id": { "type": "string" }, - "name": { - "type": "string" - }, - "keyPrefix": { - "type": "string" - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - } + "receiving": { + "$ref": "#/components/schemas/DomainReceiving" } } } @@ -12272,6 +12576,16 @@ } } }, + "402": { + "description": "Plan limit reached - upgrade the plan to add more", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "Forbidden - domain not verified or insufficient permissions", "content": { @@ -12292,6 +12606,16 @@ } } }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -12311,26 +12635,37 @@ } } } + }, + "503": { + "description": "Stripe could not be reached - retry shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ - "apiKeys" + "domains" ], "x-sdk-method": "update" - }, - "delete": { - "operationId": "revokeApiKey", - "summary": "Revoke API key", - "description": "Permanently revoke an API key. Any requests using this key will be rejected immediately.", + } + }, + "/v1/domains/claim": { + "get": { + "operationId": "getDomainClaimRecord", + "summary": "Get the claim record for a domain", + "description": "The TXT record a team publishes to prove DNS control before claiming a domain another account added but never verified. 404 when nobody holds the domain, 409 with code already_yours or verified_elsewhere when it cannot be claimed. Requires the domain:read scope.", "tags": [ - "API Keys" + "Domains" ], "parameters": [ { - "name": "id", - "in": "path", + "name": "domain", + "in": "query", "required": true, - "description": "The API key ID (e.g. key_abc123)", "schema": { "type": "string" } @@ -12338,30 +12673,48 @@ ], "responses": { "200": { - "description": "API key revoked", + "description": "Record to publish", "content": { "application/json": { "schema": { "type": "object", - "required": [ - "success", - "message" - ], "properties": { - "success": { - "type": "boolean", - "example": true + "domain": { + "type": "string" }, - "message": { - "type": "string", - "example": "API key revoked", - "description": "\"Connected app disconnected\" for a connected app's key" + "record": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "TXT" + }, + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "ttl": { + "type": "string" + } + } } } } } } }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "401": { "description": "Invalid or missing API key", "content": { @@ -12372,6 +12725,16 @@ } } }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "404": { "description": "Resource not found", "content": { @@ -12382,8 +12745,8 @@ } } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", "content": { "application/json": { "schema": { @@ -12404,36 +12767,62 @@ } }, "x-sdk-group": [ - "apiKeys" + "domains" ], - "x-sdk-method": "revoke" - } - }, - "/v1/webhooks": { + "x-sdk-method": "getClaimRecord" + }, "post": { - "operationId": "createWebhook", - "summary": "Create webhook", - "description": "Register a new webhook endpoint. SMTPfast will POST event payloads to this URL when the specified events occur.", + "operationId": "claimDomain", + "summary": "Claim a domain held by another account", + "description": "Checks the claim TXT record and moves a never-verified domain from the account that holds it to the caller's team, with its verification reset and any mail the previous holder still had queued through it cancelled. POST /v1/domains answers 409 with code domain_claimable and the record when this applies. 422 claim_record_missing when the record is not visible, 503 dns_unavailable when the lookup itself failed. The previous holder is notified. Requires the domain:write scope; 20 checks per hour per team.", "tags": [ - "Webhooks" + "Domains" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWebhookRequest" + "type": "object", + "required": [ + "domain" + ], + "properties": { + "domain": { + "type": "string", + "example": "acme.com" + } + } } } } }, "responses": { - "201": { - "description": "Webhook created", + "200": { + "description": "Domain moved to the caller's team, unverified", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Webhook" + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "status": { + "type": "string", + "example": "pending" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "message": { + "type": "string" + } + } } } } @@ -12478,6 +12867,36 @@ } } }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -12497,46 +12916,94 @@ } } } + }, + "503": { + "description": "Stripe could not be reached - retry shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ - "webhooks" + "domains" ], - "x-sdk-method": "create" - }, - "get": { - "operationId": "listWebhooks", - "summary": "List webhooks", - "description": "Return all webhook endpoints configured for the current account.", + "x-sdk-method": "claim" + } + }, + "/v1/domains/{id}/verify": { + "post": { + "operationId": "verifyDomain", + "summary": "Verify a domain", + "description": "Check whether the required DNS records have been configured correctly. If verification succeeds, the domain status changes to verified and it can be used for sending.", "tags": [ - "Webhooks" + "Domains" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The domain ID (e.g. dom_xyz789)", + "schema": { + "type": "string" + } + } ], "responses": { "200": { - "description": "List of webhooks", + "description": "Verification result", "content": { "application/json": { "schema": { "type": "object", - "required": [ - "object", - "has_more", - "data" - ], "properties": { - "object": { + "domain": { "type": "string", - "example": "list" + "example": "mail.example.com" }, - "has_more": { - "type": "boolean", - "example": false + "status": { + "type": "string", + "example": "verified" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Webhook" + "verification_phase": { + "type": "string" + }, + "checks": { + "type": "object", + "properties": { + "dkim": { + "type": "string" + }, + "spf": { + "type": "string" + }, + "dmarc": { + "type": "string" + }, + "mail_from": { + "type": "string" + } } + }, + "mail_from_domain": { + "type": [ + "string", + "null" + ] + }, + "ses_configured": { + "type": "boolean" + }, + "dkim_check_restarted": { + "type": "boolean", + "description": "True when the email provider had stopped checking DKIM and this request restarted the check." + }, + "message": { + "type": "string" } } } @@ -12553,6 +13020,16 @@ } } }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -12575,149 +13052,176 @@ } }, "x-sdk-group": [ - "webhooks" + "domains" ], - "x-sdk-method": "list" + "x-sdk-method": "verify" } }, - "/v1/webhooks/{id}": { - "get": { - "operationId": "getWebhook", - "summary": "Get webhook", - "description": "Retrieve a single webhook endpoint by ID.", + "/v1/api-keys": { + "post": { + "operationId": "createApiKey", + "summary": "Create API key", + "description": "Generate a new API key. The full key value is returned only once in the response. Store it securely.", "tags": [ - "Webhooks" + "API Keys" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID (e.g. wh_abc123)", - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyRequest" + } } } - ], + }, "responses": { "200": { - "description": "The webhook", + "description": "API key created (includes the full key value)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Webhook" + "$ref": "#/components/schemas/ApiKeyCreated" } } } }, - "404": { - "description": "Webhook not found" - } - }, - "x-sdk-group": [ - "webhooks" - ], - "x-sdk-method": "get" - }, - "delete": { - "operationId": "deleteWebhook", - "summary": "Delete webhook", - "description": "Remove a webhook endpoint. Its recorded deliveries are removed with it.", - "tags": [ - "Webhooks" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID (e.g. wh_abc123)", - "schema": { - "type": "string" + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } } - } - ], - "responses": { - "200": { - "description": "Deleted", + }, + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Success" + "$ref": "#/components/schemas/Error" } } } }, - "404": { - "description": "Webhook not found" + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ - "webhooks" + "apiKeys" ], - "x-sdk-method": "delete" + "x-sdk-method": "create" }, - "patch": { - "operationId": "patchWebhook", - "summary": "Update webhook", - "description": "Same as PUT: a partial update that leaves out any field you omit. Accepted because Resend uses PATCH for this, so ported code works unchanged.", + "get": { + "operationId": "listApiKeys", + "summary": "List API keys", + "description": "Return all API keys for the current account. The full key value is never included; only the prefix is shown.", "tags": [ - "Webhooks" + "API Keys" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID (e.g. wh_abc123)", - "schema": { - "type": "string" + "responses": { + "200": { + "description": "List of API keys", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "object", + "has_more", + "data" + ], + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean", + "example": false + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKey" + } + } + } + } + } } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWebhookRequest" + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } } } - } - }, - "responses": { - "200": { - "description": "The updated webhook", + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookUpdated" + "$ref": "#/components/schemas/Error" } } } }, - "404": { - "description": "Webhook not found" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } }, "x-sdk-group": [ - "webhooks" + "apiKeys" ], - "x-sdk-method": "update" - }, - "put": { - "operationId": "updateWebhook", - "summary": "Update webhook", - "description": "Update the URL, events, or active status of an existing webhook endpoint.", + "x-sdk-method": "list" + } + }, + "/v1/api-keys/{id}": { + "patch": { + "operationId": "updateApiKey", + "summary": "Update API key", + "description": "Rename a key or change its scopes. The secret is unchanged; scope changes apply on the key's next request. Requires the apikey:manage scope when called with an API key.", "tags": [ - "Webhooks" + "API Keys" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The webhook ID (e.g. wh_abc123)", + "description": "The API key ID", "schema": { "type": "string" } @@ -12728,18 +13232,48 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateWebhookRequest" + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Replaces the key's scopes" + } + } } } } }, "responses": { "200": { - "description": "Webhook updated", + "description": "The updated key", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookUpdated" + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "keyPrefix": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + } + } } } } @@ -12764,6 +13298,16 @@ } } }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "404": { "description": "Resource not found", "content": { @@ -12796,25 +13340,23 @@ } }, "x-sdk-group": [ - "webhooks" + "apiKeys" ], - "x-sdk-method": "replace" - } - }, - "/v1/webhooks/{id}/test": { - "post": { - "operationId": "testWebhook", - "summary": "Test webhook", - "description": "Send a test event payload to the webhook URL and return the delivery result. Useful for verifying that your endpoint is reachable and responds correctly.", + "x-sdk-method": "update" + }, + "delete": { + "operationId": "revokeApiKey", + "summary": "Revoke API key", + "description": "Permanently revoke an API key. Any requests using this key will be rejected immediately.", "tags": [ - "Webhooks" + "API Keys" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The webhook ID (e.g. wh_abc123)", + "description": "The API key ID (e.g. key_abc123)", "schema": { "type": "string" } @@ -12822,28 +13364,24 @@ ], "responses": { "200": { - "description": "Test delivery result", + "description": "API key revoked", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "success", + "message" + ], "properties": { "success": { "type": "boolean", - "description": "Whether the test payload was delivered successfully" - }, - "status": { - "type": [ - "integer", - "null" - ], - "description": "HTTP status code returned by the webhook endpoint, null when it did not answer" + "example": true }, "message": { - "type": "string" - }, - "delivery": { - "$ref": "#/components/schemas/WebhookDelivery" + "type": "string", + "example": "API key revoked", + "description": "\"Connected app disconnected\" for a connected app's key" } } } @@ -12892,84 +13430,101 @@ } }, "x-sdk-group": [ - "webhooks" + "apiKeys" ], - "x-sdk-method": "test" + "x-sdk-method": "revoke" } }, - "/v1/webhooks/{id}/deliveries": { - "get": { - "operationId": "listWebhookDeliveries", - "summary": "List webhook deliveries", - "description": "Delivery log of one webhook, newest first: every event we POSTed with its attempts, status code, response time, response snippet and error. Failed deliveries are retried after 1s, 30s, 5min and 30min; entries are kept for 30 days. Requires the webhook:read scope.", + "/v1/cli/device": { + "post": { + "operationId": "createCliDeviceCode", + "summary": "Start a CLI login", + "description": "Start a device login for a command-line tool. Show the user_code and verification_uri to the person, then poll POST /v1/cli/token every interval seconds with the device_code. A team owner or admin approves the code on the dashboard and picks the team. No API key: this is how a CLI gets its first one. The flow follows RFC 8628 (device authorization grant) with JSON bodies. Limited to 10 a quarter hour per network.", "tags": [ - "Webhooks" + "CLI Login" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID", - "schema": { - "type": "string" - } - }, - { - "name": "status", - "in": "query", - "required": false, - "schema": { - "type": "string", - "enum": [ - "pending", - "sending", - "succeeded", - "failed" - ] - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 + "security": [], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "client_name": { + "type": "string", + "maxLength": 60, + "example": "SMTPfast CLI 0.15.0", + "description": "Shown on the approval page. Default: SMTPfast CLI" + }, + "hostname": { + "type": "string", + "maxLength": 63, + "example": "build-01", + "description": "The machine's name; the key is called \"CLI on ()\"" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "email:send", + "email:read", + "domain:read", + "domain:write" + ], + "description": "Scopes for the key. Default: the default API key scopes. The approver's role caps them: billing scopes are granted only for a team the approver owns." + } + } + } } - }, - { - "name": "after", - "in": "query", - "required": false, - "schema": { - "type": "string" - }, - "description": "Return deliveries older than this id" } - ], + }, "responses": { "200": { - "description": "Deliveries", + "description": "Codes for the person and for polling", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "device_code", + "user_code", + "verification_uri", + "verification_uri_complete", + "expires_in", + "interval" + ], "properties": { - "object": { + "device_code": { "type": "string", - "example": "list" + "description": "Secret. Send it to POST /v1/cli/token; never show it." }, - "has_more": { - "type": "boolean" + "user_code": { + "type": "string", + "example": "BCDF-GHJK", + "description": "For the person to check or type on the approval page" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookDelivery" - } + "verification_uri": { + "type": "string", + "format": "uri", + "example": "https://smtpfa.st/cli/device" + }, + "verification_uri_complete": { + "type": "string", + "format": "uri", + "example": "https://smtpfa.st/cli/device?code=BCDF-GHJK" + }, + "expires_in": { + "type": "integer", + "example": 600, + "description": "Seconds until both codes expire" + }, + "interval": { + "type": "integer", + "example": 5, + "description": "Seconds to wait between polls" } } } @@ -12986,36 +13541,6 @@ } } }, - "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -13038,76 +13563,138 @@ } }, "x-sdk-group": [ - "webhooks", - "deliveries" + "cli" ], - "x-sdk-method": "list" + "x-sdk-method": "createDeviceCode" } }, - "/v1/webhooks/{id}/deliveries/{delivery_id}": { - "get": { - "operationId": "getWebhookDelivery", - "summary": "Get a webhook delivery", - "description": "One delivery with the exact payload that was sent. Requires the webhook:read scope.", + "/v1/cli/token": { + "post": { + "operationId": "exchangeCliDeviceCode", + "summary": "Collect the CLI login key", + "description": "Poll with the device_code from POST /v1/cli/device. Until the code is approved this answers 400 with an RFC 8628 error. Once approved it returns a new API key, once, and the code is used up; presenting it again revokes that key. No API key: this is how a CLI gets its first one. The flow follows RFC 8628 (device authorization grant) with JSON bodies.", "tags": [ - "Webhooks" + "CLI Login" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID", - "schema": { - "type": "string" - } - }, - { - "name": "delivery_id", - "in": "path", - "required": true, - "description": "The delivery ID", - "schema": { - "type": "string" + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "device_code" + ], + "properties": { + "device_code": { + "type": "string" + } + } + } } } - ], + }, "responses": { "200": { - "description": "Delivery", + "description": "The new API key. The key value is returned only here.", "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/WebhookDelivery" + "type": "object", + "required": [ + "id", + "name", + "key", + "prefix", + "scopes", + "created_at", + "team" + ], + "properties": { + "id": { + "type": "string" }, - { + "name": { + "type": "string", + "example": "CLI on build-01 (2026-10-29)" + }, + "key": { + "type": "string", + "example": "sf_a1b2c3...", + "description": "The full key. Store it; it cannot be read again." + }, + "prefix": { + "type": "string", + "example": "sf_a1b2c3d4e" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "team": { "type": "object", + "required": [ + "id", + "name" + ], "properties": { - "payload": { - "type": "string", - "description": "The JSON body as sent" + "id": { + "type": "string" + }, + "name": { + "type": "string" } } } - ] + } } } } }, - "401": { - "description": "Invalid or missing API key", + "400": { + "description": "Not approved yet, polled too soon, expired, denied or unknown", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "type": "object", + "required": [ + "error", + "error_description" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "authorization_pending", + "slow_down", + "expired_token", + "access_denied", + "invalid_grant" + ], + "description": "authorization_pending: nobody has decided yet, poll again after interval seconds. slow_down: polled too soon, wait the new interval. expired_token: start again. access_denied: the person denied it, or can no longer create the key. invalid_grant: unknown code, or one already used (the key it issued is then revoked)." + }, + "error_description": { + "type": "string" + }, + "interval": { + "type": "integer", + "example": 10, + "description": "With slow_down: the seconds to wait between polls from now on" + } + } } } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { @@ -13116,28 +13703,8 @@ } } }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "500": { - "description": "Internal server error", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -13148,64 +13715,98 @@ } }, "x-sdk-group": [ - "webhooks", - "deliveries" + "cli" ], - "x-sdk-method": "get" + "x-sdk-method": "exchangeDeviceCode" } }, - "/v1/webhooks/{id}/deliveries/{delivery_id}/retry": { + "/v1/cli/signup": { "post": { - "operationId": "retryWebhookDelivery", - "summary": "Retry a webhook delivery", - "description": "Send the same payload again, once, right now, and return the result of that attempt. A delivery with a scheduled attempt still pending returns 409. Requires the webhook:write scope.", + "operationId": "startCliSignup", + "summary": "Start a signup from the CLI", + "description": "Emails an 8-digit code to the address. The answer is the same whether or not the address has an account: an address that has one is emailed to say so, and its code never comes. The address must pass the website's signup checks. Limited per network, per address and overall; codes expire after 10 minutes. No API key: this is how an account starts. Nothing is created until the code from the email comes back.", "tags": [ - "Webhooks" + "CLI Login" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID", - "schema": { - "type": "string" - } - }, - { - "name": "delivery_id", - "in": "path", - "required": true, - "description": "The delivery ID", - "schema": { - "type": "string" + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "you@example.com" + }, + "name": { + "type": "string", + "maxLength": 100, + "example": "Ada Lovelace" + }, + "newsletter": { + "type": "boolean", + "default": true, + "description": "The monthly product update email, as the checkbox on the signup form" + }, + "client_name": { + "type": "string", + "maxLength": 60, + "example": "SMTPfast CLI 0.15.0" + }, + "hostname": { + "type": "string", + "maxLength": 63, + "example": "build-01", + "description": "The machine's name; the first API key is called \"CLI on ()\"" + } + } + } } } - ], + }, "responses": { "200": { - "description": "Result of the attempt", + "description": "A code is on its way, or a note that the address already has an account", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "signup_id", + "email", + "expires_in", + "code_length" + ], "properties": { - "success": { - "type": "boolean" + "signup_id": { + "type": "string", + "description": "Send it back with the code to POST /v1/cli/signup/verify" }, - "message": { - "type": "string" + "email": { + "type": "string", + "example": "you@example.com" }, - "delivery": { - "$ref": "#/components/schemas/WebhookDelivery" + "expires_in": { + "type": "integer", + "example": 600 + }, + "code_length": { + "type": "integer", + "example": 8 } } } } } }, - "401": { - "description": "Invalid or missing API key", + "400": { + "description": "Bad request - check your payload", "content": { "application/json": { "schema": { @@ -13224,26 +13825,6 @@ } } }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -13263,85 +13844,153 @@ } } } + }, + "503": { + "description": "Signing up from the CLI is not available right now" } }, "x-sdk-group": [ - "webhooks", - "deliveries" + "cli" ], - "x-sdk-method": "retry" + "x-sdk-method": "startSignup" } }, - "/v1/webhooks/{id}/events": { - "get": { - "operationId": "listWebhookEvents", - "summary": "List webhook events", - "description": "Resend-compatible list of the events sent to one webhook, newest first, each with its delivery status: success, failed, attempting (a retry is scheduled) or pending (not tried yet). Requires the webhook:read scope.", + "/v1/cli/signup/verify": { + "post": { + "operationId": "verifyCliSignup", + "summary": "Finish a CLI signup with the emailed code", + "description": "Checks the code and creates the account as the website does: pending until it is activated, on the free plan, with a team, the address confirmed. Returns the first API key, once, with the default key scopes. The key works for GET /v1/account/activation and its actions right away and for everything else once the account is active. A code works once and allows five tries. No API key: this is how an account starts. Nothing is created until the code from the email comes back.", "tags": [ - "Webhooks" + "CLI Login" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID", - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Page size, 1 to 100 (default 20)", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100 - } - }, - { - "name": "after", - "in": "query", - "description": "Return events older than this event ID", - "schema": { - "type": "string" - } - }, - { - "name": "status", - "in": "query", - "description": "Only events in this status", - "schema": { - "type": "string", - "enum": [ - "success", - "failed", - "attempting", - "pending" - ] + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "signup_id", + "code" + ], + "properties": { + "signup_id": { + "type": "string" + }, + "code": { + "type": "string", + "example": "4831-0927", + "description": "The 8 digits from the email, with or without the dash" + } + } + } } } - ], + }, "responses": { "200": { - "description": "Events", + "description": "The account and its first API key. The key value is returned only here.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "id", + "name", + "key", + "prefix", + "scopes", + "created_at", + "team", + "account", + "activation" + ], "properties": { - "object": { + "id": { + "type": "string" + }, + "name": { "type": "string", - "example": "list" + "example": "CLI on build-01 (2026-10-29)" }, - "has_more": { - "type": "boolean" + "key": { + "type": "string", + "example": "sf_a1b2c3...", + "description": "The full key. Store it; it cannot be read again." }, - "data": { + "prefix": { + "type": "string", + "example": "sf_a1b2c3d4e" + }, + "scopes": { "type": "array", "items": { - "$ref": "#/components/schemas/WebhookEventSummary" + "type": "string" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "team": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "account": { + "type": "object", + "required": [ + "id", + "email", + "status", + "plan" + ], + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "plan": { + "type": "string", + "enum": [ + "free" + ] + } + } + }, + "activation": { + "type": "object", + "required": [ + "required", + "docs" + ], + "properties": { + "required": { + "type": "boolean" + }, + "docs": { + "type": "string", + "format": "uri" + } } } } @@ -13350,47 +13999,147 @@ } }, "400": { - "description": "Bad request - check your payload", + "description": "Wrong, expired or used code", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "type": "object", + "required": [ + "error", + "error_description" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "invalid_code", + "expired_code", + "account_exists", + "signup_refused", + "too_many_attempts", + "unavailable" + ], + "description": "invalid_code: wrong code, try again (attempts_remaining says how often). expired_code: the signup expired, ran out of tries or was used; start again. account_exists: the address has an account; use smtpfast login. signup_refused: the address cannot sign up. too_many_attempts: wait for Retry-After. unavailable: try later." + }, + "error_description": { + "type": "string" + }, + "attempts_remaining": { + "type": "integer", + "example": 4 + } + } } } } }, - "401": { - "description": "Invalid or missing API key", + "403": { + "description": "The address cannot sign up", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "type": "object", + "required": [ + "error", + "error_description" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "invalid_code", + "expired_code", + "account_exists", + "signup_refused", + "too_many_attempts", + "unavailable" + ], + "description": "invalid_code: wrong code, try again (attempts_remaining says how often). expired_code: the signup expired, ran out of tries or was used; start again. account_exists: the address has an account; use smtpfast login. signup_refused: the address cannot sign up. too_many_attempts: wait for Retry-After. unavailable: try later." + }, + "error_description": { + "type": "string" + }, + "attempts_remaining": { + "type": "integer", + "example": 4 + } + } } } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "409": { + "description": "The address already has an account", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "type": "object", + "required": [ + "error", + "error_description" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "invalid_code", + "expired_code", + "account_exists", + "signup_refused", + "too_many_attempts", + "unavailable" + ], + "description": "invalid_code: wrong code, try again (attempts_remaining says how often). expired_code: the signup expired, ran out of tries or was used; start again. account_exists: the address has an account; use smtpfast login. signup_refused: the address cannot sign up. too_many_attempts: wait for Retry-After. unavailable: try later." + }, + "error_description": { + "type": "string" + }, + "attempts_remaining": { + "type": "integer", + "example": 4 + } + } } } } }, - "404": { - "description": "Resource not found", + "429": { + "description": "Too many attempts; see Retry-After", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "type": "object", + "required": [ + "error", + "error_description" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "invalid_code", + "expired_code", + "account_exists", + "signup_refused", + "too_many_attempts", + "unavailable" + ], + "description": "invalid_code: wrong code, try again (attempts_remaining says how often). expired_code: the signup expired, ran out of tries or was used; start again. account_exists: the address has an account; use smtpfast login. signup_refused: the address cannot sign up. too_many_attempts: wait for Retry-After. unavailable: try later." + }, + "error_description": { + "type": "string" + }, + "attempts_remaining": { + "type": "integer", + "example": 4 + } + } } } } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -13399,59 +14148,83 @@ } } }, - "500": { - "description": "Internal server error", + "503": { + "description": "Signing up from the CLI is paused or not available", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "type": "object", + "required": [ + "error", + "error_description" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "invalid_code", + "expired_code", + "account_exists", + "signup_refused", + "too_many_attempts", + "unavailable" + ], + "description": "invalid_code: wrong code, try again (attempts_remaining says how often). expired_code: the signup expired, ran out of tries or was used; start again. account_exists: the address has an account; use smtpfast login. signup_refused: the address cannot sign up. too_many_attempts: wait for Retry-After. unavailable: try later." + }, + "error_description": { + "type": "string" + }, + "attempts_remaining": { + "type": "integer", + "example": 4 + } + } } } } } }, "x-sdk-group": [ - "webhooks", - "events" + "cli" ], - "x-sdk-method": "list" + "x-sdk-method": "verifySignup" } }, - "/v1/webhooks/{id}/events/{event_id}": { - "get": { - "operationId": "getWebhookEvent", - "summary": "Retrieve a webhook event", - "description": "One event with the exact payload that was sent and the time of the next retry, if any. Requires the webhook:read scope.", + "/v1/webhooks": { + "post": { + "operationId": "createWebhook", + "summary": "Create webhook", + "description": "Register a new webhook endpoint. SMTPfast will POST event payloads to this URL when the specified events occur.", "tags": [ "Webhooks" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID", - "schema": { - "type": "string" - } - }, - { - "name": "event_id", - "in": "path", - "required": true, - "description": "The event ID", - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookRequest" + } } } - ], + }, "responses": { - "200": { - "description": "The event", + "201": { + "description": "Webhook created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEvent" + "$ref": "#/components/schemas/Webhook" + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } } @@ -13466,8 +14239,8 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "402": { + "description": "Plan limit reached - upgrade the plan to add more", "content": { "application/json": { "schema": { @@ -13476,8 +14249,8 @@ } } }, - "404": { - "description": "Resource not found", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { @@ -13508,89 +14281,42 @@ } }, "x-sdk-group": [ - "webhooks", - "events" + "webhooks" ], - "x-sdk-method": "get" - } - }, - "/v1/webhooks/{id}/events/{event_id}/attempts": { + "x-sdk-method": "create" + }, "get": { - "operationId": "listWebhookEventAttempts", - "summary": "List the attempts of a webhook event", - "description": "Every attempt to deliver one event, newest first, with the HTTP status your endpoint returned. The latest attempt carries the start of the response body; earlier ones carry the error, if any. Requires the webhook:read scope.", + "operationId": "listWebhooks", + "summary": "List webhooks", + "description": "Return all webhook endpoints configured for the current account.", "tags": [ "Webhooks" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The webhook ID", - "schema": { - "type": "string" - } - }, - { - "name": "event_id", - "in": "path", - "required": true, - "description": "The event ID", - "schema": { - "type": "string" - } - } - ], "responses": { "200": { - "description": "Attempts", + "description": "List of webhooks", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "object", + "has_more", + "data" + ], "properties": { "object": { "type": "string", "example": "list" }, "has_more": { - "type": "boolean" + "type": "boolean", + "example": false }, "data": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "http_status_code": { - "type": [ - "integer", - "null" - ] - }, - "response": { - "type": [ - "string", - "null" - ] - }, - "sent_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "duration_ms": { - "type": [ - "integer", - "null" - ] - } - } + "$ref": "#/components/schemas/Webhook" } } } @@ -13608,26 +14334,6 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -13650,18 +14356,16 @@ } }, "x-sdk-group": [ - "webhooks", - "events", - "attempts" + "webhooks" ], "x-sdk-method": "list" } }, - "/v1/webhooks/{id}/events/{event_id}/replay": { - "post": { - "operationId": "replayWebhookEvent", - "summary": "Replay a webhook event", - "description": "One more delivery attempt for an event, now, with the same body, signed with the current secret. A retry that is already scheduled is brought forward instead of doubled; only an attempt in flight right now returns 409. Requires the webhook:write scope.", + "/v1/webhooks/{id}": { + "get": { + "operationId": "getWebhook", + "summary": "Get webhook", + "description": "Retrieve a single webhook endpoint by ID.", "tags": [ "Webhooks" ], @@ -13670,16 +14374,7 @@ "name": "id", "in": "path", "required": true, - "description": "The webhook ID", - "schema": { - "type": "string" - } - }, - { - "name": "event_id", - "in": "path", - "required": true, - "description": "The event ID", + "description": "The webhook ID (e.g. wh_abc123)", "schema": { "type": "string" } @@ -13687,103 +14382,114 @@ ], "responses": { "200": { - "description": "Result of the attempt", + "description": "The webhook", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "object": { - "type": "string", - "example": "webhook_event" - }, - "id": { - "type": "string" - }, - "delivered": { - "type": "boolean" - }, - "error": { - "type": "string" - } - } + "$ref": "#/components/schemas/Webhook" } } } }, - "401": { - "description": "Invalid or missing API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } + "404": { + "description": "Webhook not found" + } + }, + "x-sdk-group": [ + "webhooks" + ], + "x-sdk-method": "get" + }, + "delete": { + "operationId": "deleteWebhook", + "summary": "Delete webhook", + "description": "Remove a webhook endpoint. Its recorded deliveries are removed with it.", + "tags": [ + "Webhooks" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The webhook ID (e.g. wh_abc123)", + "schema": { + "type": "string" } - }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + } + ], + "responses": { + "200": { + "description": "Deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Success" } } } }, "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } + "description": "Webhook not found" + } + }, + "x-sdk-group": [ + "webhooks" + ], + "x-sdk-method": "delete" + }, + "patch": { + "operationId": "patchWebhook", + "summary": "Update webhook", + "description": "Same as PUT: a partial update that leaves out any field you omit. Accepted because Resend uses PATCH for this, so ported code works unchanged.", + "tags": [ + "Webhooks" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The webhook ID (e.g. wh_abc123)", + "schema": { + "type": "string" } - }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookRequest" } } - }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + } + }, + "responses": { + "200": { + "description": "The updated webhook", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/WebhookUpdated" } } } }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } + "404": { + "description": "Webhook not found" } }, "x-sdk-group": [ - "webhooks", - "events" + "webhooks" ], - "x-sdk-method": "replay" - } - }, - "/v1/webhooks/{id}/signing-secret/rotate": { - "post": { - "operationId": "rotateWebhookSigningSecret", - "summary": "Rotate a webhook signing secret", - "description": "Replace the signing secret. The new secret is in this response only, and every event sent after this call, retries included, is signed with it, so update your endpoint at once. Requires the webhook:write scope and an owner or admin.", + "x-sdk-method": "update" + }, + "put": { + "operationId": "updateWebhook", + "summary": "Update webhook", + "description": "Update the URL, events, or active status of an existing webhook endpoint.", "tags": [ "Webhooks" ], @@ -13792,37 +14498,35 @@ "name": "id", "in": "path", "required": true, - "description": "The webhook ID", + "description": "The webhook ID (e.g. wh_abc123)", "schema": { "type": "string" } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookRequest" + } + } + } + }, "responses": { "200": { - "description": "The new secret", + "description": "Webhook updated", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "object": { - "type": "string", - "example": "webhook" - }, - "id": { - "type": "string" - }, - "signing_secret": { - "type": "string" - } - } + "$ref": "#/components/schemas/WebhookUpdated" } } } }, - "401": { - "description": "Invalid or missing API key", + "400": { + "description": "Bad request - check your payload", "content": { "application/json": { "schema": { @@ -13831,8 +14535,8 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -13875,24 +14579,54 @@ "x-sdk-group": [ "webhooks" ], - "x-sdk-method": "rotateSigningSecret" + "x-sdk-method": "replace" } }, - "/v1/forms": { - "get": { - "operationId": "listSignupForms", - "summary": "List signup forms", - "description": "Return all signup forms for the current account.", + "/v1/webhooks/{id}/test": { + "post": { + "operationId": "testWebhook", + "summary": "Test webhook", + "description": "Send a test event payload to the webhook URL and return the delivery result. Useful for verifying that your endpoint is reachable and responds correctly.", "tags": [ - "Forms" + "Webhooks" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The webhook ID (e.g. wh_abc123)", + "schema": { + "type": "string" + } + } ], "responses": { "200": { - "description": "List of signup forms", + "description": "Test delivery result", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignupFormList" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the test payload was delivered successfully" + }, + "status": { + "type": [ + "integer", + "null" + ], + "description": "HTTP status code returned by the webhook endpoint, null when it did not answer" + }, + "message": { + "type": "string" + }, + "delivery": { + "$ref": "#/components/schemas/WebhookDelivery" + } + } } } } @@ -13907,8 +14641,8 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "404": { + "description": "Resource not found", "content": { "application/json": { "schema": { @@ -13939,34 +14673,86 @@ } }, "x-sdk-group": [ - "forms" + "webhooks" ], - "x-sdk-method": "list" - }, - "post": { - "operationId": "createSignupForm", - "summary": "Create a signup form", - "description": "Create a new signup form, including its confirmation sender and welcome email if you send them. After creation, the embed snippet at /api/forms/{id}/submit (HTML) or /api/forms/{id}/embed.js (JavaScript widget) can be used on third-party sites. Request keys are snake_case; the older camelCase keys are still accepted. Requires the form:write scope.", + "x-sdk-method": "test" + } + }, + "/v1/webhooks/{id}/deliveries": { + "get": { + "operationId": "listWebhookDeliveries", + "summary": "List webhook deliveries", + "description": "Delivery log of one webhook, newest first: every event we POSTed with its attempts, status code, response time, response snippet and error. Failed deliveries are retried after 1s, 30s, 5min and 30min; entries are kept for 30 days. Requires the webhook:read scope.", "tags": [ - "Forms" + "Webhooks" ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSignupFormRequest" - } + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The webhook ID", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "pending", + "sending", + "succeeded", + "failed" + ] + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 } + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Return deliveries older than this id" } - }, + ], "responses": { - "201": { - "description": "Form created", + "200": { + "description": "Deliveries", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignupForm" + "type": "object", + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDelivery" + } + } + } } } } @@ -14001,6 +14787,16 @@ } } }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -14023,25 +14819,35 @@ } }, "x-sdk-group": [ - "forms" + "webhooks", + "deliveries" ], - "x-sdk-method": "create" + "x-sdk-method": "list" } }, - "/v1/domains/{id}/domain-connect/cloudflare": { - "post": { - "operationId": "startCloudflareDomainConnect", - "summary": "Set up sending DNS on Cloudflare in one click", - "description": "For a domain whose DNS is on Cloudflare, returns a signed Domain Connect link. The domain owner opens it, approves the change in Cloudflare, and the sending records are added for them. Not in Resend. Requires the domain:write scope.", + "/v1/webhooks/{id}/deliveries/{delivery_id}": { + "get": { + "operationId": "getWebhookDelivery", + "summary": "Get a webhook delivery", + "description": "One delivery with the exact payload that was sent. Requires the webhook:read scope.", "tags": [ - "Domains" + "Webhooks" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The domain ID", + "description": "The webhook ID", + "schema": { + "type": "string" + } + }, + { + "name": "delivery_id", + "in": "path", + "required": true, + "description": "The delivery ID", "schema": { "type": "string" } @@ -14049,12 +14855,24 @@ ], "responses": { "200": { - "description": "The link to open", + "description": "Delivery", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDelivery" + }, + { + "type": "object", + "properties": { + "payload": { + "type": "string", + "description": "The JSON body as sent" + } + } + } + ] } } } @@ -14089,16 +14907,6 @@ } } }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -14121,25 +14929,35 @@ } }, "x-sdk-group": [ - "domains" + "webhooks", + "deliveries" ], - "x-sdk-method": "connectCloudflare" + "x-sdk-method": "get" } }, - "/v1/domains/{id}/domain-connect/cloudflare-inbound": { + "/v1/webhooks/{id}/deliveries/{delivery_id}/retry": { "post": { - "operationId": "startCloudflareInboundDomainConnect", - "summary": "Set up receiving DNS on Cloudflare in one click", - "description": "Like the sending version, for the MX record that turns on receiving. Not in Resend. Requires the domain:write scope.", + "operationId": "retryWebhookDelivery", + "summary": "Retry a webhook delivery", + "description": "Send the same payload again, once, right now, and return the result of that attempt. A delivery with a scheduled attempt still pending returns 409. Requires the webhook:write scope.", "tags": [ - "Domains" + "Webhooks" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The domain ID", + "description": "The webhook ID", + "schema": { + "type": "string" + } + }, + { + "name": "delivery_id", + "in": "path", + "required": true, + "description": "The delivery ID", "schema": { "type": "string" } @@ -14147,12 +14965,22 @@ ], "responses": { "200": { - "description": "The link to open", + "description": "Result of the attempt", "content": { "application/json": { "schema": { "type": "object", - "additionalProperties": true + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "delivery": { + "$ref": "#/components/schemas/WebhookDelivery" + } + } } } } @@ -14219,32 +15047,101 @@ } }, "x-sdk-group": [ - "domains" + "webhooks", + "deliveries" ], - "x-sdk-method": "connectCloudflareInbound" + "x-sdk-method": "retry" } }, - "/v1/link-domains": { + "/v1/webhooks/{id}/events": { "get": { - "operationId": "listLinkDomains", - "summary": "List link domains", - "description": "The team's custom link domains with their status and DNS records. available says whether the feature is on, included_in_plan whether the team's plan has it. Not in Resend. Requires the domain:read scope.", + "operationId": "listWebhookEvents", + "summary": "List webhook events", + "description": "Resend-compatible list of the events sent to one webhook, newest first, each with its delivery status: success, failed, attempting (a retry is scheduled) or pending (not tried yet). Requires the webhook:read scope.", "tags": [ - "Domains" + "Webhooks" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The webhook ID", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Page size, 1 to 100 (default 20)", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "after", + "in": "query", + "description": "Return events older than this event ID", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Only events in this status", + "schema": { + "type": "string", + "enum": [ + "success", + "failed", + "attempting", + "pending" + ] + } + } ], "responses": { "200": { - "description": "Link domains", + "description": "Events", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomainList" + "type": "object", + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookEventSummary" + } + } + } } } } }, - "401": { - "description": "Invalid or missing API key", + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -14263,6 +15160,16 @@ } } }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -14285,44 +15192,47 @@ } }, "x-sdk-group": [ - "linkDomains" + "webhooks", + "events" ], "x-sdk-method": "list" - }, - "post": { - "operationId": "createLinkDomain", - "summary": "Add a link domain", - "description": "Add a custom hostname for tracking and unsubscribe links, such as links.example.com. It must be a subdomain of one of your verified sending domains, one per sending domain. Publish the CNAME record from dns_records; the link domain turns active once Cloudflare sees it and issues a certificate, usually within minutes. Paid plans only: 403 with code plan_upgrade_required on the free plan. 422 when the hostname is not under a verified sending domain, 409 when that domain already has one, 503 with code link_domains_unavailable when the feature is off. Not in Resend. Requires the domain:write scope and an owner or admin; 5 per hour per team.", + } + }, + "/v1/webhooks/{id}/events/{event_id}": { + "get": { + "operationId": "getWebhookEvent", + "summary": "Retrieve a webhook event", + "description": "One event with the exact payload that was sent and the time of the next retry, if any. Requires the webhook:read scope.", "tags": [ - "Domains" + "Webhooks" ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateLinkDomainRequest" - } + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The webhook ID", + "schema": { + "type": "string" + } + }, + { + "name": "event_id", + "in": "path", + "required": true, + "description": "The event ID", + "schema": { + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "The link domain, pending until the CNAME record is in place", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LinkDomain" - } - } - } - }, - "400": { - "description": "Bad request - check your payload", + "200": { + "description": "The event", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomainError" + "$ref": "#/components/schemas/WebhookEvent" } } } @@ -14342,27 +15252,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomainError" - } - } - } - }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LinkDomainError" + "$ref": "#/components/schemas/Error" } } } }, - "422": { - "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", + "404": { + "description": "Resource not found", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomainError" + "$ref": "#/components/schemas/Error" } } } @@ -14386,48 +15286,38 @@ } } } - }, - "502": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LinkDomainError" - } - } - } - }, - "503": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LinkDomainError" - } - } - } } }, "x-sdk-group": [ - "linkDomains" + "webhooks", + "events" ], - "x-sdk-method": "create" + "x-sdk-method": "get" } }, - "/v1/link-domains/{id}": { + "/v1/webhooks/{id}/events/{event_id}/attempts": { "get": { - "operationId": "getLinkDomain", - "summary": "Get a link domain", - "description": "One link domain with the DNS records to publish: the CNAME pointing at SMTPfast, and any TXT ownership record Cloudflare offers. Shows the state from the last check; refresh asks Cloudflare again. Not in Resend. Requires the domain:read scope.", + "operationId": "listWebhookEventAttempts", + "summary": "List the attempts of a webhook event", + "description": "Every attempt to deliver one event, newest first, with the HTTP status your endpoint returned. The latest attempt carries the start of the response body; earlier ones carry the error, if any. Requires the webhook:read scope.", "tags": [ - "Domains" + "Webhooks" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The link domain ID", + "description": "The webhook ID", + "schema": { + "type": "string" + } + }, + { + "name": "event_id", + "in": "path", + "required": true, + "description": "The event ID", "schema": { "type": "string" } @@ -14435,11 +15325,56 @@ ], "responses": { "200": { - "description": "The link domain", + "description": "Attempts", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomain" + "type": "object", + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "http_status_code": { + "type": [ + "integer", + "null" + ] + }, + "response": { + "type": [ + "string", + "null" + ] + }, + "sent_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "duration_ms": { + "type": [ + "integer", + "null" + ] + } + } + } + } + } } } } @@ -14496,23 +15431,36 @@ } }, "x-sdk-group": [ - "linkDomains" + "webhooks", + "events", + "attempts" ], - "x-sdk-method": "get" - }, - "delete": { - "operationId": "deleteLinkDomain", - "summary": "Delete a link domain", - "description": "Mail stops using the link domain at once and the hostname is removed from Cloudflare. Links in mail already sent through it stop working once the hostname is gone. Not in Resend. Requires the domain:write scope and an owner or admin.", + "x-sdk-method": "list" + } + }, + "/v1/webhooks/{id}/events/{event_id}/replay": { + "post": { + "operationId": "replayWebhookEvent", + "summary": "Replay a webhook event", + "description": "One more delivery attempt for an event, now, with the same body, signed with the current secret. A retry that is already scheduled is brought forward instead of doubled; only an attempt in flight right now returns 409. Requires the webhook:write scope.", "tags": [ - "Domains" + "Webhooks" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The link domain ID", + "description": "The webhook ID", + "schema": { + "type": "string" + } + }, + { + "name": "event_id", + "in": "path", + "required": true, + "description": "The event ID", "schema": { "type": "string" } @@ -14520,11 +15468,26 @@ ], "responses": { "200": { - "description": "Deleted", + "description": "Result of the attempt", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomainDeleted" + "type": "object", + "properties": { + "object": { + "type": "string", + "example": "webhook_event" + }, + "id": { + "type": "string" + }, + "delivered": { + "type": "boolean" + }, + "error": { + "type": "string" + } + } } } } @@ -14559,6 +15522,16 @@ } } }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -14581,25 +15554,26 @@ } }, "x-sdk-group": [ - "linkDomains" - ], - "x-sdk-method": "delete" + "webhooks", + "events" + ], + "x-sdk-method": "replay" } }, - "/v1/link-domains/{id}/domain-connect": { - "get": { - "operationId": "getLinkDomainDomainConnect", - "summary": "Check one-click DNS setup for a link domain", - "description": "Whether the link domain's DNS provider can add its CNAME record through Domain Connect, the open standard behind one-click DNS setup. Not in Resend. Requires the domain:read scope; 30 checks per 10 minutes per team.", + "/v1/webhooks/{id}/signing-secret/rotate": { + "post": { + "operationId": "rotateWebhookSigningSecret", + "summary": "Rotate a webhook signing secret", + "description": "Replace the signing secret. The new secret is in this response only, and every event sent after this call, retries included, is signed with it, so update your endpoint at once. Requires the webhook:write scope and an owner or admin.", "tags": [ - "Domains" + "Webhooks" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The link domain ID", + "description": "The webhook ID", "schema": { "type": "string" } @@ -14607,11 +15581,23 @@ ], "responses": { "200": { - "description": "Whether one-click setup is available", + "description": "The new secret", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomainConnectCheck" + "type": "object", + "properties": { + "object": { + "type": "string", + "example": "webhook" + }, + "id": { + "type": "string" + }, + "signing_secret": { + "type": "string" + } + } } } } @@ -14668,35 +15654,26 @@ } }, "x-sdk-group": [ - "linkDomains" + "webhooks" ], - "x-sdk-method": "getDomainConnect" - }, - "post": { - "operationId": "startLinkDomainDomainConnect", - "summary": "Set up a link domain's DNS in one click", - "description": "Returns a signed Domain Connect link. The domain owner opens it, approves the CNAME record at the DNS provider, and the link domain turns active once Cloudflare sees the record. 409 with the reason when the provider cannot apply it, or the link domain is not pending (refresh a failed one first). Not in Resend. Requires the domain:write scope; 30 per 10 minutes per team.", + "x-sdk-method": "rotateSigningSecret" + } + }, + "/v1/forms": { + "get": { + "operationId": "listSignupForms", + "summary": "List signup forms", + "description": "Return all signup forms for the current account.", "tags": [ - "Domains" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The link domain ID", - "schema": { - "type": "string" - } - } + "Forms" ], "responses": { "200": { - "description": "The link to open", + "description": "List of signup forms", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomainConnectLink" + "$ref": "#/components/schemas/SignupFormList" } } } @@ -14721,26 +15698,6 @@ } } }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -14763,43 +15720,40 @@ } }, "x-sdk-group": [ - "linkDomains" + "forms" ], - "x-sdk-method": "startDomainConnect" - } - }, - "/v1/link-domains/{id}/refresh": { + "x-sdk-method": "list" + }, "post": { - "operationId": "refreshLinkDomain", - "summary": "Refresh a link domain's status", - "description": "Ask Cloudflare for the current state now, instead of waiting for the automatic check (every few minutes while pending, daily once active). Also restarts automatic checks for a failed link domain. Not in Resend. Requires the domain:write scope; 30 per 10 minutes per team.", + "operationId": "createSignupForm", + "summary": "Create a signup form", + "description": "Create a new signup form, including its confirmation sender and welcome email if you send them. After creation, the embed snippet at /api/forms/{id}/submit (HTML) or /api/forms/{id}/embed.js (JavaScript widget) can be used on third-party sites. Request keys are snake_case; the older camelCase keys are still accepted. Requires the form:write scope.", "tags": [ - "Domains" + "Forms" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The link domain ID", - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignupFormRequest" + } } } - ], + }, "responses": { - "200": { - "description": "The link domain with its current status", + "201": { + "description": "Form created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LinkDomain" + "$ref": "#/components/schemas/SignupForm" } } } }, - "401": { - "description": "Invalid or missing API key", + "400": { + "description": "Bad request - check your payload", "content": { "application/json": { "schema": { @@ -14808,8 +15762,8 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -14818,8 +15772,8 @@ } } }, - "404": { - "description": "Resource not found", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { @@ -14847,88 +15801,36 @@ } } } - }, - "502": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LinkDomainError" - } - } - } - }, - "503": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LinkDomainError" - } - } - } } }, "x-sdk-group": [ - "linkDomains" + "forms" ], - "x-sdk-method": "refresh" + "x-sdk-method": "create" } }, - "/v1/forms/{id}/welcome-preview": { + "/v1/domains/{id}/domain-connect/cloudflare": { "post": { - "operationId": "previewFormWelcomeEmail", - "summary": "Preview a form's welcome email", - "description": "Render the welcome email of a signup form for a sample contact, without sending it. Pass subject and markdown to preview unsaved changes. Not in Resend. Requires the form:read scope.", + "operationId": "startCloudflareDomainConnect", + "summary": "Set up sending DNS on Cloudflare in one click", + "description": "For a domain whose DNS is on Cloudflare, returns a signed Domain Connect link. The domain owner opens it, approves the change in Cloudflare, and the sending records are added for them. Not in Resend. Requires the domain:write scope.", "tags": [ - "Forms" + "Domains" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The form ID", + "description": "The domain ID", "schema": { "type": "string" } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "subject": { - "type": "string" - }, - "markdown": { - "type": "string" - }, - "sample": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - } - } - } - } - } - } - } - }, "responses": { "200": { - "description": "The rendered subject, HTML and text", + "description": "The link to open", "content": { "application/json": { "schema": { @@ -14938,8 +15840,8 @@ } } }, - "400": { - "description": "Bad request - check your payload", + "401": { + "description": "Invalid or missing API key", "content": { "application/json": { "schema": { @@ -14948,8 +15850,8 @@ } } }, - "401": { - "description": "Invalid or missing API key", + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", "content": { "application/json": { "schema": { @@ -14958,8 +15860,8 @@ } } }, - "403": { - "description": "Forbidden - domain not verified or insufficient permissions", + "404": { + "description": "Resource not found", "content": { "application/json": { "schema": { @@ -14968,8 +15870,8 @@ } } }, - "404": { - "description": "Resource not found", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", "content": { "application/json": { "schema": { @@ -15000,25 +15902,25 @@ } }, "x-sdk-group": [ - "forms" + "domains" ], - "x-sdk-method": "previewWelcomeEmail" + "x-sdk-method": "connectCloudflare" } }, - "/v1/forms/{id}": { - "get": { - "operationId": "getSignupForm", - "summary": "Retrieve a signup form", - "description": "Retrieve a signup form by ID.", + "/v1/domains/{id}/domain-connect/cloudflare-inbound": { + "post": { + "operationId": "startCloudflareInboundDomainConnect", + "summary": "Set up receiving DNS on Cloudflare in one click", + "description": "Like the sending version, for the MX record that turns on receiving. Not in Resend. Requires the domain:write scope.", "tags": [ - "Forms" + "Domains" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The signup form ID", + "description": "The domain ID", "schema": { "type": "string" } @@ -15026,11 +15928,12 @@ ], "responses": { "200": { - "description": "Form details", + "description": "The link to open", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignupForm" + "type": "object", + "additionalProperties": true } } } @@ -15065,8 +15968,8 @@ } } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", "content": { "application/json": { "schema": { @@ -15075,8 +15978,18 @@ } } }, - "500": { - "description": "Internal server error", + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -15087,45 +16000,100 @@ } }, "x-sdk-group": [ - "forms" + "domains" ], - "x-sdk-method": "get" - }, - "patch": { - "operationId": "updateSignupForm", - "summary": "Update a signup form", - "description": "Update fields, styling, double opt-in behaviour, the confirmation and welcome emails, or active state on an existing form. Only the fields sent change. Request keys are snake_case; the older camelCase keys are still accepted. Requires the form:write scope.", + "x-sdk-method": "connectCloudflareInbound" + } + }, + "/v1/link-domains": { + "get": { + "operationId": "listLinkDomains", + "summary": "List link domains", + "description": "The team's custom link domains with their status and DNS records. available says whether the feature is on, included_in_plan whether the team's plan has it. Not in Resend. Requires the domain:read scope.", "tags": [ - "Forms" + "Domains" ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The signup form ID", - "schema": { - "type": "string" + "responses": { + "200": { + "description": "Link domains", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkDomainList" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } } } + }, + "x-sdk-group": [ + "linkDomains" + ], + "x-sdk-method": "list" + }, + "post": { + "operationId": "createLinkDomain", + "summary": "Add a link domain", + "description": "Add a custom hostname for tracking and unsubscribe links, such as links.example.com. It must be a subdomain of one of your verified sending domains, one per sending domain. Publish the CNAME record from dns_records; the link domain turns active once Cloudflare sees it and issues a certificate, usually within minutes. Paid plans only: 403 with code plan_upgrade_required on the free plan. 422 when the hostname is not under a verified sending domain, 409 when that domain already has one, 503 with code link_domains_unavailable when the feature is off. Not in Resend. Requires the domain:write scope and an owner or admin; 5 per hour per team.", + "tags": [ + "Domains" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateSignupFormRequest" + "$ref": "#/components/schemas/CreateLinkDomainRequest" } } } }, "responses": { - "200": { - "description": "Form updated", + "201": { + "description": "The link domain, pending until the CNAME record is in place", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignupForm" + "$ref": "#/components/schemas/LinkDomain" } } } @@ -15135,7 +16103,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/LinkDomainError" } } } @@ -15155,17 +16123,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/LinkDomainError" } } } }, - "404": { - "description": "Resource not found", + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/LinkDomainError" + } + } + } + }, + "422": { + "description": "The request is well formed but cannot be carried out in the current state, for example an unpublished template", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkDomainError" } } } @@ -15189,26 +16167,48 @@ } } } + }, + "502": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkDomainError" + } + } + } + }, + "503": { + "description": "Stripe could not be reached - retry shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkDomainError" + } + } + } } }, "x-sdk-group": [ - "forms" + "linkDomains" ], - "x-sdk-method": "update" - }, - "delete": { - "operationId": "deleteSignupForm", - "summary": "Delete a signup form", - "description": "Delete a signup form. Embedded snippets pointing at this form will stop working.", + "x-sdk-method": "create" + } + }, + "/v1/link-domains/{id}": { + "get": { + "operationId": "getLinkDomain", + "summary": "Get a link domain", + "description": "One link domain with the DNS records to publish: the CNAME pointing at SMTPfast, and any TXT ownership record Cloudflare offers. Shows the state from the last check; refresh asks Cloudflare again. Not in Resend. Requires the domain:read scope.", "tags": [ - "Forms" + "Domains" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The signup form ID", + "description": "The link domain ID", "schema": { "type": "string" } @@ -15216,11 +16216,11 @@ ], "responses": { "200": { - "description": "Form deleted", + "description": "The link domain", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignupFormDeleted" + "$ref": "#/components/schemas/LinkDomain" } } } @@ -15277,55 +16277,23 @@ } }, "x-sdk-group": [ - "forms" + "linkDomains" ], - "x-sdk-method": "delete" - } - }, - "/v1/forms/{id}/pending": { - "get": { - "operationId": "listPendingSignups", - "summary": "List pending signups", - "description": "Signups on a double opt-in form that have not clicked their confirmation link and have not expired, newest first. Each one carries the status of its confirmation email, the two-letter country it came from and the page it was submitted on, where known. Requires the form:read scope.", + "x-sdk-method": "get" + }, + "delete": { + "operationId": "deleteLinkDomain", + "summary": "Delete a link domain", + "description": "Mail stops using the link domain at once and the hostname is removed from Cloudflare. Links in mail already sent through it stop working once the hostname is gone. Not in Resend. Requires the domain:write scope and an owner or admin.", "tags": [ - "Forms" + "Domains" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The signup form ID", - "schema": { - "type": "string" - } - }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - } - }, - { - "name": "q", - "in": "query", - "required": false, - "description": "Search by email, first name or last name", + "description": "The link domain ID", "schema": { "type": "string" } @@ -15333,11 +16301,11 @@ ], "responses": { "200": { - "description": "A page of pending signups", + "description": "Deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PendingSignupList" + "$ref": "#/components/schemas/LinkDomainDeleted" } } } @@ -15394,83 +16362,37 @@ } }, "x-sdk-group": [ - "forms", - "pending" + "linkDomains" ], - "x-sdk-method": "list" + "x-sdk-method": "delete" } }, - "/v1/forms/{id}/pending/approve-all": { - "post": { - "operationId": "approveAllPendingSignups", - "summary": "Approve all pending signups", - "description": "Confirm pending signups without waiting for the link click, up to 200 per call; call again while `remaining` is above zero. Each one becomes a contact, fires contact.subscribed and gets the welcome email if enabled. Signups whose confirmation email bounced, was marked as spam or failed are left pending and counted in `skipped_undeliverable`. Signups submitted through Tor are left pending and counted in `skipped_tor`. Requires the form:write scope.", + "/v1/link-domains/{id}/domain-connect": { + "get": { + "operationId": "getLinkDomainDomainConnect", + "summary": "Check one-click DNS setup for a link domain", + "description": "Whether the link domain's DNS provider can add its CNAME record through Domain Connect, the open standard behind one-click DNS setup. Not in Resend. Requires the domain:read scope; 30 checks per 10 minutes per team.", "tags": [ - "Forms" + "Domains" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The signup form ID", + "description": "The link domain ID", "schema": { "type": "string" } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "q": { - "type": "string", - "description": "Only approve signups matching this search" - } - } - } - } - } - }, "responses": { "200": { - "description": "Approval result", + "description": "Whether one-click setup is available", "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "approved", - "skipped", - "remaining", - "skipped_undeliverable", - "skipped_tor" - ], - "properties": { - "approved": { - "type": "integer", - "example": 8 - }, - "skipped": { - "type": "integer", - "example": 0 - }, - "remaining": { - "type": "integer", - "example": 0 - }, - "skipped_undeliverable": { - "type": "integer", - "example": 1 - }, - "skipped_tor": { - "type": "integer", - "example": 2 - } - } + "$ref": "#/components/schemas/LinkDomainConnectCheck" } } } @@ -15527,35 +16449,23 @@ } }, "x-sdk-group": [ - "forms", - "pending" + "linkDomains" ], - "x-sdk-method": "approveAll" - } - }, - "/v1/forms/{id}/pending/{pendingId}": { - "delete": { - "operationId": "removePendingSignup", - "summary": "Remove a pending signup", - "description": "Remove a pending signup without adding it as a contact. Its confirmation link stops working. Requires the form:write scope.", + "x-sdk-method": "getDomainConnect" + }, + "post": { + "operationId": "startLinkDomainDomainConnect", + "summary": "Set up a link domain's DNS in one click", + "description": "Returns a signed Domain Connect link. The domain owner opens it, approves the CNAME record at the DNS provider, and the link domain turns active once Cloudflare sees the record. 409 with the reason when the provider cannot apply it, or the link domain is not pending (refresh a failed one first). Not in Resend. Requires the domain:write scope; 30 per 10 minutes per team.", "tags": [ - "Forms" + "Domains" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The signup form ID", - "schema": { - "type": "string" - } - }, - { - "name": "pendingId", - "in": "path", - "required": true, - "description": "The pending signup ID", + "description": "The link domain ID", "schema": { "type": "string" } @@ -15563,20 +16473,11 @@ ], "responses": { "200": { - "description": "Removed", + "description": "The link to open", "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "deleted" - ], - "properties": { - "deleted": { - "type": "boolean", - "example": true - } - } + "$ref": "#/components/schemas/LinkDomainConnectLink" } } } @@ -15611,6 +16512,16 @@ } } }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -15633,35 +16544,25 @@ } }, "x-sdk-group": [ - "forms", - "pending" + "linkDomains" ], - "x-sdk-method": "remove" + "x-sdk-method": "startDomainConnect" } }, - "/v1/forms/{id}/pending/{pendingId}/approve": { + "/v1/link-domains/{id}/refresh": { "post": { - "operationId": "approvePendingSignup", - "summary": "Approve a pending signup", - "description": "Confirm one pending signup without waiting for the link click. It becomes a contact, fires contact.subscribed and gets the welcome email if enabled. Approving one that is already confirmed returns 200 with already_confirmed. Returns 409 when the form is no longer active. Requires the form:write scope.", + "operationId": "refreshLinkDomain", + "summary": "Refresh a link domain's status", + "description": "Ask Cloudflare for the current state now, instead of waiting for the automatic check (every few minutes while pending, daily once active). Also restarts automatic checks for a failed link domain. Not in Resend. Requires the domain:write scope; 30 per 10 minutes per team.", "tags": [ - "Forms" + "Domains" ], "parameters": [ { "name": "id", "in": "path", "required": true, - "description": "The signup form ID", - "schema": { - "type": "string" - } - }, - { - "name": "pendingId", - "in": "path", - "required": true, - "description": "The pending signup ID", + "description": "The link domain ID", "schema": { "type": "string" } @@ -15669,33 +16570,11 @@ ], "responses": { "200": { - "description": "Approved", + "description": "The link domain with its current status", "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "approved", - "email" - ], - "properties": { - "approved": { - "type": "boolean", - "example": true - }, - "contact_id": { - "type": "string", - "example": "clx_contact123" - }, - "email": { - "type": "string", - "example": "jane@example.com" - }, - "already_confirmed": { - "type": "boolean", - "example": false - } - } + "$ref": "#/components/schemas/LinkDomain" } } } @@ -15730,8 +16609,8 @@ } } }, - "409": { - "description": "Conflict - the resource already exists or changed meanwhile", + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { "application/json": { "schema": { @@ -15740,8 +16619,8 @@ } } }, - "429": { - "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -15750,60 +16629,92 @@ } } }, - "500": { - "description": "Internal server error", + "502": { + "description": "Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/LinkDomainError" + } + } + } + }, + "503": { + "description": "Stripe could not be reached - retry shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkDomainError" } } } } }, "x-sdk-group": [ - "forms", - "pending" + "linkDomains" ], - "x-sdk-method": "approve" + "x-sdk-method": "refresh" } }, - "/v1/analytics": { - "get": { - "operationId": "getAnalytics", - "summary": "Get analytics", - "description": "Sending and deliverability totals for the account, plus a 30-day daily series. Pass days and/or domain to add a `window` object holding exact counts for that window and sending domain, aggregated in the database rather than sampled from the event log.", + "/v1/forms/{id}/welcome-preview": { + "post": { + "operationId": "previewFormWelcomeEmail", + "summary": "Preview a form's welcome email", + "description": "Render the welcome email of a signup form for a sample contact, without sending it. Pass subject and markdown to preview unsaved changes. Not in Resend. Requires the form:read scope.", "tags": [ - "Analytics" + "Forms" ], "parameters": [ { - "name": "days", - "in": "query", - "description": "Window in days (1-90) for the `window` block. Present in the response only when days or domain is given.", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 90, - "default": 30 - } - }, - { - "name": "domain", - "in": "query", - "description": "Restrict the `window` block to one sending domain, by name or id.", + "name": "id", + "in": "path", + "required": true, + "description": "The form ID", "schema": { "type": "string" } } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "subject": { + "type": "string" + }, + "markdown": { + "type": "string" + }, + "sample": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + } + } + } + } + } + } + } + }, "responses": { "200": { - "description": "Analytics data", + "description": "The rendered subject, HTML and text", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Analytics" + "type": "object", + "additionalProperties": true } } } @@ -15828,6 +16739,26 @@ } } }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", "content": { @@ -15850,7 +16781,857 @@ } }, "x-sdk-group": [ - "analytics" + "forms" + ], + "x-sdk-method": "previewWelcomeEmail" + } + }, + "/v1/forms/{id}": { + "get": { + "operationId": "getSignupForm", + "summary": "Retrieve a signup form", + "description": "Retrieve a signup form by ID.", + "tags": [ + "Forms" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The signup form ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Form details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignupForm" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "forms" + ], + "x-sdk-method": "get" + }, + "patch": { + "operationId": "updateSignupForm", + "summary": "Update a signup form", + "description": "Update fields, styling, double opt-in behaviour, the confirmation and welcome emails, or active state on an existing form. Only the fields sent change. Request keys are snake_case; the older camelCase keys are still accepted. Requires the form:write scope.", + "tags": [ + "Forms" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The signup form ID", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSignupFormRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Form updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignupForm" + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "forms" + ], + "x-sdk-method": "update" + }, + "delete": { + "operationId": "deleteSignupForm", + "summary": "Delete a signup form", + "description": "Delete a signup form. Embedded snippets pointing at this form will stop working.", + "tags": [ + "Forms" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The signup form ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Form deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignupFormDeleted" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "forms" + ], + "x-sdk-method": "delete" + } + }, + "/v1/forms/{id}/pending": { + "get": { + "operationId": "listPendingSignups", + "summary": "List pending signups", + "description": "Signups on a double opt-in form that have not clicked their confirmation link and have not expired, newest first. Each one carries the status of its confirmation email, the two-letter country it came from and the page it was submitted on, where known. Requires the form:read scope.", + "tags": [ + "Forms" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The signup form ID", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + { + "name": "q", + "in": "query", + "required": false, + "description": "Search by email, first name or last name", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A page of pending signups", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PendingSignupList" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "forms", + "pending" + ], + "x-sdk-method": "list" + } + }, + "/v1/forms/{id}/pending/approve-all": { + "post": { + "operationId": "approveAllPendingSignups", + "summary": "Approve all pending signups", + "description": "Confirm pending signups without waiting for the link click, up to 200 per call; call again while `remaining` is above zero. Each one becomes a contact, fires contact.subscribed and gets the welcome email if enabled. Signups whose confirmation email bounced, was marked as spam or failed are left pending and counted in `skipped_undeliverable`. Signups submitted through Tor are left pending and counted in `skipped_tor`. Requires the form:write scope.", + "tags": [ + "Forms" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The signup form ID", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "Only approve signups matching this search" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Approval result", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "approved", + "skipped", + "remaining", + "skipped_undeliverable", + "skipped_tor" + ], + "properties": { + "approved": { + "type": "integer", + "example": 8 + }, + "skipped": { + "type": "integer", + "example": 0 + }, + "remaining": { + "type": "integer", + "example": 0 + }, + "skipped_undeliverable": { + "type": "integer", + "example": 1 + }, + "skipped_tor": { + "type": "integer", + "example": 2 + } + } + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "forms", + "pending" + ], + "x-sdk-method": "approveAll" + } + }, + "/v1/forms/{id}/pending/{pendingId}": { + "delete": { + "operationId": "removePendingSignup", + "summary": "Remove a pending signup", + "description": "Remove a pending signup without adding it as a contact. Its confirmation link stops working. Requires the form:write scope.", + "tags": [ + "Forms" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The signup form ID", + "schema": { + "type": "string" + } + }, + { + "name": "pendingId", + "in": "path", + "required": true, + "description": "The pending signup ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Removed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean", + "example": true + } + } + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "forms", + "pending" + ], + "x-sdk-method": "remove" + } + }, + "/v1/forms/{id}/pending/{pendingId}/approve": { + "post": { + "operationId": "approvePendingSignup", + "summary": "Approve a pending signup", + "description": "Confirm one pending signup without waiting for the link click. It becomes a contact, fires contact.subscribed and gets the welcome email if enabled. Approving one that is already confirmed returns 200 with already_confirmed. Returns 409 when the form is no longer active. Requires the form:write scope.", + "tags": [ + "Forms" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The signup form ID", + "schema": { + "type": "string" + } + }, + { + "name": "pendingId", + "in": "path", + "required": true, + "description": "The pending signup ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Approved", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "approved", + "email" + ], + "properties": { + "approved": { + "type": "boolean", + "example": true + }, + "contact_id": { + "type": "string", + "example": "clx_contact123" + }, + "email": { + "type": "string", + "example": "jane@example.com" + }, + "already_confirmed": { + "type": "boolean", + "example": false + } + } + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden - domain not verified or insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Conflict - the resource already exists or changed meanwhile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "forms", + "pending" + ], + "x-sdk-method": "approve" + } + }, + "/v1/analytics": { + "get": { + "operationId": "getAnalytics", + "summary": "Get analytics", + "description": "Sending and deliverability totals for the account, plus a 30-day daily series. Pass days and/or domain to add a `window` object holding exact counts for that window and sending domain, aggregated in the database rather than sampled from the event log.", + "tags": [ + "Analytics" + ], + "parameters": [ + { + "name": "days", + "in": "query", + "description": "Window in days (1-90) for the `window` block. Present in the response only when days or domain is given.", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 90, + "default": 30 + } + }, + { + "name": "domain", + "in": "query", + "description": "Restrict the `window` block to one sending domain, by name or id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Analytics data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Analytics" + } + } + } + }, + "400": { + "description": "Bad request - check your payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Invalid or missing API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Rate limit exceeded - retry after the period indicated in the Retry-After header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-sdk-group": [ + "analytics" ], "x-sdk-method": "get" } @@ -17020,10 +18801,11 @@ "items": { "type": "string", "format": "email" - } + }, + "maxItems": 50 } ], - "description": "Reply-To address or addresses" + "description": "Reply-To address or addresses, up to 50. Every address goes in the Reply-To header." }, "subject": { "type": "string", @@ -17252,7 +19034,7 @@ "string", "null" ], - "description": "The Reply-To address the email was sent with, or null." + "description": "The Reply-To address the email was sent with, or the first one when it had several, or null." }, "tags": { "type": "array", @@ -17663,7 +19445,7 @@ }, "example": { "company": "Acme", - "plan": "pro" + "plan": "growth" } }, "segment_ids": { @@ -17874,17 +19656,18 @@ }, "contact_limit": { "type": "integer", - "example": 25000, + "example": 50000, "description": "The plan's contact limit, with any override for the account" }, "contact_limit_base": { "type": "integer", - "example": 25000, + "example": 50000, "description": "The plan's contact limit without overrides" }, "tier": { "type": "string", - "example": "pro" + "example": "growth", + "description": "The plan: free, starter, growth or scale." } } }, @@ -18285,7 +20068,8 @@ }, "tier": { "type": "string", - "example": "starter" + "example": "starter", + "description": "The plan: free, starter, growth or scale." } } }, @@ -19228,8 +21012,109 @@ "example": 0.2 }, "bounce_rate": { - "type": "number", - "example": 0.7 + "type": "number", + "example": 0.7 + } + } + }, + "MailboxProviderMetrics": { + "type": "object", + "description": "Sent email to one mailbox provider. Each email counts once, by the domain of its first To address; a domain outside the consumer domains is classified by its MX hosts, so Google Workspace counts as gmail and Microsoft 365 as outlook. Rates are percentages with two decimals, null when there is nothing to rate yet.", + "required": [ + "provider", + "name", + "sent", + "share", + "delivered", + "bounced", + "complained", + "tracked_delivered", + "opened", + "delivery_rate", + "open_rate", + "bounce_rate", + "complaint_rate" + ], + "properties": { + "provider": { + "type": "string", + "enum": [ + "gmail", + "outlook", + "icloud", + "yahoo", + "proton", + "other" + ] + }, + "name": { + "type": "string", + "example": "Gmail" + }, + "sent": { + "type": "integer", + "description": "Handed to SES: sent, delivered, opened, clicked, bounced or complained.", + "example": 812 + }, + "share": { + "type": "number", + "description": "Percent of all sent email in the breakdown.", + "example": 67.44 + }, + "delivered": { + "type": "integer", + "description": "Accepted by the receiving server, complained included.", + "example": 798 + }, + "bounced": { + "type": "integer", + "example": 9 + }, + "complained": { + "type": "integer", + "example": 1 + }, + "tracked_delivered": { + "type": "integer", + "description": "Delivered with open tracking on: the open rate's denominator.", + "example": 790 + }, + "opened": { + "type": "integer", + "description": "Of tracked_delivered, opened at least once.", + "example": 402 + }, + "delivery_rate": { + "type": [ + "number", + "null" + ], + "description": "delivered / (delivered + bounced): over emails with a delivery or bounce report.", + "example": 98.88 + }, + "open_rate": { + "type": [ + "number", + "null" + ], + "description": "opened / tracked_delivered. Apple Mail Privacy Protection loads the open pixel before anyone reads the email, so the icloud rate reads high.", + "example": 50.89 + }, + "bounce_rate": { + "type": [ + "number", + "null" + ], + "description": "bounced / (delivered + bounced).", + "example": 1.12 + }, + "complaint_rate": { + "type": [ + "number", + "null" + ], + "description": "complained / delivered.", + "example": 0.13 } } }, @@ -20820,7 +22705,7 @@ "items": { "type": "string" }, - "description": "Optional list of scopes to restrict the key. Available: email:send, email:read, domain:read, domain:write, contact:read, contact:write, form:read, form:write, webhook:read, webhook:write, logs:read, inbound:read, inbound:delete, team:read, team:manage, apikey:manage. If omitted the key gets the default set: everything except logs:read, inbound:read, inbound:delete, team:read, team:manage and apikey:manage. Listing, creating or revoking keys through the API requires apikey:manage.", + "description": "Optional list of scopes to restrict the key. Available: email:send, email:read, domain:read, domain:write, contact:read, contact:write, form:read, form:write, webhook:read, webhook:write, logs:read, inbound:read, inbound:delete, team:read, team:manage, billing:read, billing:manage, apikey:manage. If omitted the key gets the default set: everything except logs:read, inbound:read, inbound:delete, team:read, team:manage, billing:read, billing:manage and apikey:manage. Listing, creating or revoking keys through the API requires apikey:manage.", "example": [ "email:send", "domain:read" @@ -21533,7 +23418,7 @@ }, "limit": { "type": "integer", - "example": 50000 + "example": 100000 }, "sent": { "type": "integer", @@ -21564,7 +23449,7 @@ }, "effective_remaining": { "type": "integer", - "example": 41580, + "example": 91580, "description": "What can still be sent before a send is refused: the monthly allowance left plus credits" } } @@ -21581,7 +23466,7 @@ }, "limit": { "type": "integer", - "example": 25000 + "example": 50000 } } }, @@ -21823,11 +23708,12 @@ }, "limit": { "type": "integer", - "example": 50000 + "example": 100000 }, "tier": { "type": "string", - "example": "pro" + "example": "growth", + "description": "The plan: free, starter, growth or scale." } } }, @@ -21906,304 +23792,820 @@ "error" ], "properties": { - "error": { - "type": "string", - "description": "A human-readable error message", - "example": "Invalid API key" + "error": { + "type": "string", + "description": "A human-readable error message", + "example": "Invalid API key" + } + } + }, + "AudienceMeta": { + "type": "object", + "additionalProperties": true, + "description": "Details of the audience, such as the segment_id", + "example": { + "segment_id": "clx_segment123" + } + }, + "LinkDomainError": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "example": "links.example.com must be a subdomain of one of your verified sending domains." + }, + "code": { + "type": "string", + "enum": [ + "invalid_hostname", + "reserved_hostname", + "no_matching_domain", + "domain_not_verified", + "link_domain_exists", + "link_domains_unavailable", + "link_domains_at_capacity", + "plan_upgrade_required", + "cloudflare_error" + ] + } + } + }, + "CreateLinkDomainRequest": { + "type": "object", + "required": [ + "hostname" + ], + "properties": { + "hostname": { + "type": "string", + "example": "links.example.com", + "description": "A subdomain of one of your verified sending domains. One link domain per sending domain." + } + } + }, + "LinkDomainDnsRecord": { + "type": "object", + "required": [ + "type", + "name", + "value", + "ttl", + "purpose", + "required", + "status" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "CNAME", + "TXT" + ], + "example": "CNAME" + }, + "name": { + "type": "string", + "example": "links.example.com" + }, + "value": { + "type": "string", + "example": "links.smtpfa.st" + }, + "ttl": { + "type": "string", + "example": "Auto" + }, + "purpose": { + "type": "string", + "example": "Points the link domain at SMTPfast" + }, + "required": { + "type": "boolean", + "description": "The CNAME is required. A TXT record Cloudflare offers is optional: it lets the hostname activate before the CNAME is live." + }, + "status": { + "type": "string", + "enum": [ + "pending", + "verified", + "not_needed" + ], + "description": "pending while the link domain is not active. Once it is active the CNAME is verified (keep it: certificate renewals need it) and a TXT record is not_needed, so you can remove it." + } + } + }, + "LinkDomain": { + "type": "object", + "description": "A custom hostname for open tracking, click tracking and unsubscribe links. While it is active, email whose From address is on the same registrable domain uses it instead of smtpfa.st, in the body and in the List-Unsubscribe header.", + "required": [ + "object", + "id", + "hostname", + "domain_id", + "domain", + "status", + "ssl_status", + "last_error", + "dns_records", + "verified_at", + "last_checked_at", + "created_at" + ], + "properties": { + "object": { + "type": "string", + "example": "link_domain" + }, + "id": { + "type": "string", + "example": "cmlink123" + }, + "hostname": { + "type": "string", + "example": "links.example.com" + }, + "domain_id": { + "type": [ + "string", + "null" + ], + "description": "The sending domain the hostname sits under." + }, + "domain": { + "type": [ + "string", + "null" + ], + "example": "example.com" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "active", + "failed" + ], + "description": "pending until Cloudflare sees the CNAME record and issues the certificate, then active. failed means it will not become active without a change on your side (last_error says why); fix it and refresh. Mail uses the link domain only while it is active." + }, + "ssl_status": { + "type": [ + "string", + "null" + ], + "example": "pending_validation", + "description": "The certificate status Cloudflare reports, for troubleshooting." + }, + "last_error": { + "type": [ + "string", + "null" + ], + "description": "Why the link domain is not active yet, when Cloudflare says." + }, + "dns_records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkDomainDnsRecord" + } + }, + "verified_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it first became active." + }, + "last_checked_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "LinkDomainList": { + "type": "object", + "required": [ + "object", + "has_more", + "data", + "available", + "included_in_plan" + ], + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "has_more": { + "type": "boolean", + "example": false + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkDomain" + } + }, + "available": { + "type": "boolean", + "description": "Whether custom link domains are available on this server. When false, creating one answers 503." + }, + "included_in_plan": { + "type": "boolean", + "description": "Whether the team's plan includes custom link domains (every paid plan). When false, creating one answers 403 and mail uses smtpfa.st links, also through a link domain added earlier." + } + } + }, + "LinkDomainConnectCheck": { + "type": "object", + "required": [ + "available", + "provider", + "reason" + ], + "properties": { + "available": { + "type": "boolean", + "description": "Whether the DNS provider can add the CNAME record in one click through Domain Connect." + }, + "provider": { + "type": [ + "string", + "null" + ], + "example": "Cloudflare", + "description": "The DNS provider, when discovery found one." + }, + "reason": { + "type": [ + "string", + "null" + ], + "description": "Why one-click setup is not available, when it is not." } } }, - "AudienceMeta": { - "type": "object", - "additionalProperties": true, - "description": "Details of the audience, such as the segment_id", - "example": { - "segment_id": "clx_segment123" - } - }, - "LinkDomainError": { + "LinkDomainConnectLink": { "type": "object", "required": [ - "error" + "url", + "provider" ], "properties": { - "error": { + "url": { "type": "string", - "example": "links.example.com must be a subdomain of one of your verified sending domains." + "description": "The signed link to open. The DNS provider shows the CNAME record and asks the domain owner to approve it." }, - "code": { + "provider": { "type": "string", - "enum": [ - "invalid_hostname", - "reserved_hostname", - "no_matching_domain", - "domain_not_verified", - "link_domain_exists", - "link_domains_unavailable", - "link_domains_at_capacity", - "plan_upgrade_required", - "cloudflare_error" - ] + "example": "Cloudflare" } } }, - "CreateLinkDomainRequest": { + "LinkDomainDeleted": { "type": "object", "required": [ - "hostname" + "object", + "id", + "deleted" ], "properties": { - "hostname": { + "object": { "type": "string", - "example": "links.example.com", - "description": "A subdomain of one of your verified sending domains. One link domain per sending domain." + "example": "link_domain" + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "example": true } } }, - "LinkDomainDnsRecord": { + "Billing": { "type": "object", "required": [ - "type", - "name", - "value", - "ttl", - "purpose", - "required", - "status" + "object", + "plan", + "interval", + "status", + "current_period_end", + "cancel_at_period_end", + "cancel_at", + "scheduled_change", + "payment_issue", + "usage", + "next_tier" ], "properties": { - "type": { + "object": { "type": "string", "enum": [ - "CNAME", - "TXT" - ], - "example": "CNAME" + "billing" + ] }, - "name": { + "plan": { "type": "string", - "example": "links.example.com" + "example": "growth", + "description": "free, starter, growth or scale" }, - "value": { - "type": "string", - "example": "links.smtpfa.st" + "interval": { + "type": [ + "string", + "null" + ], + "enum": [ + "monthly", + "yearly", + null + ], + "description": "How the subscription is billed; null without one" }, - "ttl": { - "type": "string", - "example": "Auto" + "status": { + "type": [ + "string", + "null" + ], + "example": "active", + "description": "The Stripe subscription status (active, past_due, unpaid and so on); null without a subscription" }, - "purpose": { - "type": "string", - "example": "Points the link domain at SMTPfast" + "current_period_end": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When the paid plan renews, or ends when it is cancelled" }, - "required": { + "cancel_at_period_end": { "type": "boolean", - "description": "The CNAME is required. A TXT record Cloudflare offers is optional: it lets the hostname activate before the CNAME is live." + "description": "True when the subscription is cancelled and will not renew" + }, + "cancel_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When a cancelled subscription ends" + }, + "scheduled_change": { + "type": [ + "object", + "null" + ], + "description": "A smaller plan waiting for the end of the paid period", + "required": [ + "plan", + "interval", + "effective_at" + ], + "properties": { + "plan": { + "type": "string", + "example": "starter" + }, + "interval": { + "type": [ + "string", + "null" + ], + "enum": [ + "monthly", + "yearly", + null + ] + }, + "effective_at": { + "type": "string", + "format": "date-time" + } + } + }, + "payment_issue": { + "type": [ + "object", + "null" + ], + "description": "The last renewal was not paid. past_due: Stripe is still retrying. unpaid: it stopped.", + "required": [ + "status", + "pay_url" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "past_due", + "unpaid" + ] + }, + "pay_url": { + "type": [ + "string", + "null" + ], + "description": "Stripe's page for the open invoice" + } + } + }, + "usage": { + "type": "object", + "description": "This month's usage against the plan, the same numbers as GET /v1/usage. The month is the calendar month in UTC, not the billing period.", + "required": [ + "period_start", + "resets_at", + "emails", + "contacts", + "segments", + "broadcasts", + "domains", + "members" + ], + "properties": { + "period_start": { + "type": "string", + "format": "date-time" + }, + "resets_at": { + "type": "string", + "format": "date-time" + }, + "emails": { + "type": "object", + "required": [ + "used", + "limit", + "credits" + ], + "properties": { + "used": { + "type": "integer", + "example": 8420 + }, + "limit": { + "type": "integer", + "example": 100000 + }, + "credits": { + "type": "integer", + "example": 0, + "description": "Prepaid email credits, spent after the monthly allowance" + } + } + }, + "contacts": { + "type": "object", + "required": [ + "used", + "limit" + ], + "properties": { + "used": { + "type": "integer", + "example": 1840 + }, + "limit": { + "type": "integer", + "example": 50000 + } + } + }, + "segments": { + "type": "object", + "required": [ + "used", + "limit" + ], + "properties": { + "used": { + "type": "integer", + "example": 4 + }, + "limit": { + "type": "integer", + "example": 50 + } + } + }, + "broadcasts": { + "type": "object", + "required": [ + "used", + "limit" + ], + "properties": { + "used": { + "type": "integer", + "example": 3 + }, + "limit": { + "type": "integer", + "example": 50 + } + } + }, + "domains": { + "type": "object", + "required": [ + "used", + "limit" + ], + "properties": { + "used": { + "type": "integer", + "example": 2 + }, + "limit": { + "type": [ + "integer", + "null" + ] + } + } + }, + "members": { + "type": "object", + "required": [ + "used", + "limit" + ], + "properties": { + "used": { + "type": "integer", + "example": 3 + }, + "limit": { + "type": "integer", + "example": 10 + } + }, + "description": "Members plus pending invitations, as the seat limit counts them" + } + } }, - "status": { - "type": "string", - "enum": [ - "pending", - "verified", - "not_needed" + "next_tier": { + "type": [ + "object", + "null" ], - "description": "pending while the link domain is not active. Once it is active the CNAME is verified (keep it: certificate renewals need it) and a TXT record is not_needed, so you can remove it." + "description": "The plan above this one, with its price and limits; null on the top plan", + "required": [ + "plan", + "name", + "price", + "limits" + ], + "properties": { + "plan": { + "type": "string", + "example": "scale" + }, + "name": { + "type": "string", + "example": "Scale" + }, + "price": { + "type": "object", + "required": [ + "monthly", + "yearly", + "currency" + ], + "description": "In the smallest currency unit (cents)", + "properties": { + "monthly": { + "type": "integer", + "example": 4900 + }, + "yearly": { + "type": "integer", + "example": 49000 + }, + "currency": { + "type": "string", + "example": "USD" + } + } + }, + "limits": { + "type": "object", + "description": "What the plan allows", + "required": [ + "emails", + "contacts", + "segments", + "broadcasts", + "domains", + "members" + ], + "properties": { + "emails": { + "type": "integer", + "example": 300000, + "description": "Emails a month" + }, + "contacts": { + "type": "integer", + "example": 150000 + }, + "segments": { + "type": "integer", + "example": 200 + }, + "broadcasts": { + "type": "integer", + "example": 200, + "description": "Broadcasts a month" + }, + "domains": { + "type": [ + "integer", + "null" + ], + "description": "Sending domains; null means no cap" + }, + "members": { + "type": "integer", + "example": 25, + "description": "People on the team, the owner included" + } + } + } + } } } }, - "LinkDomain": { + "BillingCheckout": { "type": "object", - "description": "A custom hostname for open tracking, click tracking and unsubscribe links. While it is active, email whose From address is on the same registrable domain uses it instead of smtpfa.st, in the body and in the List-Unsubscribe header.", "required": [ "object", - "id", - "hostname", - "domain_id", - "domain", - "status", - "ssl_status", - "last_error", - "dns_records", - "verified_at", - "last_checked_at", - "created_at" + "action", + "url", + "plan", + "interval", + "direction", + "effective_at" ], "properties": { "object": { "type": "string", - "example": "link_domain" + "enum": [ + "billing_checkout" + ] }, - "id": { + "action": { "type": "string", - "example": "cmlink123" + "enum": [ + "checkout", + "confirm" + ], + "description": "checkout: a Stripe Checkout page for a first subscription. confirm: the billing page with the change open in its confirmation dialog, for an existing subscription." }, - "hostname": { + "url": { "type": "string", - "example": "links.example.com" - }, - "domain_id": { - "type": [ - "string", - "null" - ], - "description": "The sending domain the hostname sits under." + "example": "https://checkout.stripe.com/c/pay/cs_live_a1b2" }, - "domain": { - "type": [ - "string", - "null" - ], - "example": "example.com" + "plan": { + "type": "string", + "example": "growth" }, - "status": { + "interval": { "type": "string", "enum": [ - "pending", - "active", - "failed" - ], - "description": "pending until Cloudflare sees the CNAME record and issues the certificate, then active. failed means it will not become active without a change on your side (last_error says why); fix it and refresh. Mail uses the link domain only while it is active." + "monthly", + "yearly" + ] }, - "ssl_status": { + "direction": { "type": [ "string", "null" ], - "example": "pending_validation", - "description": "The certificate status Cloudflare reports, for troubleshooting." - }, - "last_error": { - "type": [ - "string", - "null" + "enum": [ + "up", + "down", + "same", + null ], - "description": "Why the link domain is not active yet, when Cloudflare says." - }, - "dns_records": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LinkDomainDnsRecord" - } + "description": "For confirm. up or same: the prorated difference is charged when the person confirms, and the plan applies once that is paid. down: nothing is charged now and the plan moves at effective_at." }, - "verified_at": { + "effective_at": { "type": [ "string", "null" ], "format": "date-time", - "description": "When it first became active." - }, - "last_checked_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "created_at": { - "type": "string", - "format": "date-time" + "description": "When a move down takes effect" } } }, - "LinkDomainList": { + "BillingPortalSession": { "type": "object", "required": [ "object", - "has_more", - "data", - "available", - "included_in_plan" + "url" ], "properties": { "object": { "type": "string", - "example": "list" - }, - "has_more": { - "type": "boolean", - "example": false - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LinkDomain" - } - }, - "available": { - "type": "boolean", - "description": "Whether custom link domains are available on this server. When false, creating one answers 503." + "enum": [ + "billing_portal_session" + ] }, - "included_in_plan": { - "type": "boolean", - "description": "Whether the team's plan includes custom link domains (every paid plan). When false, creating one answers 403 and mail uses smtpfa.st links, also through a link domain added earlier." + "url": { + "type": "string", + "example": "https://billing.stripe.com/p/session/live_a1b2" } } }, - "LinkDomainConnectCheck": { + "Invoice": { "type": "object", "required": [ - "available", - "provider", - "reason" + "object", + "id", + "number", + "created_at", + "amount", + "amount_paid", + "currency", + "status", + "hosted_invoice_url", + "invoice_pdf" ], "properties": { - "available": { - "type": "boolean", - "description": "Whether the DNS provider can add the CNAME record in one click through Domain Connect." + "object": { + "type": "string", + "enum": [ + "invoice" + ] }, - "provider": { + "id": { + "type": "string", + "example": "in_1QxYz2" + }, + "number": { "type": [ "string", "null" ], - "example": "Cloudflare", - "description": "The DNS provider, when discovery found one." + "example": "A1B2C3D4-0007" }, - "reason": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "amount": { + "type": "integer", + "example": 1900, + "description": "The invoice total, in the smallest currency unit" + }, + "amount_paid": { + "type": "integer", + "example": 1900 + }, + "currency": { + "type": "string", + "example": "USD" + }, + "status": { "type": [ "string", "null" ], - "description": "Why one-click setup is not available, when it is not." - } - } - }, - "LinkDomainConnectLink": { - "type": "object", - "required": [ - "url", - "provider" - ], - "properties": { - "url": { - "type": "string", - "description": "The signed link to open. The DNS provider shows the CNAME record and asks the domain owner to approve it." - }, - "provider": { - "type": "string", - "example": "Cloudflare" - } - } - }, - "LinkDomainDeleted": { - "type": "object", - "required": [ - "object", - "id", - "deleted" - ], - "properties": { - "object": { - "type": "string", - "example": "link_domain" + "enum": [ + "draft", + "open", + "paid", + "uncollectible", + "void", + null + ] }, - "id": { - "type": "string" + "hosted_invoice_url": { + "type": [ + "string", + "null" + ], + "description": "Stripe's page for the invoice, where an open one can be paid" }, - "deleted": { - "type": "boolean", - "example": true + "invoice_pdf": { + "type": [ + "string", + "null" + ], + "description": "A link to the PDF" } } } @@ -22256,7 +24658,7 @@ }, { "name": "Account", - "description": "The calling key or connection: scopes, team and plan usage" + "description": "The calling key or connection: scopes, team and plan usage, and activating a new account" }, { "name": "Suppressions", @@ -22269,6 +24671,14 @@ { "name": "Team", "description": "Members and invitations" + }, + { + "name": "Billing", + "description": "The plan, usage against its limits, invoices, and Checkout and billing portal links" + }, + { + "name": "CLI Login", + "description": "The device login behind smtpfast login and the emailed-code signup behind smtpfast signup" } ] } diff --git a/src/smtpfast/_resources.py b/src/smtpfast/_resources.py index df470ab..50729ea 100644 --- a/src/smtpfast/_resources.py +++ b/src/smtpfast/_resources.py @@ -107,9 +107,9 @@ def share(self, id: str, params: ShareEmailParams | None = None, *, options: Req """Create a share link for a sent email. `POST /v1/emails/{id}/share`""" return self._core.request("POST", "/v1/emails/{id}/share", path_params={"id": id}, query=None, body=params, options=options) # type: ignore[no-any-return] - def get_metrics(self, *, start_date: str | None = None, end_date: str | None = None, timezone: str | None = None, granularity: Literal["hourly", "daily", "weekly", "monthly"] | None = None, metrics: str | None = None, dimensions: str | None = None, domain_id: str | None = None, email_id: str | None = None, broadcast_id: str | None = None, options: RequestOptions | None = None) -> GetEmailMetricsResponse: + def get_metrics(self, *, start_date: str | None = None, end_date: str | None = None, timezone: str | None = None, granularity: Literal["hourly", "daily", "weekly", "monthly"] | None = None, metrics: str | None = None, dimensions: str | None = None, domain_id: str | None = None, email_id: str | None = None, broadcast_id: str | None = None, include: Literal["providers"] | None = None, options: RequestOptions | None = None) -> GetEmailMetricsResponse: """Retrieve email metrics. `GET /v1/emails/metrics`""" - return self._core.request("GET", "/v1/emails/metrics", path_params=None, query={"start_date": start_date, "end_date": end_date, "timezone": timezone, "granularity": granularity, "metrics": metrics, "dimensions": dimensions, "domain_id": domain_id, "email_id": email_id, "broadcast_id": broadcast_id}, body=None, options=options) # type: ignore[no-any-return] + return self._core.request("GET", "/v1/emails/metrics", path_params=None, query={"start_date": start_date, "end_date": end_date, "timezone": timezone, "granularity": granularity, "metrics": metrics, "dimensions": dimensions, "domain_id": domain_id, "email_id": email_id, "broadcast_id": broadcast_id, "include": include}, body=None, options=options) # type: ignore[no-any-return] def list_clicked_links(self, *, days: int | None = None, domain_id: str | None = None, url: str | None = None, options: RequestOptions | None = None) -> ListClickedLinksResponse: """List clicked links. `GET /v1/emails/clicked-links`""" @@ -381,6 +381,22 @@ def usage(self, *, options: RequestOptions | None = None) -> Usage: """Usage against the plan. `GET /v1/usage`""" return self._core.request("GET", "/v1/usage", path_params=None, query=None, body=None, options=options) # type: ignore[no-any-return] + def activation(self, *, options: RequestOptions | None = None) -> GetAccountActivationResponse: + """Whether the account is active. `GET /v1/account/activation`""" + return self._core.request("GET", "/v1/account/activation", path_params=None, query=None, body=None, options=options) # type: ignore[no-any-return] + + def request_free_trial(self, params: RequestFreeTrialParams, *, options: RequestOptions | None = None) -> RequestFreeTrialResponse: + """Ask for the free plan. `POST /v1/account/activation/free-trial`""" + return self._core.request("POST", "/v1/account/activation/free-trial", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + def redeem_invite(self, params: RedeemActivationInviteParams, *, options: RequestOptions | None = None) -> RedeemActivationInviteResponse: + """Activate with an invite code. `POST /v1/account/activation/invite`""" + return self._core.request("POST", "/v1/account/activation/invite", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + def activation_checkout(self, params: CreateActivationCheckoutParams, *, options: RequestOptions | None = None) -> CreateActivationCheckoutResponse: + """Activate with a paid plan. `POST /v1/account/activation/checkout`""" + return self._core.request("POST", "/v1/account/activation/checkout", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + class TeamMembersResource(_SyncResource): """`client.team.members`""" @@ -427,6 +443,48 @@ def __init__(self, core: Any) -> None: self.invites = TeamInvitesResource(core) +class BillingCheckoutResource(_SyncResource): + """`client.billing.checkout`""" + + def create(self, params: CreateBillingCheckoutParams, *, options: RequestOptions | None = None) -> BillingCheckout: + """Get a link to start or change a plan. `POST /v1/billing/checkout`""" + return self._core.request("POST", "/v1/billing/checkout", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + +class BillingPortalResource(_SyncResource): + """`client.billing.portal`""" + + def create(self, params: CreateBillingPortalSessionParams | None = None, *, options: RequestOptions | None = None) -> BillingPortalSession: + """Get a link to the billing portal. `POST /v1/billing/portal`""" + return self._core.request("POST", "/v1/billing/portal", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + +class BillingInvoicesResource(_SyncResource): + """`client.billing.invoices`""" + + def list(self, *, limit: int | None = None, after: str | None = None, before: str | None = None, options: RequestOptions | None = None) -> ListBillingInvoicesResponse: + """List invoices. `GET /v1/billing/invoices`""" + return self._core.request("GET", "/v1/billing/invoices", path_params=None, query={"limit": limit, "after": after, "before": before}, body=None, options=options) # type: ignore[no-any-return] + + def list_all(self, *, limit: int | None = None, after: str | None = None, options: RequestOptions | None = None) -> Iterator[Invoice]: + """Every item of list, fetching the following pages as you iterate.""" + return self._core.paginate("GET", "/v1/billing/invoices", path_params=None, query={"limit": limit, "after": after}, body=None, options=options) + + +class BillingResource(_SyncResource): + """`client.billing`""" + + def __init__(self, core: Any) -> None: + super().__init__(core) + self.checkout = BillingCheckoutResource(core) + self.portal = BillingPortalResource(core) + self.invoices = BillingInvoicesResource(core) + + def get(self, *, options: RequestOptions | None = None) -> Billing: + """Get the plan and usage. `GET /v1/billing`""" + return self._core.request("GET", "/v1/billing", path_params=None, query=None, body=None, options=options) # type: ignore[no-any-return] + + class SuppressionsResource(_SyncResource): """`client.suppressions`""" @@ -623,6 +681,26 @@ def revoke(self, id: str, *, options: RequestOptions | None = None) -> RevokeApi return self._core.request("DELETE", "/v1/api-keys/{id}", path_params={"id": id}, query=None, body=None, options=options) # type: ignore[no-any-return] +class CliResource(_SyncResource): + """`client.cli`""" + + def create_device_code(self, params: CreateCliDeviceCodeParams | None = None, *, options: RequestOptions | None = None) -> CreateCliDeviceCodeResponse: + """Start a CLI login. `POST /v1/cli/device`""" + return self._core.request("POST", "/v1/cli/device", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + def exchange_device_code(self, params: ExchangeCliDeviceCodeParams, *, options: RequestOptions | None = None) -> ExchangeCliDeviceCodeResponse: + """Collect the CLI login key. `POST /v1/cli/token`""" + return self._core.request("POST", "/v1/cli/token", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + def start_signup(self, params: StartCliSignupParams, *, options: RequestOptions | None = None) -> StartCliSignupResponse: + """Start a signup from the CLI. `POST /v1/cli/signup`""" + return self._core.request("POST", "/v1/cli/signup", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + def verify_signup(self, params: VerifyCliSignupParams, *, options: RequestOptions | None = None) -> VerifyCliSignupResponse: + """Finish a CLI signup with the emailed code. `POST /v1/cli/signup/verify`""" + return self._core.request("POST", "/v1/cli/signup/verify", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + class WebhooksDeliveriesResource(_SyncResource): """`client.webhooks.deliveries`""" @@ -820,11 +898,13 @@ def __init__(self, core: Any) -> None: self.segments = SegmentsResource(core) self.account = AccountResource(core) self.team = TeamResource(core) + self.billing = BillingResource(core) self.suppressions = SuppressionsResource(core) self.broadcasts = BroadcastsResource(core) self.templates = TemplatesResource(core) self.domains = DomainsResource(core) self.api_keys = ApiKeysResource(core) + self.cli = CliResource(core) self.webhooks = WebhooksResource(core) self.forms = FormsResource(core) self.link_domains = LinkDomainsResource(core) @@ -930,9 +1010,9 @@ async def share(self, id: str, params: ShareEmailParams | None = None, *, option """Create a share link for a sent email. `POST /v1/emails/{id}/share`""" return await self._core.request("POST", "/v1/emails/{id}/share", path_params={"id": id}, query=None, body=params, options=options) # type: ignore[no-any-return] - async def get_metrics(self, *, start_date: str | None = None, end_date: str | None = None, timezone: str | None = None, granularity: Literal["hourly", "daily", "weekly", "monthly"] | None = None, metrics: str | None = None, dimensions: str | None = None, domain_id: str | None = None, email_id: str | None = None, broadcast_id: str | None = None, options: RequestOptions | None = None) -> GetEmailMetricsResponse: + async def get_metrics(self, *, start_date: str | None = None, end_date: str | None = None, timezone: str | None = None, granularity: Literal["hourly", "daily", "weekly", "monthly"] | None = None, metrics: str | None = None, dimensions: str | None = None, domain_id: str | None = None, email_id: str | None = None, broadcast_id: str | None = None, include: Literal["providers"] | None = None, options: RequestOptions | None = None) -> GetEmailMetricsResponse: """Retrieve email metrics. `GET /v1/emails/metrics`""" - return await self._core.request("GET", "/v1/emails/metrics", path_params=None, query={"start_date": start_date, "end_date": end_date, "timezone": timezone, "granularity": granularity, "metrics": metrics, "dimensions": dimensions, "domain_id": domain_id, "email_id": email_id, "broadcast_id": broadcast_id}, body=None, options=options) # type: ignore[no-any-return] + return await self._core.request("GET", "/v1/emails/metrics", path_params=None, query={"start_date": start_date, "end_date": end_date, "timezone": timezone, "granularity": granularity, "metrics": metrics, "dimensions": dimensions, "domain_id": domain_id, "email_id": email_id, "broadcast_id": broadcast_id, "include": include}, body=None, options=options) # type: ignore[no-any-return] async def list_clicked_links(self, *, days: int | None = None, domain_id: str | None = None, url: str | None = None, options: RequestOptions | None = None) -> ListClickedLinksResponse: """List clicked links. `GET /v1/emails/clicked-links`""" @@ -1204,6 +1284,22 @@ async def usage(self, *, options: RequestOptions | None = None) -> Usage: """Usage against the plan. `GET /v1/usage`""" return await self._core.request("GET", "/v1/usage", path_params=None, query=None, body=None, options=options) # type: ignore[no-any-return] + async def activation(self, *, options: RequestOptions | None = None) -> GetAccountActivationResponse: + """Whether the account is active. `GET /v1/account/activation`""" + return await self._core.request("GET", "/v1/account/activation", path_params=None, query=None, body=None, options=options) # type: ignore[no-any-return] + + async def request_free_trial(self, params: RequestFreeTrialParams, *, options: RequestOptions | None = None) -> RequestFreeTrialResponse: + """Ask for the free plan. `POST /v1/account/activation/free-trial`""" + return await self._core.request("POST", "/v1/account/activation/free-trial", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + async def redeem_invite(self, params: RedeemActivationInviteParams, *, options: RequestOptions | None = None) -> RedeemActivationInviteResponse: + """Activate with an invite code. `POST /v1/account/activation/invite`""" + return await self._core.request("POST", "/v1/account/activation/invite", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + async def activation_checkout(self, params: CreateActivationCheckoutParams, *, options: RequestOptions | None = None) -> CreateActivationCheckoutResponse: + """Activate with a paid plan. `POST /v1/account/activation/checkout`""" + return await self._core.request("POST", "/v1/account/activation/checkout", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + class AsyncTeamMembersResource(_AsyncResource): """`client.team.members`""" @@ -1250,6 +1346,48 @@ def __init__(self, core: Any) -> None: self.invites = AsyncTeamInvitesResource(core) +class AsyncBillingCheckoutResource(_AsyncResource): + """`client.billing.checkout`""" + + async def create(self, params: CreateBillingCheckoutParams, *, options: RequestOptions | None = None) -> BillingCheckout: + """Get a link to start or change a plan. `POST /v1/billing/checkout`""" + return await self._core.request("POST", "/v1/billing/checkout", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + +class AsyncBillingPortalResource(_AsyncResource): + """`client.billing.portal`""" + + async def create(self, params: CreateBillingPortalSessionParams | None = None, *, options: RequestOptions | None = None) -> BillingPortalSession: + """Get a link to the billing portal. `POST /v1/billing/portal`""" + return await self._core.request("POST", "/v1/billing/portal", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + +class AsyncBillingInvoicesResource(_AsyncResource): + """`client.billing.invoices`""" + + async def list(self, *, limit: int | None = None, after: str | None = None, before: str | None = None, options: RequestOptions | None = None) -> ListBillingInvoicesResponse: + """List invoices. `GET /v1/billing/invoices`""" + return await self._core.request("GET", "/v1/billing/invoices", path_params=None, query={"limit": limit, "after": after, "before": before}, body=None, options=options) # type: ignore[no-any-return] + + def list_all(self, *, limit: int | None = None, after: str | None = None, options: RequestOptions | None = None) -> AsyncIterator[Invoice]: + """Every item of list, fetching the following pages as you iterate.""" + return self._core.paginate("GET", "/v1/billing/invoices", path_params=None, query={"limit": limit, "after": after}, body=None, options=options) + + +class AsyncBillingResource(_AsyncResource): + """`client.billing`""" + + def __init__(self, core: Any) -> None: + super().__init__(core) + self.checkout = AsyncBillingCheckoutResource(core) + self.portal = AsyncBillingPortalResource(core) + self.invoices = AsyncBillingInvoicesResource(core) + + async def get(self, *, options: RequestOptions | None = None) -> Billing: + """Get the plan and usage. `GET /v1/billing`""" + return await self._core.request("GET", "/v1/billing", path_params=None, query=None, body=None, options=options) # type: ignore[no-any-return] + + class AsyncSuppressionsResource(_AsyncResource): """`client.suppressions`""" @@ -1446,6 +1584,26 @@ async def revoke(self, id: str, *, options: RequestOptions | None = None) -> Rev return await self._core.request("DELETE", "/v1/api-keys/{id}", path_params={"id": id}, query=None, body=None, options=options) # type: ignore[no-any-return] +class AsyncCliResource(_AsyncResource): + """`client.cli`""" + + async def create_device_code(self, params: CreateCliDeviceCodeParams | None = None, *, options: RequestOptions | None = None) -> CreateCliDeviceCodeResponse: + """Start a CLI login. `POST /v1/cli/device`""" + return await self._core.request("POST", "/v1/cli/device", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + async def exchange_device_code(self, params: ExchangeCliDeviceCodeParams, *, options: RequestOptions | None = None) -> ExchangeCliDeviceCodeResponse: + """Collect the CLI login key. `POST /v1/cli/token`""" + return await self._core.request("POST", "/v1/cli/token", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + async def start_signup(self, params: StartCliSignupParams, *, options: RequestOptions | None = None) -> StartCliSignupResponse: + """Start a signup from the CLI. `POST /v1/cli/signup`""" + return await self._core.request("POST", "/v1/cli/signup", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + async def verify_signup(self, params: VerifyCliSignupParams, *, options: RequestOptions | None = None) -> VerifyCliSignupResponse: + """Finish a CLI signup with the emailed code. `POST /v1/cli/signup/verify`""" + return await self._core.request("POST", "/v1/cli/signup/verify", path_params=None, query=None, body=params, options=options) # type: ignore[no-any-return] + + class AsyncWebhooksDeliveriesResource(_AsyncResource): """`client.webhooks.deliveries`""" @@ -1643,11 +1801,13 @@ def __init__(self, core: Any) -> None: self.segments = AsyncSegmentsResource(core) self.account = AsyncAccountResource(core) self.team = AsyncTeamResource(core) + self.billing = AsyncBillingResource(core) self.suppressions = AsyncSuppressionsResource(core) self.broadcasts = AsyncBroadcastsResource(core) self.templates = AsyncTemplatesResource(core) self.domains = AsyncDomainsResource(core) self.api_keys = AsyncApiKeysResource(core) + self.cli = AsyncCliResource(core) self.webhooks = AsyncWebhooksResource(core) self.forms = AsyncFormsResource(core) self.link_domains = AsyncLinkDomainsResource(core) diff --git a/src/smtpfast/types.py b/src/smtpfast/types.py index 50efe7c..301965c 100644 --- a/src/smtpfast/types.py +++ b/src/smtpfast/types.py @@ -1194,6 +1194,27 @@ ) +# Sent email to one mailbox provider. Each email counts once, by the domain of its first To address; a domain outside the consumer domains is classified +MailboxProviderMetrics = TypedDict( + "MailboxProviderMetrics", + { + "provider": Literal["gmail", "outlook", "icloud", "yahoo", "proton", "other"], + "name": str, + "sent": int, + "share": float, + "delivered": int, + "bounced": int, + "complained": int, + "tracked_delivered": int, + "opened": int, + "delivery_rate": Union[float, None], + "open_rate": Union[float, None], + "bounce_rate": Union[float, None], + "complaint_rate": Union[float, None], + }, +) + + TemplateVariable = TypedDict( "TemplateVariable", { @@ -2107,6 +2128,194 @@ ) +# What the plan allows +BillingNextTierLimits = TypedDict( + "BillingNextTierLimits", + { + "emails": int, + "contacts": int, + "segments": int, + "broadcasts": int, + "domains": Union[int, None], + "members": int, + }, +) + + +# In the smallest currency unit (cents) +BillingNextTierPrice = TypedDict( + "BillingNextTierPrice", + { + "monthly": int, + "yearly": int, + "currency": str, + }, +) + + +# The plan above this one, with its price and limits; null on the top plan +BillingNextTier = TypedDict( + "BillingNextTier", + { + "plan": str, + "name": str, + "price": BillingNextTierPrice, + "limits": BillingNextTierLimits, + }, +) + + +# The last renewal was not paid. past_due: Stripe is still retrying. unpaid: it stopped. +BillingPaymentIssue = TypedDict( + "BillingPaymentIssue", + { + "status": Literal["past_due", "unpaid"], + "pay_url": Union[str, None], + }, +) + + +# A smaller plan waiting for the end of the paid period +BillingScheduledChange = TypedDict( + "BillingScheduledChange", + { + "plan": str, + "interval": Union[Literal["monthly", "yearly"], None], + "effective_at": str, + }, +) + + +BillingUsageBroadcasts = TypedDict( + "BillingUsageBroadcasts", + { + "used": int, + "limit": int, + }, +) + + +BillingUsageContacts = TypedDict( + "BillingUsageContacts", + { + "used": int, + "limit": int, + }, +) + + +BillingUsageDomains = TypedDict( + "BillingUsageDomains", + { + "used": int, + "limit": Union[int, None], + }, +) + + +BillingUsageEmails = TypedDict( + "BillingUsageEmails", + { + "used": int, + "limit": int, + "credits": int, + }, +) + + +# Members plus pending invitations, as the seat limit counts them +BillingUsageMembers = TypedDict( + "BillingUsageMembers", + { + "used": int, + "limit": int, + }, +) + + +BillingUsageSegments = TypedDict( + "BillingUsageSegments", + { + "used": int, + "limit": int, + }, +) + + +# This month's usage against the plan, the same numbers as GET /v1/usage. The month is the calendar month in UTC, not the billing period. +BillingUsage = TypedDict( + "BillingUsage", + { + "period_start": str, + "resets_at": str, + "emails": BillingUsageEmails, + "contacts": BillingUsageContacts, + "segments": BillingUsageSegments, + "broadcasts": BillingUsageBroadcasts, + "domains": BillingUsageDomains, + "members": BillingUsageMembers, + }, +) + + +Billing = TypedDict( + "Billing", + { + "object": Literal["billing"], + "plan": str, + "interval": Union[Literal["monthly", "yearly"], None], + "status": Union[str, None], + "current_period_end": Union[str, None], + "cancel_at_period_end": bool, + "cancel_at": Union[str, None], + "scheduled_change": Union[BillingScheduledChange, None], + "payment_issue": Union[BillingPaymentIssue, None], + "usage": BillingUsage, + "next_tier": Union[BillingNextTier, None], + }, +) + + +BillingCheckout = TypedDict( + "BillingCheckout", + { + "object": Literal["billing_checkout"], + "action": Literal["checkout", "confirm"], + "url": str, + "plan": str, + "interval": Literal["monthly", "yearly"], + "direction": Union[Literal["up", "down", "same"], None], + "effective_at": Union[str, None], + }, +) + + +BillingPortalSession = TypedDict( + "BillingPortalSession", + { + "object": Literal["billing_portal_session"], + "url": str, + }, +) + + +Invoice = TypedDict( + "Invoice", + { + "object": Literal["invoice"], + "id": str, + "number": Union[str, None], + "created_at": str, + "amount": int, + "amount_paid": int, + "currency": str, + "status": Union[Literal["draft", "open", "paid", "uncollectible", "void"], None], + "hosted_invoice_url": Union[str, None], + "invoice_pdf": Union[str, None], + }, +) + + ListLogsResponse = TypedDict( "ListLogsResponse", { @@ -2188,6 +2397,7 @@ "granularity": Literal["hourly", "daily", "weekly", "monthly"], "totals": dict[str, float], "data": NotRequired[list[GetEmailMetricsResponseDataItem]], + "providers": NotRequired[list[MailboxProviderMetrics]], }, ) @@ -2630,6 +2840,71 @@ ) +GetAccountActivationResponse = TypedDict( + "GetAccountActivationResponse", + { + "status": Literal["pending", "active", "rejected"], + "plan": str, + "email_verified": bool, + "free_trial_requested_at": Union[str, None], + "rejection_reason": Union[str, None], + }, +) + + +RequestFreeTrialParams = TypedDict( + "RequestFreeTrialParams", + { + "use_case": str, + "source": NotRequired[str], + "source_other": NotRequired[str], + }, +) + + +RequestFreeTrialResponse = TypedDict( + "RequestFreeTrialResponse", + { + "requested": bool, + "message": str, + }, +) + + +RedeemActivationInviteParams = TypedDict( + "RedeemActivationInviteParams", + { + "code": str, + }, +) + + +RedeemActivationInviteResponse = TypedDict( + "RedeemActivationInviteResponse", + { + "status": Literal["active"], + "plan": Literal["free"], + }, +) + + +CreateActivationCheckoutParams = TypedDict( + "CreateActivationCheckoutParams", + { + "plan": Literal["starter", "growth", "scale"], + "interval": NotRequired[Literal["monthly", "yearly"]], + }, +) + + +CreateActivationCheckoutResponse = TypedDict( + "CreateActivationCheckoutResponse", + { + "url": str, + }, +) + + ListTeamMembersResponse = TypedDict( "ListTeamMembersResponse", { @@ -2677,6 +2952,33 @@ RevokeTeamInviteResponse: TypeAlias = dict[str, Any] +CreateBillingCheckoutParams = TypedDict( + "CreateBillingCheckoutParams", + { + "plan": Literal["starter", "growth", "scale"], + "interval": NotRequired[Literal["monthly", "yearly"]], + }, +) + + +CreateBillingPortalSessionParams = TypedDict( + "CreateBillingPortalSessionParams", + { + "flow": NotRequired[Literal["cancel"]], + }, +) + + +ListBillingInvoicesResponse = TypedDict( + "ListBillingInvoicesResponse", + { + "object": Literal["list"], + "has_more": bool, + "data": list[Invoice], + }, +) + + ListContactSegmentsResponse: TypeAlias = dict[str, Any] @@ -2913,6 +3215,137 @@ ) +CreateCliDeviceCodeParams = TypedDict( + "CreateCliDeviceCodeParams", + { + "client_name": NotRequired[str], + "hostname": NotRequired[str], + "scopes": NotRequired[list[str]], + }, +) + + +CreateCliDeviceCodeResponse = TypedDict( + "CreateCliDeviceCodeResponse", + { + "device_code": str, + "user_code": str, + "verification_uri": str, + "verification_uri_complete": str, + "expires_in": int, + "interval": int, + }, +) + + +ExchangeCliDeviceCodeParams = TypedDict( + "ExchangeCliDeviceCodeParams", + { + "device_code": str, + }, +) + + +ExchangeCliDeviceCodeResponseTeam = TypedDict( + "ExchangeCliDeviceCodeResponseTeam", + { + "id": str, + "name": str, + }, +) + + +ExchangeCliDeviceCodeResponse = TypedDict( + "ExchangeCliDeviceCodeResponse", + { + "id": str, + "name": str, + "key": str, + "prefix": str, + "scopes": list[str], + "created_at": str, + "team": ExchangeCliDeviceCodeResponseTeam, + }, +) + + +StartCliSignupParams = TypedDict( + "StartCliSignupParams", + { + "email": str, + "name": NotRequired[str], + "newsletter": NotRequired[bool], + "client_name": NotRequired[str], + "hostname": NotRequired[str], + }, +) + + +StartCliSignupResponse = TypedDict( + "StartCliSignupResponse", + { + "signup_id": str, + "email": str, + "expires_in": int, + "code_length": int, + }, +) + + +VerifyCliSignupParams = TypedDict( + "VerifyCliSignupParams", + { + "signup_id": str, + "code": str, + }, +) + + +VerifyCliSignupResponseAccount = TypedDict( + "VerifyCliSignupResponseAccount", + { + "id": str, + "email": str, + "status": Literal["pending"], + "plan": Literal["free"], + }, +) + + +VerifyCliSignupResponseActivation = TypedDict( + "VerifyCliSignupResponseActivation", + { + "required": bool, + "docs": str, + }, +) + + +VerifyCliSignupResponseTeam = TypedDict( + "VerifyCliSignupResponseTeam", + { + "id": str, + "name": str, + }, +) + + +VerifyCliSignupResponse = TypedDict( + "VerifyCliSignupResponse", + { + "id": str, + "name": str, + "key": str, + "prefix": str, + "scopes": list[str], + "created_at": str, + "team": VerifyCliSignupResponseTeam, + "account": VerifyCliSignupResponseAccount, + "activation": VerifyCliSignupResponseActivation, + }, +) + + ListWebhooksResponse = TypedDict( "ListWebhooksResponse", {