diff --git a/tests/fixtures/openapi.json b/tests/fixtures/openapi.json index 02b5196..4e88e3a 100644 --- a/tests/fixtures/openapi.json +++ b/tests/fixtures/openapi.json @@ -1,1731 +1,1760 @@ { - "openapi": "3.1.0", - "info": { - "title": "Sendly API", - "version": "1.0.0", - "description": "Sendly's public REST API. Authenticate either with a project API key as `Authorization: Bearer ` (`sk_*` for full access, `pk_*` for sending-only) or with a BetterAuth session cookie. Legacy `/api/*` endpoints return JSON envelopes of the form `{ success, data }` (success) or `{ error: { message, code } }` (failure). `/api/v1/*` endpoints return bare resource bodies on success and RFC 9457 `application/problem+json` documents on failure.", - "contact": { - "name": "Sendly Support", - "url": "https://sendly.now" - }, - "license": { - "name": "AGPL-3.0", - "url": "https://www.gnu.org/licenses/agpl-3.0.txt" - } - }, - "servers": [ - { - "url": "https://api.sendly.now" - } - ], - "tags": [ - { - "name": "Emails", - "description": "Send transactional email and inspect deliveries." - }, - { - "name": "Campaigns", - "description": "Bulk sends to a list, segment, or filtered audience. Served by the `/api/v1` surface (bare payloads, RFC 9457 errors)." - }, - { - "name": "Segments", - "description": "Saved audiences — a `DYNAMIC` filter re-evaluated on read, or a `STATIC` membership list. Served by the `/api/v1` surface (bare payloads, RFC 9457 errors)." - }, - { - "name": "Contacts", - "description": "Manage subscribers and per-contact custom data." - }, - { - "name": "Lists", - "description": "Self-serve list membership. These two endpoints accept sending-only keys so they can back a public subscribe/unsubscribe form; list management itself is dashboard-only." - }, - { - "name": "Domains", - "description": "Register sending domains and manage SES verification." - }, - { - "name": "Templates", - "description": "Reusable email templates referenced by send operations." - }, - { - "name": "Webhooks", - "description": "User-managed outbound webhooks for email and contact events." - }, - { - "name": "Suppression", - "description": "Project-scoped suppression list. Hard bounces and complaints land here automatically." - }, - { - "name": "Workflows", - "description": "Event-triggered automations and the contact runs through them. Served by the `/api/v1` surface (bare payloads, RFC 9457 errors)." - }, - { - "name": "Events", - "description": "Track custom contact events from your application." - }, - { - "name": "Analytics", - "description": "Aggregate sending and engagement metrics. Every read is bounded to a window of at most 90 days and cached for 15 minutes." - }, - { - "name": "Usage", - "description": "Current email usage against the monthly and daily limits the platform enforces." - }, - { - "name": "Verify", - "description": "Open email-validation endpoint (no auth required). Used by the marketing-site verifier." - } - ], "components": { - "securitySchemes": { - "ApiKeyAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "API Key", - "description": "API key authentication. Use a `sk_*` (FULL) or `pk_*` (SENDING_ONLY) key as the bearer token. Public keys (`pk_*`) are restricted to the email-send endpoints and the self-serve list subscribe/unsubscribe pair; every other endpoint — event tracking included — answers 403 for them. In scope terms (used by `/api/v1/*` operations): FULL keys hold every scope; SENDING_ONLY keys hold only `emails:send`." - }, - "SessionAuth": { - "type": "apiKey", - "in": "cookie", - "name": "better-auth.session_token", - "description": "BetterAuth session cookie. Used by the dashboard / browser clients. When present, the active project is taken from the `x-project-id` header." - } - }, + "parameters": {}, "schemas": { - "Error": { - "type": "object", + "AddDomainBody": { + "description": "Body for POST /api/domains. `projectId` is optional for API-key auth (derived from key) and required for session auth. `region` pins the SES region; on the first domain it locks the project, after that it must match the project's region.", "properties": { - "success": { - "type": "boolean", - "enum": [ - false - ] + "domain": { + "maxLength": 253, + "minLength": 3, + "type": "string" }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - }, - "details": { - "type": "object", - "properties": { - "errors": { - "type": "array", - "items": {} - } - }, - "required": [ - "errors" - ] - } - }, - "required": [ - "message", - "code" - ] + "projectId": { + "format": "uuid", + "type": "string" + }, + "region": { + "description": "Override SES region for this domain. Defaults to the project region or the env default. Required to match an existing project region.", + "enum": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ], + "type": "string" } }, "required": [ - "error" + "domain" ], - "description": "Standard error envelope returned by all 4xx/5xx responses. Migrated routes include `success: false`; 422 validation errors add `error.details.errors`." + "type": "object" }, - "Problem": { - "type": "object", + "AddSuppression": { + "description": "Body for POST /api/suppression — manually add an email to the suppression list.", "properties": { - "type": { - "type": "string", - "format": "uri", - "description": "Dereferenceable URI identifying the error class, anchored on the docs errors page." - }, - "title": { - "type": "string", - "description": "Short, stable summary — the same for every occurrence of a `type`." + "email": { + "format": "email", + "type": "string" }, - "status": { - "type": "integer", - "description": "HTTP status code, repeated in the body." + "reason": { + "default": "MANUAL", + "enum": [ + "HARD_BOUNCE", + "COMPLAINT", + "MANUAL", + "UNSUBSCRIBE" + ], + "type": "string" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "AnalyticsCampaignStatsV1": { + "description": "Campaign counters and engagement over the window.", + "properties": { + "active": { + "description": "Campaigns in DRAFT or SCHEDULED.", + "type": "integer" }, - "detail": { - "type": "string", - "description": "Explanation specific to this occurrence." + "average_click_rate": { + "type": "number" }, - "instance": { - "type": "string", - "description": "Request path the failure occurred on." + "average_open_rate": { + "description": "Percentage, one decimal place.", + "type": "number" }, - "code": { - "type": "string", - "description": "Machine-readable lowercase error code, e.g. `scope_missing`." + "completed": { + "type": "integer" }, - "request_id": { - "type": "string", - "description": "Correlation id — quote it in support requests." + "total": { + "type": "integer" }, - "errors": { - "type": "array", + "window": { + "$ref": "#/components/schemas/AnalyticsWindowV1" + } + }, + "required": [ + "total", + "active", + "completed", + "average_open_rate", + "average_click_rate", + "window" + ], + "type": "object" + }, + "AnalyticsTimeseriesV1": { + "description": "Daily email counters across the window. Every day in range is present, zero-filled.", + "properties": { + "data": { "items": { - "type": "object", "properties": { - "pointer": { - "type": "string", - "description": "RFC 6901 JSON Pointer to the offending field." + "bounces": { + "type": "integer" }, - "code": { - "type": "string" + "clicks": { + "type": "integer" }, - "message": { + "date": { + "format": "date-time", "type": "string" + }, + "delivered": { + "type": "integer" + }, + "emails": { + "type": "integer" + }, + "opens": { + "type": "integer" } }, "required": [ - "pointer", - "code", - "message" - ] + "date", + "emails", + "delivered", + "opens", + "clicks", + "bounces" + ], + "type": "object" }, - "description": "Field-level failures. Present on 422 `validation_error` responses." + "type": "array" + }, + "window": { + "$ref": "#/components/schemas/AnalyticsWindowV1" } }, "required": [ - "type", - "title", - "status", - "code" + "data", + "window" ], - "description": "RFC 9457 problem document, served as `application/problem+json`. Returned by every 4xx/5xx response on the `/api/v1` surface." + "type": "object" }, - "SuccessEmpty": { - "type": "object", + "AnalyticsTopCampaignsV1": { + "description": "Sent campaigns ranked by open rate.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "data": { + "items": { + "properties": { + "click_rate": { + "type": "number" + }, + "clicked": { + "type": "integer" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "open_rate": { + "type": "number" + }, + "opened": { + "type": "integer" + }, + "sent": { + "type": "integer" + }, + "subject": { + "type": "string" + } + }, + "required": [ + "id", + "subject", + "sent", + "opened", + "clicked", + "open_rate", + "click_rate" + ], + "type": "object" + }, + "type": "array" + }, + "window": { + "$ref": "#/components/schemas/AnalyticsWindowV1" } }, "required": [ - "success" + "data", + "window" ], - "description": "Bare success envelope with no payload." + "type": "object" }, - "IdResponse": { - "type": "object", + "AnalyticsWindowV1": { + "description": "The time range this response was computed over, after the 90-day clamp.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "from": { + "format": "date-time", + "type": "string" }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - } - }, - "required": [ - "id" - ] + "to": { + "format": "date-time", + "type": "string" } }, "required": [ - "success", - "data" + "from", + "to" ], - "description": "Success envelope carrying the affected resource's id, e.g. after a delete." + "type": "object" }, - "Contact": { - "type": "object", + "BatchEntryResult": { + "description": "Per-row result in a batch send response.", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "projectId": { - "type": "string", - "format": "uuid" - }, - "email": { - "type": "string", - "format": "email" - }, - "subscribed": { - "type": "boolean" + "data": { + "$ref": "#/components/schemas/SendEmailData" }, - "customFields": { - "type": [ - "object", - "null" + "error": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message", + "code" ], - "additionalProperties": {} + "type": "object" }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" + "index": { + "type": "integer" }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" + "status": { + "enum": [ + "ok", + "error" + ], + "type": "string" } }, "required": [ - "id", - "projectId", - "email", - "subscribed", - "createdAt", - "updatedAt" + "index", + "status" ], - "description": "A subscriber/contact within a project." + "type": "object" }, - "ContactListResponse": { - "type": "object", + "BatchSendBody": { + "description": "Batch send wrapper. Up to 100 entries.", + "properties": { + "emails": { + "items": { + "$ref": "#/components/schemas/SendEmail" + }, + "maxItems": 100, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "emails" + ], + "type": "object" + }, + "BatchSendResponse": { + "description": "Multi-status response for `POST /api/emails/batch`. HTTP 207 if any entry failed, else 200.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, "data": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Contact" - } - }, - "total": { - "type": "integer" - }, - "nextCursor": { - "type": [ - "string", - "null" - ], - "description": "Cursor for the next page, or null on the last page." - }, - "hasMore": { - "type": "boolean" - } + "items": { + "$ref": "#/components/schemas/BatchEntryResult" }, - "required": [ - "data", - "total", - "nextCursor", - "hasMore" - ] + "type": "array" + }, + "success": { + "type": "boolean" } }, "required": [ "success", "data" ], - "description": "Cursor-paginated list of contacts." + "type": "object" }, - "Template": { - "type": "object", + "CampaignV1": { + "description": "A campaign as exposed on the v1 API.", "properties": { - "id": { - "type": "string", - "format": "uuid" + "audience_type": { + "enum": [ + "ALL", + "FILTERED", + "SEGMENT" + ], + "type": "string" }, - "projectId": { - "type": "string", - "format": "uuid" + "created_at": { + "format": "date-time", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" }, "name": { "type": "string" }, - "description": { + "scheduled_at": { + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "sent_at": { + "format": "date-time", "type": [ "string", "null" ] }, + "stats": { + "properties": { + "bounced": { + "type": "integer" + }, + "clicked": { + "type": "integer" + }, + "delivered": { + "type": "integer" + }, + "opened": { + "type": "integer" + }, + "sent": { + "type": "integer" + }, + "total_recipients": { + "type": "integer" + } + }, + "required": [ + "total_recipients", + "sent", + "delivered", + "opened", + "clicked", + "bounced" + ], + "type": "object" + }, + "status": { + "enum": [ + "DRAFT", + "SCHEDULED", + "SENDING", + "PAUSED", + "SENT", + "CANCELLED" + ], + "type": "string" + }, "subject": { "type": "string" + } + }, + "required": [ + "id", + "name", + "status", + "subject", + "audience_type", + "scheduled_at", + "sent_at", + "created_at", + "stats" + ], + "type": "object" + }, + "CampaignV1Create": { + "description": "Body for POST /api/v1/campaigns. `segment_id` is required when `audience_type` is `SEGMENT`, and `audience_condition` is required when it is `FILTERED`.", + "properties": { + "audience_condition": { + "additionalProperties": {}, + "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND.", + "type": "object" + }, + "audience_type": { + "description": "`ALL` — every subscribed contact. `FILTERED` — the contacts matching `audience_condition`. `SEGMENT` — the members of `segment_id`.", + "enum": [ + "ALL", + "FILTERED", + "SEGMENT" + ], + "type": "string" }, "body": { + "minLength": 1, + "type": "string" + }, + "description": { + "maxLength": 500, "type": "string" }, "from": { - "type": "string", - "format": "email" + "description": "Sender address. Its domain must be verified for this project.", + "format": "email", + "type": "string" }, - "fromName": { + "from_name": { + "maxLength": 100, "type": [ "string", "null" ] }, - "replyTo": { + "name": { + "maxLength": 200, + "minLength": 1, + "type": "string" + }, + "reply_to": { + "format": "email", "type": [ "string", "null" - ], - "format": "email" + ] + }, + "segment_id": { + "format": "uuid", + "type": "string" + }, + "subject": { + "minLength": 1, + "type": "string" }, "type": { - "type": "string", + "default": "MARKETING", "enum": [ - "MARKETING", "TRANSACTIONAL", + "MARKETING", "HEADLESS" - ] - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" + ], + "type": "string" } }, "required": [ - "id", - "projectId", "name", "subject", "body", "from", - "type", - "createdAt", - "updatedAt" + "audience_type" ], - "description": "A reusable email template." + "type": "object" }, - "TemplateListResponse": { - "type": "object", + "CampaignV1Deleted": { + "description": "Acknowledgement that a campaign was deleted.", "properties": { - "success": { - "type": "boolean", + "deleted": { "enum": [ true - ] + ], + "type": "boolean" }, - "data": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Template" - } - }, - "total": { - "type": "integer" - }, - "cursor": { - "type": "string", - "description": "Cursor for the next page; omitted on the last page." - }, - "hasMore": { - "type": "boolean" - } + "id": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "id", + "deleted" + ], + "type": "object" + }, + "CampaignV1List": { + "description": "Cursor-paginated list of campaigns.", + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/CampaignV1" }, - "required": [ - "data", - "total", - "hasMore" + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "next_cursor": { + "description": "Pass as `after` to fetch the next page. `null` on the last page.", + "type": [ + "string", + "null" ] } }, "required": [ - "success", - "data" + "data", + "has_more", + "next_cursor" ], - "description": "Cursor-paginated list of templates." + "type": "object" }, - "Domain": { - "type": "object", + "CampaignV1Send": { + "description": "Body for POST /api/v1/campaigns/{id}/send.", "properties": { - "id": { - "type": "string", - "format": "uuid" + "scheduled_for": { + "description": "RFC 3339 timestamp, strictly in the future. Omit to start sending immediately.", + "format": "date-time", + "type": "string" + } + }, + "type": "object" + }, + "CampaignV1Stats": { + "description": "Materialized delivery and engagement counters for one campaign.", + "properties": { + "bounce_rate": { + "type": "number" }, - "projectId": { - "type": "string", - "format": "uuid" + "bounced": { + "type": "integer" }, - "name": { + "click_rate": { + "type": "number" + }, + "clicked": { + "type": "integer" + }, + "delivered": { + "type": "integer" + }, + "delivery_rate": { + "type": "number" + }, + "open_rate": { + "type": "number" + }, + "opened": { + "type": "integer" + }, + "sent": { + "type": "integer" + }, + "total_recipients": { + "type": "integer" + } + }, + "required": [ + "total_recipients", + "sent", + "delivered", + "opened", + "clicked", + "bounced", + "open_rate", + "click_rate", + "bounce_rate", + "delivery_rate" + ], + "type": "object" + }, + "CampaignV1Update": { + "description": "Body for PATCH /api/v1/campaigns/{id}. All fields optional.", + "properties": { + "audience_condition": { + "additionalProperties": {}, + "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND.", + "type": "object" + }, + "audience_type": { + "enum": [ + "ALL", + "FILTERED", + "SEGMENT" + ], "type": "string" }, - "verified": { - "type": "boolean" + "body": { + "minLength": 1, + "type": "string" }, - "region": { + "description": { + "maxLength": 500, + "type": "string" + }, + "from": { + "description": "Sender address. Its domain must be verified for this project.", + "format": "email", + "type": "string" + }, + "from_name": { + "maxLength": 100, "type": [ "string", "null" ] }, - "dkim": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "required": [ - "type", - "name", - "value" - ] - } + "name": { + "maxLength": 200, + "minLength": 1, + "type": "string" }, - "mailFromDomain": { + "reply_to": { + "format": "email", "type": [ "string", "null" - ], - "description": "Custom MAIL FROM subdomain SES has on record (normally `sendly.`)." + ] }, - "mailFromStatus": { - "type": [ - "string", - "null" - ], + "segment_id": { + "format": "uuid", + "type": "string" + }, + "subject": { + "minLength": 1, + "type": "string" + }, + "type": { "enum": [ - "Pending", - "Success", - "Failed", - "TemporaryFailure", - "NotConfigured", - null + "TRANSACTIONAL", + "MARKETING", + "HEADLESS" ], - "description": "SES custom MAIL FROM setup state. Only `Success` means SES is using it." - }, + "type": "string" + } + }, + "type": "object" + }, + "Contact": { + "description": "A subscriber/contact within a project.", + "properties": { "createdAt": { - "type": "string", + "description": "ISO 8601 datetime string", "format": "date-time", - "description": "ISO 8601 datetime string" + "type": "string" + }, + "customFields": { + "additionalProperties": {}, + "type": [ + "object", + "null" + ] + }, + "email": { + "format": "email", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "projectId": { + "format": "uuid", + "type": "string" + }, + "subscribed": { + "type": "boolean" }, "updatedAt": { - "type": "string", + "description": "ISO 8601 datetime string", "format": "date-time", - "description": "ISO 8601 datetime string" + "type": "string" } }, "required": [ "id", "projectId", - "name", - "verified", + "email", + "subscribed", "createdAt", "updatedAt" ], - "description": "A sending domain registered with SES." + "type": "object" }, - "DomainListResponse": { - "type": "object", + "ContactBulkCreateBody": { + "description": "Bulk create up to 1000 contacts.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "type": "array", + "contacts": { "items": { - "$ref": "#/components/schemas/Domain" - } + "$ref": "#/components/schemas/CreateContact" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" } }, "required": [ - "success", - "data" + "contacts" ], - "description": "List of all domains for the auth'd project." + "type": "object" }, - "DomainVerificationStatus": { - "type": "object", + "ContactBulkDeleteBody": { + "description": "Bulk delete contacts. Provide either `ids` or `emails` (max 1000 each).", "properties": { - "verified": { - "type": "boolean" - }, - "mxRecords": { - "type": "array", + "emails": { "items": { + "format": "email", "type": "string" - } + }, + "maxItems": 1000, + "type": "array" }, - "dkim": { - "type": "array", + "ids": { "items": { - "type": "object", - "properties": { - "type": { - "type": "string" + "format": "uuid", + "type": "string" + }, + "maxItems": 1000, + "type": "array" + } + }, + "type": "object" + }, + "ContactListResponse": { + "description": "Cursor-paginated list of contacts.", + "properties": { + "data": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/Contact" }, - "name": { - "type": "string" - }, - "value": { - "type": "string" - } + "type": "array" }, - "required": [ - "type", - "name", - "value" - ] - } - }, - "mailFromDomain": { - "type": [ - "string", - "null" - ] - }, - "mailFromStatus": { - "type": [ - "string", - "null" + "hasMore": { + "type": "boolean" + }, + "nextCursor": { + "description": "Cursor for the next page, or null on the last page.", + "type": [ + "string", + "null" + ] + }, + "total": { + "type": "integer" + } + }, + "required": [ + "data", + "total", + "nextCursor", + "hasMore" ], + "type": "object" + }, + "success": { "enum": [ - "Pending", - "Success", - "Failed", - "TemporaryFailure", - "NotConfigured", - null + true ], - "description": "SES custom MAIL FROM setup state. Only `Success` means SES is using it." + "type": "boolean" } }, "required": [ - "verified" + "success", + "data" ], - "description": "Outcome of a verification check against SES." + "type": "object" }, - "Email": { - "type": "object", + "CreateContact": { + "description": "Body for POST /api/contacts and /api/contacts/upsert.", "properties": { - "id": { - "type": "string", - "format": "uuid" + "customFields": { + "additionalProperties": {}, + "description": "Arbitrary JSON value (string, number, boolean, null, array, or object).", + "type": "object" }, - "projectId": { - "type": "string", - "format": "uuid" + "email": { + "format": "email", + "type": "string" }, - "from": { + "subscribed": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "CreateTemplate": { + "description": "Body for POST /api/templates.", + "properties": { + "body": { + "minLength": 1, "type": "string" }, - "to": { + "description": { + "maxLength": 500, "type": "string" }, - "subject": { + "from": { + "format": "email", "type": "string" }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "SENT", - "DELIVERED", - "OPENED", - "CLICKED", - "BOUNCED", - "COMPLAINED", - "FAILED" + "fromName": { + "maxLength": 100, + "type": [ + "string", + "null" ] }, - "tags": { - "type": "array", - "items": { - "type": "string" - } + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" }, - "error": { + "replyTo": { + "format": "email", "type": [ "string", "null" ] }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" + "subject": { + "minLength": 1, + "type": "string" }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" + "type": { + "default": "MARKETING", + "enum": [ + "TRANSACTIONAL", + "MARKETING", + "HEADLESS" + ], + "type": "string" } }, "required": [ - "id", - "projectId", - "from", - "to", + "name", "subject", - "status", - "tags", - "createdAt", - "updatedAt" + "body", + "from" ], - "description": "A sent (or queued) transactional email." + "type": "object" }, - "SendEmailRecipientResult": { - "type": "object", + "CreateWebhook": { + "description": "Body for POST /api/webhooks — register a user webhook for one or more events.", "properties": { - "contact": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "email": { - "type": "string", - "format": "email" - } + "eventTypes": { + "items": { + "enum": [ + "email.sent", + "email.delivered", + "email.opened", + "email.clicked", + "email.bounced", + "email.complained", + "email.failed", + "contact.created", + "contact.unsubscribed", + "contacts.bulk_created" + ], + "type": "string" }, - "required": [ - "id", - "email" - ] + "minItems": 1, + "type": "array" }, - "email": { - "type": "string", - "format": "uuid" + "url": { + "format": "uri", + "type": "string" } }, "required": [ - "contact", - "email" + "url", + "eventTypes" ], - "description": "Per-recipient result: the upserted `contact` (id + email) and `email` — the id of the queued email record for that recipient." + "type": "object" }, - "SendEmailData": { - "type": "object", + "Domain": { + "description": "A sending domain registered with SES.", "properties": { - "emails": { - "type": "array", + "createdAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" + }, + "dkim": { "items": { - "$ref": "#/components/schemas/SendEmailRecipientResult" - } + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "name", + "value" + ], + "type": "object" + }, + "type": "array" }, - "timestamp": { - "type": "string", + "id": { + "format": "uuid", + "type": "string" + }, + "mailFromDomain": { + "description": "Custom MAIL FROM subdomain SES has on record (normally `sendly.`).", + "type": [ + "string", + "null" + ] + }, + "mailFromStatus": { + "description": "SES custom MAIL FROM setup state. Only `Success` means SES is using it.", + "enum": [ + "Pending", + "Success", + "Failed", + "TemporaryFailure", + "NotConfigured", + null + ], + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "projectId": { + "format": "uuid", + "type": "string" + }, + "region": { + "type": [ + "string", + "null" + ] + }, + "updatedAt": { + "description": "ISO 8601 datetime string", "format": "date-time", - "description": "ISO 8601 datetime string" + "type": "string" + }, + "verified": { + "type": "boolean" } }, "required": [ - "emails", - "timestamp" + "id", + "projectId", + "name", + "verified", + "createdAt", + "updatedAt" ], - "description": "Result of a send (`executeSendEmail`): one `emails` entry per recipient — a single request with an array `to` fans out to several — plus the send `timestamp`." + "type": "object" }, - "SendEmailResponse": { - "type": "object", + "DomainListResponse": { + "description": "List of all domains for the auth'd project.", "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + }, "success": { - "type": "boolean", "enum": [ true - ] - }, - "data": { - "$ref": "#/components/schemas/SendEmailData" + ], + "type": "boolean" } }, "required": [ "success", "data" ], - "description": "Successful response for `POST /api/emails`. `data.emails[i].email` is the queued email id for recipient `i`; poll `GET /api/emails/{id}` for its delivery status." + "type": "object" }, - "BatchEntryResult": { - "type": "object", + "DomainVerificationStatus": { + "description": "Outcome of a verification check against SES.", "properties": { - "index": { - "type": "integer" + "dkim": { + "items": { + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "name", + "value" + ], + "type": "object" + }, + "type": "array" }, - "status": { - "type": "string", + "mailFromDomain": { + "type": [ + "string", + "null" + ] + }, + "mailFromStatus": { + "description": "SES custom MAIL FROM setup state. Only `Success` means SES is using it.", "enum": [ - "ok", - "error" + "Pending", + "Success", + "Failed", + "TemporaryFailure", + "NotConfigured", + null + ], + "type": [ + "string", + "null" ] }, - "data": { - "$ref": "#/components/schemas/SendEmailData" + "mxRecords": { + "items": { + "type": "string" + }, + "type": "array" + }, + "verified": { + "type": "boolean" + } + }, + "required": [ + "verified" + ], + "type": "object" + }, + "Email": { + "description": "A sent (or queued) transactional email.", + "properties": { + "createdAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" }, "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message", - "code" + "type": [ + "string", + "null" ] + }, + "from": { + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "projectId": { + "format": "uuid", + "type": "string" + }, + "status": { + "enum": [ + "PENDING", + "SENT", + "DELIVERED", + "OPENED", + "CLICKED", + "BOUNCED", + "COMPLAINED", + "FAILED" + ], + "type": "string" + }, + "subject": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "to": { + "type": "string" + }, + "updatedAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" } }, "required": [ - "index", - "status" + "id", + "projectId", + "from", + "to", + "subject", + "status", + "tags", + "createdAt", + "updatedAt" ], - "description": "Per-row result in a batch send response." + "type": "object" }, - "BatchSendResponse": { - "type": "object", + "EmailGetResponse": { + "description": "Single email with its events.", "properties": { + "data": { + "$ref": "#/components/schemas/Email" + }, "success": { + "enum": [ + true + ], "type": "boolean" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BatchEntryResult" - } } }, "required": [ "success", "data" ], - "description": "Multi-status response for `POST /api/emails/batch`. HTTP 207 if any entry failed, else 200." + "type": "object" }, - "BatchSendBody": { - "type": "object", + "EmailListResponse": { + "description": "Cursor-paginated list of emails.", "properties": { - "emails": { - "type": "array", + "data": { "items": { - "$ref": "#/components/schemas/SendEmail" + "$ref": "#/components/schemas/Email" }, - "minItems": 1, - "maxItems": 100 + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + }, + "success": { + "enum": [ + true + ], + "type": "boolean" } }, "required": [ - "emails" + "success", + "data" ], - "description": "Batch send wrapper. Up to 100 entries." + "type": "object" }, - "SendEmail": { - "type": "object", + "Error": { + "description": "Standard error envelope returned by all 4xx/5xx responses. Migrated routes include `success: false`; 422 validation errors add `error.details.errors`.", "properties": { - "to": { - "anyOf": [ - { - "type": "string", - "format": "email" + "error": { + "properties": { + "code": { + "type": "string" }, - { - "type": "object", + "details": { "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" + "errors": { + "items": {}, + "type": "array" } }, "required": [ - "email" - ] + "errors" + ], + "type": "object" }, - { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "format": "email" - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - } - }, - "required": [ - "email" - ] - } - ] - } + "message": { + "type": "string" } - ] - }, - "subject": { - "type": "string", - "minLength": 1, - "maxLength": 998, - "pattern": "^[^\\r\\n]*$" - }, - "body": { - "type": "string", - "minLength": 1 - }, - "template": { - "type": "string", - "format": "uuid" + }, + "required": [ + "message", + "code" + ], + "type": "object" }, - "subscribed": { + "success": { + "enum": [ + false + ], "type": "boolean" + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "EventNamesV1": { + "description": "Every distinct event name in the project, most frequent first.", + "properties": { + "data": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, + "EventStatsV1": { + "description": "Per-name event counts over the applied window.", + "properties": { + "data": { + "items": { + "properties": { + "count": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name", + "count" + ], + "type": "object" + }, + "type": "array" }, - "name": { - "type": "string" - }, - "from": { - "anyOf": [ - { - "type": "string", - "format": "email" - }, - { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[^\\r\\n]*$" - }, - "email": { - "type": "string", - "format": "email" - } - }, - "required": [ - "email" - ] - } - ] - }, - "reply": { - "type": "string", - "format": "email" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 998, - "pattern": "^[^\\r\\n]*$" - } - }, - "data": { - "type": "object", - "additionalProperties": {}, - "description": "Arbitrary JSON value (string, number, boolean, null, array, or object)." - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "filename": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "pattern": "^[^\\r\\n\"]+$" - }, - "content": { - "type": "string", - "minLength": 1 - }, - "contentType": { - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "contentId": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "pattern": "^[^<>\\r\\n]+$" - }, - "disposition": { - "type": "string", - "enum": [ - "attachment", - "inline" - ], - "default": "attachment" - } - }, - "required": [ - "filename", - "content", - "contentType" - ] - }, - "maxItems": 10 - }, - "tags": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "maxItems": 10 - }, - "cc": { - "type": "array", - "items": { - "type": "string", - "format": "email" - } - }, - "bcc": { - "type": "array", - "items": { - "type": "string", - "format": "email" - } - } - }, - "required": [ - "to" - ], - "description": "Body for POST /api/emails — send a single transactional email. Either `template` or `subject`+`body` is required." - }, - "ContactBulkCreateBody": { - "type": "object", - "properties": { - "contacts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CreateContact" - }, - "minItems": 1, - "maxItems": 1000 + "window": { + "$ref": "#/components/schemas/AnalyticsWindowV1" } }, "required": [ - "contacts" + "data", + "window" ], - "description": "Bulk create up to 1000 contacts." + "type": "object" }, - "CreateContact": { - "type": "object", + "EventTrackV1": { + "description": "Body for POST /api/v1/events.", "properties": { - "email": { - "type": "string", - "format": "email" + "contact_id": { + "description": "Contact the event belongs to. Must already exist in this project — unlike the legacy `POST /api/track`, this endpoint never creates contacts. Omit for a project-level event.", + "format": "uuid", + "type": "string" }, - "subscribed": { - "type": "boolean", - "default": true + "data": { + "additionalProperties": { + "additionalProperties": {}, + "description": "Arbitrary JSON value (string, number, boolean, null, array, or object).", + "type": [ + "string", + "number", + "boolean", + "object", + "array", + "null" + ] + }, + "description": "Arbitrary event payload.", + "type": "object" }, - "customFields": { - "type": "object", - "additionalProperties": {}, - "description": "Arbitrary JSON value (string, number, boolean, null, array, or object)." + "name": { + "description": "Event name, e.g. `user.signup`.", + "maxLength": 200, + "minLength": 1, + "type": "string" } }, "required": [ - "email" + "name" ], - "description": "Body for POST /api/contacts and /api/contacts/upsert." - }, - "ContactBulkDeleteBody": { - "type": "object", - "properties": { - "ids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "maxItems": 1000 - }, - "emails": { - "type": "array", - "items": { - "type": "string", - "format": "email" - }, - "maxItems": 1000 - } - }, - "description": "Bulk delete contacts. Provide either `ids` or `emails` (max 1000 each)." - }, - "UpdateContactBody": { - "type": "object", - "properties": { - "subscribed": { - "type": "boolean" - }, - "customFields": { - "type": "object", - "additionalProperties": {} - } - }, - "description": "Body for PATCH /api/contacts/{id}. `email` is immutable here — use upsert to change addresses." + "type": "object" }, - "Suppression": { - "type": "object", + "EventV1": { + "description": "A recorded custom event.", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "projectId": { - "type": "string", - "format": "uuid" + "contact_id": { + "format": "uuid", + "type": [ + "string", + "null" + ] }, - "email": { - "type": "string", - "format": "email" + "created_at": { + "format": "date-time", + "type": "string" }, - "reason": { - "type": "string", - "enum": [ - "HARD_BOUNCE", - "COMPLAINT", - "MANUAL", - "UNSUBSCRIBE" + "data": { + "additionalProperties": {}, + "description": "The payload recorded with the event, or null.", + "type": [ + "object", + "null" ] }, - "source": { - "type": "string", - "enum": [ - "SES_WEBHOOK", - "API", - "DASHBOARD" + "email_id": { + "format": "uuid", + "type": [ + "string", + "null" ] }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" + "id": { + "format": "uuid", + "type": "string" + }, + "name": { + "type": "string" } }, "required": [ "id", - "projectId", - "email", - "reason", - "source", - "createdAt" + "name", + "contact_id", + "email_id", + "data", + "created_at" ], - "description": "A single suppressed-email record." + "type": "object" }, - "SuppressionListResponse": { - "type": "object", + "EventV1List": { + "description": "Cursor-paginated list of events, newest first.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, "data": { - "type": "array", "items": { - "$ref": "#/components/schemas/Suppression" - } + "$ref": "#/components/schemas/EventV1" + }, + "type": "array" }, - "nextCursor": { - "type": [ - "string", - "null" - ] + "has_more": { + "type": "boolean" }, - "cursor": { + "next_cursor": { + "description": "Pass as `after` to fetch the next page. `null` on the last page.", "type": [ "string", "null" ] - }, - "hasMore": { - "type": "boolean" } }, "required": [ - "success", - "data" + "data", + "has_more", + "next_cursor" ], - "description": "Cursor-paginated list of suppressions." + "type": "object" }, - "SuppressionCheckResponse": { - "type": "object", + "IdResponse": { + "description": "Success envelope carrying the affected resource's id, e.g. after a delete.", "properties": { - "suppressed": { - "type": "boolean" - }, - "reason": { - "type": "string", - "enum": [ - "HARD_BOUNCE", - "COMPLAINT", - "MANUAL", - "UNSUBSCRIBE" - ] + "data": { + "properties": { + "id": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" }, - "source": { - "type": "string", + "success": { "enum": [ - "SES_WEBHOOK", - "API", - "DASHBOARD" - ] - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" + true + ], + "type": "boolean" } }, "required": [ - "suppressed" + "success", + "data" ], - "description": "Result of GET /api/suppression/{email} — whether the address is suppressed." + "type": "object" }, - "Webhook": { - "type": "object", + "ListSubscribe": { + "description": "Body for POST /api/lists/{id}/subscribe.", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "projectId": { - "type": "string", - "format": "uuid" - }, - "url": { - "type": "string", - "format": "uri" - }, - "eventTypes": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email.sent", - "email.delivered", - "email.opened", - "email.clicked", - "email.bounced", - "email.complained", - "email.failed", - "contact.created", - "contact.unsubscribed", - "contacts.bulk_created" - ] - } - }, - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "PAUSED", - "DISABLED" - ] - }, - "consecutiveFailures": { - "type": "integer" + "allowResubscribe": { + "default": false, + "description": "Permission to reverse an earlier opt-out. When the email already has an UNSUBSCRIBED membership on this list, the call fails with 409 RESUBSCRIBE_CONFIRMATION_REQUIRED unless this is `true`. Send `true` only when the contact is acting for themselves — a public subscribe form they submitted is affirmative consent — never for an operator-initiated add.", + "type": "boolean" }, - "disabledAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "ISO 8601 datetime string" + "data": { + "additionalProperties": {}, + "description": "Custom fields to upsert onto the contact as part of subscribing.", + "type": "object" }, - "lastFour": { + "email": { + "format": "email", "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" } }, "required": [ - "id", - "projectId", - "url", - "eventTypes", - "status", - "consecutiveFailures", - "createdAt", - "updatedAt" + "email" ], - "description": "A user-managed outbound webhook." + "type": "object" }, - "WebhookCreateResponse": { - "type": "object", + "ListSubscribeResponse": { + "description": "Result of a list-subscribe call.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, "data": { - "allOf": [ - { - "$ref": "#/components/schemas/Webhook" + "properties": { + "confirmToken": { + "description": "Present only when the list has doubleOptIn enabled. Sendly does not send the confirmation email — deliver /api/lists/confirm?token= to the contact. Valid for 24 hours.", + "type": "string" }, - { - "type": "object", - "properties": { - "secret": { - "type": "string", - "description": "Plaintext shared secret. Returned ONCE on create." - } - }, - "required": [ - "secret" + "created": { + "description": "True when the membership row did not exist before this call.", + "type": "boolean" + }, + "membershipId": { + "format": "uuid", + "type": "string" + }, + "previousStatus": { + "description": "Status the membership held before this call; null when it did not exist. Use this rather than `created` to describe the transition to the user.", + "enum": [ + "PENDING", + "CONFIRMED", + "UNSUBSCRIBED", + null + ], + "type": [ + "string", + "null" ] + }, + "status": { + "enum": [ + "PENDING", + "CONFIRMED", + "UNSUBSCRIBED" + ], + "type": "string" } + }, + "required": [ + "membershipId", + "status", + "created", + "previousStatus" ], - "description": "A user-managed outbound webhook." - } - }, - "required": [ - "success", - "data" - ], - "description": "Result of POST /api/webhooks. `secret` is the only time the plaintext is returned — store it securely." - }, - "WebhookListResponse": { - "type": "object", - "properties": { + "type": "object" + }, "success": { - "type": "boolean", "enum": [ true - ] - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Webhook" - } + ], + "type": "boolean" } }, "required": [ "success", "data" ], - "description": "List of webhooks for the auth'd project." + "type": "object" }, - "WebhookGetResponse": { - "type": "object", + "ListUnsubscribe": { + "description": "Body for POST /api/lists/{id}/unsubscribe.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Webhook" + "email": { + "format": "email", + "type": "string" } }, "required": [ - "success", - "data" + "email" ], - "description": "Single webhook (no secret)." + "type": "object" }, - "WebhookRotateSecretResponse": { - "type": "object", + "ListUnsubscribeResponse": { + "description": "Echoes the address that was unsubscribed.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, "data": { - "type": "object", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "secret": { - "type": "string", - "description": "New plaintext shared secret." + "email": { + "format": "email", + "type": "string" } }, "required": [ - "id", - "secret" - ] + "email" + ], + "type": "object" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" } }, "required": [ "success", "data" ], - "description": "Response from POST /api/webhooks/{id}/rotate-secret — returns the new plaintext once." + "type": "object" }, - "WebhookCall": { - "type": "object", + "Problem": { + "description": "RFC 9457 problem document, served as `application/problem+json`. Returned by every 4xx/5xx response on the `/api/v1` surface.", "properties": { - "id": { - "type": "string", - "format": "uuid" + "code": { + "description": "Machine-readable lowercase error code, e.g. `scope_missing`.", + "type": "string" }, - "webhookId": { - "type": "string", - "format": "uuid" + "detail": { + "description": "Explanation specific to this occurrence.", + "type": "string" }, - "eventType": { + "errors": { + "description": "Field-level failures. Present on 422 `validation_error` responses.", + "items": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "pointer": { + "description": "RFC 6901 JSON Pointer to the offending field.", + "type": "string" + } + }, + "required": [ + "pointer", + "code", + "message" + ], + "type": "object" + }, + "type": "array" + }, + "instance": { + "description": "Request path the failure occurred on.", "type": "string" }, - "payload": { - "type": "object", - "additionalProperties": {} + "request_id": { + "description": "Correlation id — quote it in support requests.", + "type": "string" }, "status": { - "type": "string", - "enum": [ - "PENDING", - "SUCCESS", - "FAILED" - ] - }, - "attempt": { + "description": "HTTP status code, repeated in the body.", "type": "integer" }, - "responseStatus": { - "type": [ - "integer", - "null" - ] - }, - "responseBody": { - "type": [ - "string", - "null" - ] + "title": { + "description": "Short, stable summary — the same for every occurrence of a `type`.", + "type": "string" }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" + "type": { + "description": "Dereferenceable URI identifying the error class, anchored on the docs errors page.", + "format": "uri", + "type": "string" } }, "required": [ - "id", - "webhookId", - "eventType", - "payload", + "type", + "title", "status", - "attempt", - "createdAt" + "code" ], - "description": "An attempted webhook delivery." + "type": "object" }, - "WebhookCallsListResponse": { - "type": "object", + "SegmentContactV1": { + "description": "A contact belonging to a segment.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "created_at": { + "format": "date-time", + "type": "string" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookCall" - } + "custom_fields": { + "additionalProperties": {}, + "description": "Arbitrary JSON value (string, number, boolean, null, array, or object).", + "type": "object" }, - "nextCursor": { - "type": [ - "string", - "null" - ] + "email": { + "type": "string" }, - "cursor": { - "type": [ - "string", - "null" - ] + "id": { + "format": "uuid", + "type": "string" }, - "hasMore": { + "subscribed": { "type": "boolean" } }, "required": [ - "success", - "data" + "id", + "email", + "subscribed", + "custom_fields", + "created_at" ], - "description": "Cursor-paginated list of recent calls for a single webhook." + "type": "object" }, - "TrackEventResponse": { - "type": "object", + "SegmentContactV1List": { + "description": "Cursor-paginated list of the contacts belonging to a segment.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, "data": { - "type": "object", - "properties": { - "contact": { - "type": "string", - "format": "uuid" - }, - "event": { - "type": "string", - "format": "uuid" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 datetime string" - } + "items": { + "$ref": "#/components/schemas/SegmentContactV1" }, - "required": [ - "contact", - "event", - "timestamp" - ] - } - }, - "required": [ - "success", - "data" - ], - "description": "Response from POST /api/track." - }, - "VerifyEmailResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "type": "array" }, - "data": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email" - }, - "valid": { - "type": "boolean" - }, - "reason": { - "type": "string" - } - }, - "required": [ - "email", - "valid" - ], - "additionalProperties": {} + "has_more": { + "type": "boolean" + }, + "next_cursor": { + "description": "Pass as `after` to fetch the next page. `null` on the last page.", + "type": [ + "string", + "null" + ] } }, "required": [ - "success", - "data" + "data", + "has_more", + "next_cursor" ], - "description": "Response from POST /api/verify — outcome of the syntax/MX/disposable check." + "type": "object" }, - "EmailListResponse": { - "type": "object", + "SegmentV1": { + "description": "A segment as exposed on the v1 API.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "condition": { + "additionalProperties": {}, + "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND.", + "type": "object" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Email" - } + "created_at": { + "format": "date-time", + "type": "string" }, - "nextCursor": { + "description": { "type": [ "string", "null" ] - } - }, - "required": [ - "success", - "data" - ], - "description": "Cursor-paginated list of emails." - }, - "EmailGetResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", + }, + "id": { + "format": "uuid", + "type": "string" + }, + "member_count": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "track_membership": { + "type": "boolean" + }, + "type": { "enum": [ - true - ] + "DYNAMIC", + "STATIC" + ], + "type": "string" }, - "data": { - "$ref": "#/components/schemas/Email" + "updated_at": { + "format": "date-time", + "type": "string" } }, "required": [ - "success", - "data" + "id", + "name", + "description", + "type", + "condition", + "track_membership", + "member_count", + "created_at", + "updated_at" ], - "description": "Single email with its events." + "type": "object" }, - "ListSubscribeResponse": { - "type": "object", + "SegmentV1Create": { + "description": "Body for POST /api/v1/segments. `condition` is required when `type` is `DYNAMIC`.", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "condition": { + "additionalProperties": {}, + "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND.", + "type": "object" }, - "data": { - "type": "object", - "properties": { - "membershipId": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "CONFIRMED", - "UNSUBSCRIBED" - ] - }, - "created": { - "type": "boolean", - "description": "True when the membership row did not exist before this call." - }, - "previousStatus": { - "type": [ - "string", - "null" - ], - "enum": [ - "PENDING", - "CONFIRMED", - "UNSUBSCRIBED", - null - ], - "description": "Status the membership held before this call; null when it did not exist. Use this rather than `created` to describe the transition to the user." - }, - "confirmToken": { - "type": "string", - "description": "Present only when the list has doubleOptIn enabled. Sendly does not send the confirmation email — deliver /api/lists/confirm?token= to the contact. Valid for 24 hours." - } - }, - "required": [ - "membershipId", - "status", - "created", - "previousStatus" - ] + "description": { + "maxLength": 500, + "type": "string" + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "track_membership": { + "default": false, + "description": "Emit segment entry/exit events as contacts move in and out. Off by default — it costs a membership write per transition.", + "type": "boolean" + }, + "type": { + "default": "DYNAMIC", + "enum": [ + "DYNAMIC", + "STATIC" + ], + "type": "string" } }, "required": [ - "success", - "data" + "name" ], - "description": "Result of a list-subscribe call." + "type": "object" }, - "ListUnsubscribeResponse": { - "type": "object", + "SegmentV1Deleted": { + "description": "Acknowledgement that a segment was deleted.", "properties": { - "success": { - "type": "boolean", + "deleted": { "enum": [ true - ] + ], + "type": "boolean" }, - "data": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email" - } - }, - "required": [ - "email" - ] + "id": { + "format": "uuid", + "type": "string" } }, "required": [ - "success", - "data" + "id", + "deleted" ], - "description": "Echoes the address that was unsubscribed." + "type": "object" }, - "CampaignV1List": { - "type": "object", + "SegmentV1List": { + "description": "Cursor-paginated list of segments.", "properties": { "data": { - "type": "array", "items": { - "$ref": "#/components/schemas/CampaignV1" - } + "$ref": "#/components/schemas/SegmentV1" + }, + "type": "array" }, "has_more": { "type": "boolean" }, "next_cursor": { + "description": "Pass as `after` to fetch the next page. `null` on the last page.", "type": [ "string", "null" - ], - "description": "Pass as `after` to fetch the next page. `null` on the last page." + ] } }, "required": [ @@ -1733,360 +1762,431 @@ "has_more", "next_cursor" ], - "description": "Cursor-paginated list of campaigns." + "type": "object" }, - "CampaignV1": { - "type": "object", + "SegmentV1Update": { + "description": "Body for PATCH /api/v1/segments/{id}. `type` is deliberately absent — it is fixed at creation. `condition` is ignored on a STATIC segment.", "properties": { - "id": { - "type": "string", - "format": "uuid" + "condition": { + "additionalProperties": {}, + "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND.", + "type": "object" }, - "name": { + "description": { + "maxLength": 500, "type": "string" }, - "status": { - "type": "string", - "enum": [ - "DRAFT", - "SCHEDULED", - "SENDING", - "PAUSED", - "SENT", - "CANCELLED" - ] - }, - "subject": { + "name": { + "maxLength": 100, + "minLength": 1, "type": "string" }, - "audience_type": { - "type": "string", - "enum": [ - "ALL", - "FILTERED", - "SEGMENT" - ] + "track_membership": { + "type": "boolean" + } + }, + "type": "object" + }, + "SendEmail": { + "description": "Body for POST /api/emails — send a single transactional email. Either `template` or `subject`+`body` is required.", + "properties": { + "attachments": { + "items": { + "properties": { + "content": { + "minLength": 1, + "type": "string" + }, + "contentId": { + "maxLength": 255, + "minLength": 1, + "pattern": "^[^<>\\r\\n]+$", + "type": "string" + }, + "contentType": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "disposition": { + "default": "attachment", + "enum": [ + "attachment", + "inline" + ], + "type": "string" + }, + "filename": { + "maxLength": 255, + "minLength": 1, + "pattern": "^[^\\r\\n\"]+$", + "type": "string" + } + }, + "required": [ + "filename", + "content", + "contentType" + ], + "type": "object" + }, + "maxItems": 10, + "type": "array" }, - "scheduled_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" + "bcc": { + "items": { + "format": "email", + "type": "string" + }, + "type": "array" }, - "sent_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" + "body": { + "minLength": 1, + "type": "string" }, - "created_at": { - "type": "string", - "format": "date-time" + "cc": { + "items": { + "format": "email", + "type": "string" + }, + "type": "array" }, - "stats": { - "type": "object", - "properties": { - "total_recipients": { - "type": "integer" - }, - "sent": { - "type": "integer" - }, - "delivered": { - "type": "integer" - }, - "opened": { - "type": "integer" - }, - "clicked": { - "type": "integer" + "data": { + "additionalProperties": {}, + "description": "Arbitrary JSON value (string, number, boolean, null, array, or object).", + "type": "object" + }, + "from": { + "anyOf": [ + { + "format": "email", + "type": "string" }, - "bounced": { - "type": "integer" + { + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "name": { + "pattern": "^[^\\r\\n]*$", + "type": "string" + } + }, + "required": [ + "email" + ], + "type": "object" } - }, - "required": [ - "total_recipients", - "sent", - "delivered", - "opened", - "clicked", - "bounced" ] - } - }, - "required": [ - "id", - "name", - "status", - "subject", - "audience_type", - "scheduled_at", - "sent_at", - "created_at", - "stats" - ], - "description": "A campaign as exposed on the v1 API." - }, - "CampaignV1Create": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200 - }, - "description": { - "type": "string", - "maxLength": 500 }, - "subject": { - "type": "string", - "minLength": 1 + "headers": { + "additionalProperties": { + "maxLength": 998, + "pattern": "^[^\\r\\n]*$", + "type": "string" + }, + "type": "object" }, - "body": { - "type": "string", - "minLength": 1 + "name": { + "type": "string" }, - "from": { - "type": "string", + "reply": { "format": "email", - "description": "Sender address. Its domain must be verified for this project." - }, - "from_name": { - "type": [ - "string", - "null" - ], - "maxLength": 100 + "type": "string" }, - "reply_to": { - "type": [ - "string", - "null" - ], - "format": "email" + "subject": { + "maxLength": 998, + "minLength": 1, + "pattern": "^[^\\r\\n]*$", + "type": "string" }, - "type": { - "type": "string", - "enum": [ - "TRANSACTIONAL", - "MARKETING", - "HEADLESS" - ], - "default": "MARKETING" + "subscribed": { + "type": "boolean" }, - "audience_type": { - "type": "string", - "enum": [ - "ALL", - "FILTERED", - "SEGMENT" - ], - "description": "`ALL` — every subscribed contact. `FILTERED` — the contacts matching `audience_condition`. `SEGMENT` — the members of `segment_id`." + "tags": { + "items": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9_-]+$", + "type": "string" + }, + "maxItems": 10, + "type": "array" }, - "audience_condition": { - "type": "object", - "additionalProperties": {}, - "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND." + "template": { + "format": "uuid", + "type": "string" }, - "segment_id": { - "type": "string", - "format": "uuid" + "to": { + "anyOf": [ + { + "format": "email", + "type": "string" + }, + { + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + { + "items": { + "anyOf": [ + { + "format": "email", + "type": "string" + }, + { + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "email" + ], + "type": "object" + } + ] + }, + "type": "array" + } + ] } }, "required": [ - "name", - "subject", - "body", - "from", - "audience_type" + "to" ], - "description": "Body for POST /api/v1/campaigns. `segment_id` is required when `audience_type` is `SEGMENT`, and `audience_condition` is required when it is `FILTERED`." + "type": "object" }, - "CampaignV1Update": { - "type": "object", + "SendEmailData": { + "description": "Result of a send (`executeSendEmail`): one `emails` entry per recipient — a single request with an array `to` fans out to several — plus the send `timestamp`.", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200 - }, - "description": { - "type": "string", - "maxLength": 500 - }, - "subject": { - "type": "string", - "minLength": 1 - }, - "body": { - "type": "string", - "minLength": 1 - }, - "from": { - "type": "string", - "format": "email", - "description": "Sender address. Its domain must be verified for this project." - }, - "from_name": { - "type": [ - "string", - "null" - ], - "maxLength": 100 + "emails": { + "items": { + "$ref": "#/components/schemas/SendEmailRecipientResult" + }, + "type": "array" }, - "reply_to": { - "type": [ - "string", - "null" + "timestamp": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "emails", + "timestamp" + ], + "type": "object" + }, + "SendEmailRecipientResult": { + "description": "Per-recipient result: the upserted `contact` (id + email) and `email` — the id of the queued email record for that recipient.", + "properties": { + "contact": { + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "id", + "email" ], - "format": "email" - }, - "type": { - "type": "string", - "enum": [ - "TRANSACTIONAL", - "MARKETING", - "HEADLESS" - ] - }, - "audience_type": { - "type": "string", - "enum": [ - "ALL", - "FILTERED", - "SEGMENT" - ] - }, - "audience_condition": { - "type": "object", - "additionalProperties": {}, - "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND." + "type": "object" }, - "segment_id": { - "type": "string", - "format": "uuid" + "email": { + "format": "uuid", + "type": "string" } }, - "description": "Body for PATCH /api/v1/campaigns/{id}. All fields optional." + "required": [ + "contact", + "email" + ], + "type": "object" }, - "CampaignV1Deleted": { - "type": "object", + "SendEmailResponse": { + "description": "Successful response for `POST /api/emails`. `data.emails[i].email` is the queued email id for recipient `i`; poll `GET /api/emails/{id}` for its delivery status.", "properties": { - "id": { - "type": "string", - "format": "uuid" + "data": { + "$ref": "#/components/schemas/SendEmailData" }, - "deleted": { - "type": "boolean", + "success": { "enum": [ true - ] + ], + "type": "boolean" } }, "required": [ - "id", - "deleted" + "success", + "data" ], - "description": "Acknowledgement that a campaign was deleted." + "type": "object" }, - "CampaignV1Send": { - "type": "object", + "SuccessEmpty": { + "description": "Bare success envelope with no payload.", "properties": { - "scheduled_for": { - "type": "string", - "format": "date-time", - "description": "RFC 3339 timestamp, strictly in the future. Omit to start sending immediately." + "success": { + "enum": [ + true + ], + "type": "boolean" } }, - "description": "Body for POST /api/v1/campaigns/{id}/send." + "required": [ + "success" + ], + "type": "object" }, - "CampaignV1Stats": { - "type": "object", + "Suppression": { + "description": "A single suppressed-email record.", "properties": { - "total_recipients": { - "type": "integer" - }, - "sent": { - "type": "integer" + "createdAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" }, - "delivered": { - "type": "integer" + "email": { + "format": "email", + "type": "string" }, - "opened": { - "type": "integer" + "id": { + "format": "uuid", + "type": "string" }, - "clicked": { - "type": "integer" + "projectId": { + "format": "uuid", + "type": "string" }, - "bounced": { - "type": "integer" + "reason": { + "enum": [ + "HARD_BOUNCE", + "COMPLAINT", + "MANUAL", + "UNSUBSCRIBE" + ], + "type": "string" }, - "open_rate": { - "type": "number" + "source": { + "enum": [ + "SES_WEBHOOK", + "API", + "DASHBOARD" + ], + "type": "string" + } + }, + "required": [ + "id", + "projectId", + "email", + "reason", + "source", + "createdAt" + ], + "type": "object" + }, + "SuppressionCheckResponse": { + "description": "Result of GET /api/suppression/{email} — whether the address is suppressed.", + "properties": { + "createdAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" }, - "click_rate": { - "type": "number" + "reason": { + "enum": [ + "HARD_BOUNCE", + "COMPLAINT", + "MANUAL", + "UNSUBSCRIBE" + ], + "type": "string" }, - "bounce_rate": { - "type": "number" + "source": { + "enum": [ + "SES_WEBHOOK", + "API", + "DASHBOARD" + ], + "type": "string" }, - "delivery_rate": { - "type": "number" + "suppressed": { + "type": "boolean" } }, "required": [ - "total_recipients", - "sent", - "delivered", - "opened", - "clicked", - "bounced", - "open_rate", - "click_rate", - "bounce_rate", - "delivery_rate" + "suppressed" ], - "description": "Materialized delivery and engagement counters for one campaign." + "type": "object" }, - "SegmentV1List": { - "type": "object", + "SuppressionListResponse": { + "description": "Cursor-paginated list of suppressions.", "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, "data": { - "type": "array", "items": { - "$ref": "#/components/schemas/SegmentV1" - } + "$ref": "#/components/schemas/Suppression" + }, + "type": "array" }, - "has_more": { + "hasMore": { "type": "boolean" }, - "next_cursor": { + "nextCursor": { "type": [ "string", "null" + ] + }, + "success": { + "enum": [ + true ], - "description": "Pass as `after` to fetch the next page. `null` on the last page." + "type": "boolean" } }, "required": [ - "data", - "has_more", - "next_cursor" + "success", + "data" ], - "description": "Cursor-paginated list of segments." + "type": "object" }, - "SegmentV1": { - "type": "object", + "Template": { + "description": "A reusable email template.", "properties": { - "id": { - "type": "string", - "format": "uuid" + "body": { + "type": "string" }, - "name": { + "createdAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", "type": "string" }, "description": { @@ -2095,874 +2195,916 @@ "null" ] }, - "type": { - "type": "string", - "enum": [ - "DYNAMIC", - "STATIC" + "from": { + "format": "email", + "type": "string" + }, + "fromName": { + "type": [ + "string", + "null" ] }, - "condition": { - "type": "object", - "additionalProperties": {}, - "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND." + "id": { + "format": "uuid", + "type": "string" }, - "track_membership": { - "type": "boolean" + "name": { + "type": "string" }, - "member_count": { - "type": "integer" + "projectId": { + "format": "uuid", + "type": "string" }, - "created_at": { - "type": "string", - "format": "date-time" + "replyTo": { + "format": "email", + "type": [ + "string", + "null" + ] }, - "updated_at": { - "type": "string", - "format": "date-time" + "subject": { + "type": "string" + }, + "type": { + "enum": [ + "MARKETING", + "TRANSACTIONAL", + "HEADLESS" + ], + "type": "string" + }, + "updatedAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" } }, "required": [ "id", + "projectId", "name", - "description", + "subject", + "body", + "from", "type", - "condition", - "track_membership", - "member_count", - "created_at", - "updated_at" + "createdAt", + "updatedAt" ], - "description": "A segment as exposed on the v1 API." + "type": "object" }, - "SegmentV1Create": { - "type": "object", + "TemplateListResponse": { + "description": "Cursor-paginated list of templates.", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 500 + "data": { + "properties": { + "cursor": { + "description": "Cursor for the next page; omitted on the last page.", + "type": "string" + }, + "data": { + "items": { + "$ref": "#/components/schemas/Template" + }, + "type": "array" + }, + "hasMore": { + "type": "boolean" + }, + "total": { + "type": "integer" + } + }, + "required": [ + "data", + "total", + "hasMore" + ], + "type": "object" }, - "type": { - "type": "string", + "success": { "enum": [ - "DYNAMIC", - "STATIC" + true ], - "default": "DYNAMIC" - }, - "condition": { - "type": "object", - "additionalProperties": {}, - "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND." - }, - "track_membership": { - "type": "boolean", - "default": false, - "description": "Emit segment entry/exit events as contacts move in and out. Off by default — it costs a membership write per transition." + "type": "boolean" } }, "required": [ - "name" + "success", + "data" ], - "description": "Body for POST /api/v1/segments. `condition` is required when `type` is `DYNAMIC`." + "type": "object" }, - "SegmentV1Update": { - "type": "object", + "TrackEvent": { + "description": "Body for POST /api/track — record a custom event for a contact.", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 + "data": { + "additionalProperties": {}, + "description": "Arbitrary JSON value (string, number, boolean, null, array, or object).", + "type": "object" }, - "description": { - "type": "string", - "maxLength": 500 + "email": { + "format": "email", + "type": "string" }, - "condition": { - "type": "object", - "additionalProperties": {}, - "description": "Filter condition: `{ logic: \"AND\" | \"OR\", groups: [{ filters: [{ field, operator, value?, unit? }], conditions?: }] }`. `field` addresses a contact column or a `customFields.` path; `operator` is one of the segment operators (equals, notEquals, contains, greaterThan, lessThan, within, exists, …). Groups combine with `logic`; filters inside one group always combine with AND." + "event": { + "minLength": 1, + "type": "string" }, - "track_membership": { + "subscribed": { "type": "boolean" } }, - "description": "Body for PATCH /api/v1/segments/{id}. `type` is deliberately absent — it is fixed at creation. `condition` is ignored on a STATIC segment." - }, - "SegmentV1Deleted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] - } - }, "required": [ - "id", - "deleted" + "event", + "email" ], - "description": "Acknowledgement that a segment was deleted." + "type": "object" }, - "SegmentContactV1List": { - "type": "object", + "TrackEventResponse": { + "description": "Response from POST /api/track.", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SegmentContactV1" - } - }, - "has_more": { - "type": "boolean" + "properties": { + "contact": { + "format": "uuid", + "type": "string" + }, + "event": { + "format": "uuid", + "type": "string" + }, + "timestamp": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "contact", + "event", + "timestamp" + ], + "type": "object" }, - "next_cursor": { - "type": [ - "string", - "null" + "success": { + "enum": [ + true ], - "description": "Pass as `after` to fetch the next page. `null` on the last page." + "type": "boolean" } }, "required": [ - "data", - "has_more", - "next_cursor" + "success", + "data" ], - "description": "Cursor-paginated list of the contacts belonging to a segment." + "type": "object" }, - "SegmentContactV1": { - "type": "object", + "UpdateContactBody": { + "description": "Body for PATCH /api/contacts/{id}. `email` is immutable here — use upsert to change addresses.", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "email": { - "type": "string" + "customFields": { + "additionalProperties": {}, + "type": "object" }, "subscribed": { "type": "boolean" - }, - "custom_fields": { - "type": "object", - "additionalProperties": {}, - "description": "Arbitrary JSON value (string, number, boolean, null, array, or object)." - }, - "created_at": { - "type": "string", - "format": "date-time" } }, - "required": [ - "id", - "email", - "subscribed", - "custom_fields", - "created_at" - ], - "description": "A contact belonging to a segment." + "type": "object" }, - "WorkflowV1List": { - "type": "object", + "UpdateTemplate": { + "description": "Body for PATCH /api/templates/{id}.", "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkflowV1" - } + "body": { + "minLength": 1, + "type": "string" }, - "has_more": { - "type": "boolean" + "description": { + "maxLength": 500, + "type": "string" }, - "next_cursor": { + "from": { + "format": "email", + "type": "string" + }, + "fromName": { + "maxLength": 100, "type": [ "string", "null" - ], - "description": "Pass as `after` to fetch the next page. `null` on the last page." - } - }, - "required": [ - "data", - "has_more", - "next_cursor" - ], - "description": "Cursor-paginated list of workflows." - }, - "WorkflowV1": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" + ] }, "name": { + "maxLength": 100, + "minLength": 1, "type": "string" }, - "description": { + "replyTo": { + "format": "email", "type": [ "string", "null" ] }, - "enabled": { - "type": "boolean" + "subject": { + "minLength": 1, + "type": "string" }, - "trigger_type": { - "type": "string", + "type": { "enum": [ - "EVENT", - "MANUAL", - "SCHEDULE" - ] - }, - "event_name": { - "type": [ - "string", - "null" + "TRANSACTIONAL", + "MARKETING", + "HEADLESS" ], - "description": "Trigger event for `EVENT` workflows; null for the other trigger types." - }, - "allow_reentry": { - "type": "boolean" - }, - "max_executions_per_hour": { - "type": [ - "integer", - "null" - ] - }, - "version": { - "type": "integer", - "description": "Incremented on every structural (step/transition) change." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" + "type": "string" } }, - "required": [ - "id", - "name", - "description", - "enabled", - "trigger_type", - "event_name", - "allow_reentry", - "max_executions_per_hour", - "version", - "created_at", - "updated_at" - ], - "description": "An automation workflow as exposed on the v1 API." + "type": "object" }, - "WorkflowCreateV1": { - "type": "object", + "UpdateWebhook": { + "description": "Body for PATCH /api/webhooks/{id}.", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200 - }, - "description": { - "type": "string", - "maxLength": 1000 - }, - "event_name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "The custom event that starts this workflow, e.g. `user.signup`." + "eventTypes": { + "items": { + "enum": [ + "email.sent", + "email.delivered", + "email.opened", + "email.clicked", + "email.bounced", + "email.complained", + "email.failed", + "contact.created", + "contact.unsubscribed", + "contacts.bulk_created" + ], + "type": "string" + }, + "minItems": 1, + "type": "array" }, - "enabled": { - "type": "boolean", - "description": "Workflows are created disabled. A workflow can only be enabled once every step is configured." + "status": { + "enum": [ + "ACTIVE", + "PAUSED", + "DISABLED" + ], + "type": "string" }, - "allow_reentry": { - "type": "boolean" + "url": { + "format": "uri", + "type": "string" } }, - "required": [ - "name", - "event_name" - ], - "description": "Body for POST /api/v1/workflows." + "type": "object" }, - "WorkflowUpdateV1": { - "type": "object", + "UsageV1": { + "description": "Current email usage against the limits that are actually enforced.", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200 - }, - "description": { - "type": "string", - "maxLength": 1000 - }, - "event_name": { - "type": "string", - "minLength": 1, - "maxLength": 200 - }, - "enabled": { - "type": "boolean" + "daily": { + "properties": { + "emails_sent": { + "description": "Today's sends. Null when the counter could not be read.", + "type": [ + "integer", + "null" + ] + }, + "limit": { + "type": "integer" + }, + "trust_tier": { + "enum": [ + "NEW", + "ESTABLISHED", + "TRUSTED" + ], + "type": "string" + } + }, + "required": [ + "emails_sent", + "limit", + "trust_tier" + ], + "type": "object" }, - "allow_reentry": { - "type": "boolean" + "monthly": { + "properties": { + "categories": { + "properties": { + "campaign": { + "properties": { + "emails_sent": { + "type": "integer" + }, + "limit": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "emails_sent", + "limit" + ], + "type": "object" + }, + "inbound": { + "properties": { + "emails_sent": { + "type": "integer" + }, + "limit": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "emails_sent", + "limit" + ], + "type": "object" + }, + "transactional": { + "properties": { + "emails_sent": { + "type": "integer" + }, + "limit": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "emails_sent", + "limit" + ], + "type": "object" + }, + "workflow": { + "properties": { + "emails_sent": { + "type": "integer" + }, + "limit": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "emails_sent", + "limit" + ], + "type": "object" + } + }, + "required": [ + "transactional", + "campaign", + "workflow", + "inbound" + ], + "type": "object" + }, + "emails_sent": { + "type": "integer" + }, + "limit": { + "description": "Monthly cap on the total. Null when per-category limits govern instead.", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "emails_sent", + "limit", + "categories" + ], + "type": "object" }, - "max_executions_per_hour": { - "type": [ - "integer", - "null" + "plan": { + "description": "`custom` when an operator set per-category limits, `pro` on an active subscription or store entitlement, else `free`.", + "enum": [ + "free", + "pro", + "custom" ], - "exclusiveMinimum": 0, - "description": "Per-workflow start rate cap. `null` removes the cap." + "type": "string" } }, - "description": "Body for PATCH /api/v1/workflows/{id}. Every field is optional; omitted fields are left unchanged. Changing the trigger while executions are running answers 409." + "required": [ + "plan", + "monthly", + "daily" + ], + "type": "object" }, - "WorkflowDeletedV1": { - "type": "object", + "VerifyEmail": { + "description": "Body for POST /api/verify — validate email syntax, MX, disposable, etc.", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] + "email": { + "format": "email", + "type": "string" } }, "required": [ - "id", - "deleted" + "email" ], - "description": "Confirmation that a workflow was deleted." + "type": "object" }, - "WorkflowExecutionV1List": { - "type": "object", + "VerifyEmailResponse": { + "description": "Response from POST /api/verify — outcome of the syntax/MX/disposable check.", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkflowExecutionV1" - } - }, - "has_more": { - "type": "boolean" + "additionalProperties": {}, + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "reason": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + }, + "required": [ + "email", + "valid" + ], + "type": "object" }, - "next_cursor": { - "type": [ - "string", - "null" + "success": { + "enum": [ + true ], - "description": "Pass as `after` to fetch the next page. `null` on the last page." + "type": "boolean" } }, "required": [ - "data", - "has_more", - "next_cursor" + "success", + "data" ], - "description": "Cursor-paginated list of workflow executions, newest first." + "type": "object" }, - "WorkflowExecutionV1": { - "type": "object", + "Webhook": { + "description": "A user-managed outbound webhook.", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "workflow_id": { - "type": "string", - "format": "uuid" - }, - "contact_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "RUNNING", - "WAITING", - "COMPLETED", - "EXITED", - "FAILED", - "CANCELLED" - ] + "consecutiveFailures": { + "type": "integer" }, - "current_step_id": { - "type": [ - "string", - "null" - ], - "format": "uuid" + "createdAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" }, - "exit_reason": { + "disabledAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", "type": [ "string", "null" ] }, - "started_at": { - "type": "string", - "format": "date-time" - }, - "completed_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - } - }, - "required": [ - "id", - "workflow_id", - "contact_id", - "status", - "current_step_id", - "exit_reason", - "started_at", - "completed_at" - ], - "description": "One contact's run through a workflow." - }, - "WorkflowExecutionStartV1": { - "type": "object", - "properties": { - "contact_id": { - "type": "string", + "eventTypes": { + "items": { + "enum": [ + "email.sent", + "email.delivered", + "email.opened", + "email.clicked", + "email.bounced", + "email.complained", + "email.failed", + "contact.created", + "contact.unsubscribed", + "contacts.bulk_created" + ], + "type": "string" + }, + "type": "array" + }, + "id": { "format": "uuid", - "description": "Contact to enter the workflow. Must belong to this project." + "type": "string" }, - "context": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {}, - "description": "Arbitrary JSON value (string, number, boolean, null, array, or object)." - }, - "description": "Extra variables merged into the contact's data for this run." + "lastFour": { + "type": "string" + }, + "projectId": { + "format": "uuid", + "type": "string" + }, + "status": { + "enum": [ + "ACTIVE", + "PAUSED", + "DISABLED" + ], + "type": "string" + }, + "updatedAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" } }, "required": [ - "contact_id" + "id", + "projectId", + "url", + "eventTypes", + "status", + "consecutiveFailures", + "createdAt", + "updatedAt" ], - "description": "Body for POST /api/v1/workflows/{id}/executions." + "type": "object" }, - "WorkflowStatsV1": { - "type": "object", + "WebhookCall": { + "description": "An attempted webhook delivery.", "properties": { - "workflow_id": { - "type": "string", - "format": "uuid" - }, - "total": { + "attempt": { "type": "integer" }, - "by_status": { - "type": "object", - "additionalProperties": { - "type": "integer" - }, - "description": "Execution counts keyed by status; a status with no executions is absent." + "createdAt": { + "description": "ISO 8601 datetime string", + "format": "date-time", + "type": "string" }, - "completion_rate": { + "eventType": { + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "payload": { + "additionalProperties": {}, + "type": "object" + }, + "responseBody": { "type": [ - "number", + "string", + "null" + ] + }, + "responseStatus": { + "type": [ + "integer", "null" + ] + }, + "status": { + "enum": [ + "PENDING", + "SUCCESS", + "FAILED" ], - "description": "Completed ÷ finished executions (0–1). Null until at least one execution has finished." + "type": "string" }, - "avg_duration_ms": { + "webhookId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "id", + "webhookId", + "eventType", + "payload", + "status", + "attempt", + "createdAt" + ], + "type": "object" + }, + "WebhookCallsListResponse": { + "description": "Cursor-paginated list of recent calls for a single webhook.", + "properties": { + "cursor": { "type": [ - "number", + "string", "null" ] }, - "emails": { - "type": "object", - "properties": { - "sent": { - "type": "integer" - }, - "opened": { - "type": "integer" - }, - "clicked": { - "type": "integer" - } + "data": { + "items": { + "$ref": "#/components/schemas/WebhookCall" }, - "required": [ - "sent", - "opened", - "clicked" + "type": "array" + }, + "hasMore": { + "type": "boolean" + }, + "nextCursor": { + "type": [ + "string", + "null" ] }, - "conversions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "goal_id": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "event_name": { - "type": "string" - }, - "count": { - "type": "integer" - } - }, - "required": [ - "goal_id", - "name", - "event_name", - "count" - ] - } + "success": { + "enum": [ + true + ], + "type": "boolean" } }, "required": [ - "workflow_id", - "total", - "by_status", - "completion_rate", - "avg_duration_ms", - "emails", - "conversions" + "success", + "data" ], - "description": "Execution, email and conversion totals for one workflow." + "type": "object" }, - "ListSubscribe": { - "type": "object", + "WebhookCreateResponse": { + "description": "Result of POST /api/webhooks. `secret` is the only time the plaintext is returned — store it securely.", "properties": { - "email": { - "type": "string", - "format": "email" - }, "data": { - "type": "object", - "additionalProperties": {}, - "description": "Custom fields to upsert onto the contact as part of subscribing." + "allOf": [ + { + "$ref": "#/components/schemas/Webhook" + }, + { + "properties": { + "secret": { + "description": "Plaintext shared secret. Returned ONCE on create.", + "type": "string" + } + }, + "required": [ + "secret" + ], + "type": "object" + } + ], + "description": "A user-managed outbound webhook." }, - "allowResubscribe": { - "type": "boolean", - "default": false, - "description": "Permission to reverse an earlier opt-out. When the email already has an UNSUBSCRIBED membership on this list, the call fails with 409 RESUBSCRIBE_CONFIRMATION_REQUIRED unless this is `true`. Send `true` only when the contact is acting for themselves — a public subscribe form they submitted is affirmative consent — never for an operator-initiated add." + "success": { + "enum": [ + true + ], + "type": "boolean" } }, "required": [ - "email" + "success", + "data" ], - "description": "Body for POST /api/lists/{id}/subscribe." + "type": "object" }, - "ListUnsubscribe": { - "type": "object", + "WebhookGetResponse": { + "description": "Single webhook (no secret).", "properties": { - "email": { - "type": "string", - "format": "email" + "data": { + "$ref": "#/components/schemas/Webhook" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" } }, "required": [ - "email" + "success", + "data" ], - "description": "Body for POST /api/lists/{id}/unsubscribe." + "type": "object" }, - "AddDomainBody": { - "type": "object", + "WebhookListResponse": { + "description": "List of webhooks for the auth'd project.", "properties": { - "projectId": { - "type": "string", - "format": "uuid" - }, - "domain": { - "type": "string", - "minLength": 3, - "maxLength": 253 + "data": { + "items": { + "$ref": "#/components/schemas/Webhook" + }, + "type": "array" }, - "region": { - "type": "string", + "success": { "enum": [ - "us-east-1", - "us-west-2", - "eu-west-1" + true ], - "description": "Override SES region for this domain. Defaults to the project region or the env default. Required to match an existing project region." + "type": "boolean" } }, "required": [ - "domain" + "success", + "data" ], - "description": "Body for POST /api/domains. `projectId` is optional for API-key auth (derived from key) and required for session auth. `region` pins the SES region; on the first domain it locks the project, after that it must match the project's region." + "type": "object" }, - "CreateTemplate": { - "type": "object", + "WebhookRotateSecretResponse": { + "description": "Response from POST /api/webhooks/{id}/rotate-secret — returns the new plaintext once.", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 500 - }, - "subject": { - "type": "string", - "minLength": 1 - }, - "body": { - "type": "string", - "minLength": 1 - }, - "from": { - "type": "string", - "format": "email" - }, - "fromName": { - "type": [ - "string", - "null" - ], - "maxLength": 100 - }, - "replyTo": { - "type": [ - "string", - "null" + "data": { + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "secret": { + "description": "New plaintext shared secret.", + "type": "string" + } + }, + "required": [ + "id", + "secret" ], - "format": "email" + "type": "object" }, - "type": { - "type": "string", + "success": { "enum": [ - "TRANSACTIONAL", - "MARKETING", - "HEADLESS" + true ], - "default": "MARKETING" + "type": "boolean" } }, "required": [ - "name", - "subject", - "body", - "from" + "success", + "data" ], - "description": "Body for POST /api/templates." + "type": "object" }, - "UpdateTemplate": { - "type": "object", + "WorkflowCreateV1": { + "description": "Body for POST /api/v1/workflows.", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 + "allow_reentry": { + "type": "boolean" }, "description": { - "type": "string", - "maxLength": 500 - }, - "subject": { - "type": "string", - "minLength": 1 - }, - "body": { - "type": "string", - "minLength": 1 - }, - "from": { - "type": "string", - "format": "email" + "maxLength": 1000, + "type": "string" }, - "fromName": { - "type": [ - "string", - "null" - ], - "maxLength": 100 + "enabled": { + "description": "Workflows are created disabled. A workflow can only be enabled once every step is configured.", + "type": "boolean" }, - "replyTo": { - "type": [ - "string", - "null" - ], - "format": "email" + "event_name": { + "description": "The custom event that starts this workflow, e.g. `user.signup`.", + "maxLength": 200, + "minLength": 1, + "type": "string" }, - "type": { - "type": "string", - "enum": [ - "TRANSACTIONAL", - "MARKETING", - "HEADLESS" - ] + "name": { + "maxLength": 200, + "minLength": 1, + "type": "string" } }, - "description": "Body for PATCH /api/templates/{id}." + "required": [ + "name", + "event_name" + ], + "type": "object" }, - "CreateWebhook": { - "type": "object", + "WorkflowDeletedV1": { + "description": "Confirmation that a workflow was deleted.", "properties": { - "url": { - "type": "string", - "format": "uri" + "deleted": { + "enum": [ + true + ], + "type": "boolean" }, - "eventTypes": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email.sent", - "email.delivered", - "email.opened", - "email.clicked", - "email.bounced", - "email.complained", - "email.failed", - "contact.created", - "contact.unsubscribed", - "contacts.bulk_created" - ] - }, - "minItems": 1 + "id": { + "format": "uuid", + "type": "string" } }, "required": [ - "url", - "eventTypes" + "id", + "deleted" ], - "description": "Body for POST /api/webhooks — register a user webhook for one or more events." + "type": "object" }, - "UpdateWebhook": { - "type": "object", + "WorkflowExecutionStartV1": { + "description": "Body for POST /api/v1/workflows/{id}/executions.", "properties": { - "url": { - "type": "string", - "format": "uri" + "contact_id": { + "description": "Contact to enter the workflow. Must belong to this project.", + "format": "uuid", + "type": "string" }, - "eventTypes": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email.sent", - "email.delivered", - "email.opened", - "email.clicked", - "email.bounced", - "email.complained", - "email.failed", - "contact.created", - "contact.unsubscribed", - "contacts.bulk_created" + "context": { + "additionalProperties": { + "additionalProperties": {}, + "description": "Arbitrary JSON value (string, number, boolean, null, array, or object).", + "type": [ + "string", + "number", + "boolean", + "object", + "array", + "null" ] }, - "minItems": 1 - }, - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "PAUSED", - "DISABLED" - ] - } - }, - "description": "Body for PATCH /api/webhooks/{id}." - }, - "AddSuppression": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email" - }, - "reason": { - "type": "string", - "enum": [ - "HARD_BOUNCE", - "COMPLAINT", - "MANUAL", - "UNSUBSCRIBE" - ], - "default": "MANUAL" + "description": "Extra variables merged into the contact's data for this run.", + "type": "object" } }, "required": [ - "email" + "contact_id" ], - "description": "Body for POST /api/suppression — manually add an email to the suppression list." + "type": "object" }, - "TrackEvent": { - "type": "object", + "WorkflowExecutionV1": { + "description": "One contact's run through a workflow.", "properties": { - "event": { - "type": "string", - "minLength": 1 + "completed_at": { + "format": "date-time", + "type": [ + "string", + "null" + ] }, - "email": { - "type": "string", - "format": "email" + "contact_id": { + "format": "uuid", + "type": "string" }, - "subscribed": { - "type": "boolean" + "current_step_id": { + "format": "uuid", + "type": [ + "string", + "null" + ] }, - "data": { - "type": "object", - "additionalProperties": {}, - "description": "Arbitrary JSON value (string, number, boolean, null, array, or object)." + "exit_reason": { + "type": [ + "string", + "null" + ] + }, + "id": { + "format": "uuid", + "type": "string" + }, + "started_at": { + "format": "date-time", + "type": "string" + }, + "status": { + "enum": [ + "RUNNING", + "WAITING", + "COMPLETED", + "EXITED", + "FAILED", + "CANCELLED" + ], + "type": "string" + }, + "workflow_id": { + "format": "uuid", + "type": "string" } }, "required": [ - "event", - "email" + "id", + "workflow_id", + "contact_id", + "status", + "current_step_id", + "exit_reason", + "started_at", + "completed_at" ], - "description": "Body for POST /api/track — record a custom event for a contact." + "type": "object" }, - "EventV1List": { - "type": "object", + "WorkflowExecutionV1List": { + "description": "Cursor-paginated list of workflow executions, newest first.", "properties": { "data": { - "type": "array", "items": { - "$ref": "#/components/schemas/EventV1" - } + "$ref": "#/components/schemas/WorkflowExecutionV1" + }, + "type": "array" }, "has_more": { "type": "boolean" }, "next_cursor": { + "description": "Pass as `after` to fetch the next page. `null` on the last page.", "type": [ "string", "null" - ], - "description": "Pass as `after` to fetch the next page. `null` on the last page." + ] } }, "required": [ @@ -2970,560 +3112,499 @@ "has_more", "next_cursor" ], - "description": "Cursor-paginated list of events, newest first." + "type": "object" }, - "EventV1": { - "type": "object", + "WorkflowStatsV1": { + "description": "Execution, email and conversion totals for one workflow.", "properties": { - "id": { - "type": "string", - "format": "uuid" + "avg_duration_ms": { + "type": [ + "number", + "null" + ] }, - "name": { - "type": "string" + "by_status": { + "additionalProperties": { + "type": "integer" + }, + "description": "Execution counts keyed by status; a status with no executions is absent.", + "type": "object" }, - "contact_id": { + "completion_rate": { + "description": "Completed ÷ finished executions (0–1). Null until at least one execution has finished.", "type": [ - "string", + "number", "null" - ], - "format": "uuid" + ] }, - "email_id": { - "type": [ - "string", - "null" - ], - "format": "uuid" - }, - "data": { - "type": [ - "object", - "null" - ], - "additionalProperties": {}, - "description": "The payload recorded with the event, or null." - }, - "created_at": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "id", - "name", - "contact_id", - "email_id", - "data", - "created_at" - ], - "description": "A recorded custom event." - }, - "EventTrackV1": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Event name, e.g. `user.signup`." - }, - "contact_id": { - "type": "string", - "format": "uuid", - "description": "Contact the event belongs to. Must already exist in this project — unlike the legacy `POST /api/track`, this endpoint never creates contacts. Omit for a project-level event." - }, - "data": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {}, - "description": "Arbitrary JSON value (string, number, boolean, null, array, or object)." - }, - "description": "Arbitrary event payload." - } - }, - "required": [ - "name" - ], - "description": "Body for POST /api/v1/events." - }, - "EventNamesV1": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "data" - ], - "description": "Every distinct event name in the project, most frequent first." - }, - "EventStatsV1": { - "type": "object", - "properties": { - "data": { - "type": "array", + "conversions": { "items": { - "type": "object", "properties": { - "name": { - "type": "string" - }, "count": { "type": "integer" + }, + "event_name": { + "type": "string" + }, + "goal_id": { + "format": "uuid", + "type": "string" + }, + "name": { + "type": "string" } }, "required": [ + "goal_id", "name", + "event_name", "count" - ] - } - }, - "window": { - "$ref": "#/components/schemas/AnalyticsWindowV1" - } - }, - "required": [ - "data", - "window" - ], - "description": "Per-name event counts over the applied window." - }, - "AnalyticsWindowV1": { - "type": "object", - "properties": { - "from": { - "type": "string", - "format": "date-time" + ], + "type": "object" + }, + "type": "array" }, - "to": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "from", - "to" - ], - "description": "The time range this response was computed over, after the 90-day clamp." - }, - "AnalyticsTimeseriesV1": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "date": { - "type": "string", - "format": "date-time" - }, - "emails": { - "type": "integer" - }, - "delivered": { - "type": "integer" - }, - "opens": { - "type": "integer" - }, - "clicks": { - "type": "integer" - }, - "bounces": { - "type": "integer" - } + "emails": { + "properties": { + "clicked": { + "type": "integer" }, - "required": [ - "date", - "emails", - "delivered", - "opens", - "clicks", - "bounces" - ] - } + "opened": { + "type": "integer" + }, + "sent": { + "type": "integer" + } + }, + "required": [ + "sent", + "opened", + "clicked" + ], + "type": "object" }, - "window": { - "$ref": "#/components/schemas/AnalyticsWindowV1" + "total": { + "type": "integer" + }, + "workflow_id": { + "format": "uuid", + "type": "string" } }, "required": [ - "data", - "window" + "workflow_id", + "total", + "by_status", + "completion_rate", + "avg_duration_ms", + "emails", + "conversions" ], - "description": "Daily email counters across the window. Every day in range is present, zero-filled." + "type": "object" }, - "AnalyticsCampaignStatsV1": { - "type": "object", + "WorkflowUpdateV1": { + "description": "Body for PATCH /api/v1/workflows/{id}. Every field is optional; omitted fields are left unchanged. Changing the trigger while executions are running answers 409.", "properties": { - "total": { - "type": "integer" + "allow_reentry": { + "type": "boolean" }, - "active": { - "type": "integer", - "description": "Campaigns in DRAFT or SCHEDULED." + "description": { + "maxLength": 1000, + "type": "string" }, - "completed": { - "type": "integer" + "enabled": { + "type": "boolean" }, - "average_open_rate": { - "type": "number", - "description": "Percentage, one decimal place." + "event_name": { + "maxLength": 200, + "minLength": 1, + "type": "string" }, - "average_click_rate": { - "type": "number" + "max_executions_per_hour": { + "description": "Per-workflow start rate cap. `null` removes the cap.", + "exclusiveMinimum": 0, + "type": [ + "integer", + "null" + ] }, - "window": { - "$ref": "#/components/schemas/AnalyticsWindowV1" + "name": { + "maxLength": 200, + "minLength": 1, + "type": "string" } }, - "required": [ - "total", - "active", - "completed", - "average_open_rate", - "average_click_rate", - "window" - ], - "description": "Campaign counters and engagement over the window." + "type": "object" }, - "AnalyticsTopCampaignsV1": { - "type": "object", + "WorkflowV1": { + "description": "An automation workflow as exposed on the v1 API.", "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "subject": { - "type": "string" - }, - "sent": { - "type": "integer" - }, - "opened": { - "type": "integer" - }, - "clicked": { - "type": "integer" - }, - "open_rate": { - "type": "number" - }, - "click_rate": { - "type": "number" - } - }, - "required": [ - "id", - "subject", - "sent", - "opened", - "clicked", - "open_rate", - "click_rate" - ] - } + "allow_reentry": { + "type": "boolean" }, - "window": { - "$ref": "#/components/schemas/AnalyticsWindowV1" - } - }, - "required": [ - "data", - "window" - ], - "description": "Sent campaigns ranked by open rate." - }, - "UsageV1": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "enum": [ - "free", - "pro", - "custom" - ], - "description": "`custom` when an operator set per-category limits, `pro` on an active subscription or store entitlement, else `free`." + "created_at": { + "format": "date-time", + "type": "string" }, - "monthly": { - "type": "object", - "properties": { - "emails_sent": { - "type": "integer" - }, - "limit": { - "type": [ - "integer", - "null" - ], - "description": "Monthly cap on the total. Null when per-category limits govern instead." - }, - "categories": { - "type": "object", - "properties": { - "transactional": { - "type": "object", - "properties": { - "emails_sent": { - "type": "integer" - }, - "limit": { - "type": [ - "integer", - "null" - ] - } - }, - "required": [ - "emails_sent", - "limit" - ] - }, - "campaign": { - "type": "object", - "properties": { - "emails_sent": { - "type": "integer" - }, - "limit": { - "type": [ - "integer", - "null" - ] - } - }, - "required": [ - "emails_sent", - "limit" - ] - }, - "workflow": { - "type": "object", - "properties": { - "emails_sent": { - "type": "integer" - }, - "limit": { - "type": [ - "integer", - "null" - ] - } - }, - "required": [ - "emails_sent", - "limit" - ] - }, - "inbound": { - "type": "object", - "properties": { - "emails_sent": { - "type": "integer" - }, - "limit": { - "type": [ - "integer", - "null" - ] - } - }, - "required": [ - "emails_sent", - "limit" - ] - } - }, - "required": [ - "transactional", - "campaign", - "workflow", - "inbound" - ] - } - }, - "required": [ - "emails_sent", - "limit", - "categories" + "description": { + "type": [ + "string", + "null" ] }, - "daily": { - "type": "object", - "properties": { - "emails_sent": { - "type": [ - "integer", - "null" - ], - "description": "Today's sends. Null when the counter could not be read." - }, - "limit": { - "type": "integer" - }, - "trust_tier": { - "type": "string", - "enum": [ - "NEW", - "ESTABLISHED", - "TRUSTED" - ] - } - }, - "required": [ - "emails_sent", - "limit", - "trust_tier" + "enabled": { + "type": "boolean" + }, + "event_name": { + "description": "Trigger event for `EVENT` workflows; null for the other trigger types.", + "type": [ + "string", + "null" ] + }, + "id": { + "format": "uuid", + "type": "string" + }, + "max_executions_per_hour": { + "type": [ + "integer", + "null" + ] + }, + "name": { + "type": "string" + }, + "trigger_type": { + "enum": [ + "EVENT", + "MANUAL", + "SCHEDULE" + ], + "type": "string" + }, + "updated_at": { + "format": "date-time", + "type": "string" + }, + "version": { + "description": "Incremented on every structural (step/transition) change.", + "type": "integer" } }, "required": [ - "plan", - "monthly", - "daily" + "id", + "name", + "description", + "enabled", + "trigger_type", + "event_name", + "allow_reentry", + "max_executions_per_hour", + "version", + "created_at", + "updated_at" ], - "description": "Current email usage against the limits that are actually enforced." + "type": "object" }, - "VerifyEmail": { - "type": "object", + "WorkflowV1List": { + "description": "Cursor-paginated list of workflows.", "properties": { - "email": { - "type": "string", - "format": "email" + "data": { + "items": { + "$ref": "#/components/schemas/WorkflowV1" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "next_cursor": { + "description": "Pass as `after` to fetch the next page. `null` on the last page.", + "type": [ + "string", + "null" + ] } }, "required": [ - "email" + "data", + "has_more", + "next_cursor" ], - "description": "Body for POST /api/verify — validate email syntax, MX, disposable, etc." + "type": "object" + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "bearerFormat": "API Key", + "description": "API key authentication. Use a `sk_*` (FULL) or `pk_*` (SENDING_ONLY) key as the bearer token. Public keys (`pk_*`) are restricted to the email-send endpoints and the self-serve list subscribe/unsubscribe pair; every other endpoint — event tracking included — answers 403 for them. In scope terms (used by `/api/v1/*` operations): FULL keys hold every scope; SENDING_ONLY keys hold only `emails:send`.", + "scheme": "bearer", + "type": "http" + }, + "SessionAuth": { + "description": "BetterAuth session cookie. Used by the dashboard / browser clients. When present, the active project is taken from the `x-project-id` header.", + "in": "cookie", + "name": "better-auth.session_token", + "type": "apiKey" } + } + }, + "info": { + "contact": { + "name": "Sendly Support", + "url": "https://sendly.now" + }, + "description": "Sendly's public REST API. Authenticate either with a project API key as `Authorization: Bearer ` (`sk_*` for full access, `pk_*` for sending-only) or with a BetterAuth session cookie. Legacy `/api/*` endpoints return JSON envelopes of the form `{ success, data }` (success) or `{ error: { message, code } }` (failure). `/api/v1/*` endpoints return bare resource bodies on success and RFC 9457 `application/problem+json` documents on failure.", + "license": { + "name": "AGPL-3.0", + "url": "https://www.gnu.org/licenses/agpl-3.0.txt" }, - "parameters": {} + "title": "Sendly API", + "version": "1.0.0" }, + "openapi": "3.1.0", "paths": { - "/api/v1/campaigns": { + "/api/contacts": { "get": { - "operationId": "v1ListCampaigns", - "tags": [ - "Campaigns" - ], - "summary": "List campaigns", - "description": "Cursor-paginated list of campaigns, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.\n\nRequires the `campaigns:read` scope — View your campaigns and their performance.\n\nUnlike the legacy `/api/*` endpoints, v1 responses are the bare payload (no `{ success, data }` envelope) and errors are RFC 9457 problem documents.", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "SessionAuth": [] - } - ], + "description": "Cursor-paginated list of contacts. Supports filter by `search` and `subscribed`.", + "operationId": "listContacts", "parameters": [ { + "in": "query", + "name": "limit", + "required": false, "schema": { - "type": "integer", - "minimum": 1, + "default": 50, "maximum": 100, - "default": 20 - }, + "minimum": 1, + "type": "integer" + } + }, + { + "in": "query", + "name": "cursor", "required": false, - "name": "limit", - "in": "query" + "schema": { + "type": "string" + } }, { + "in": "query", + "name": "search", + "required": false, "schema": { - "type": "string", - "minLength": 1, - "description": "Opaque cursor from a previous response's `next_cursor`." - }, + "type": "string" + } + }, + { + "in": "query", + "name": "subscribed", "required": false, - "description": "Opaque cursor from a previous response's `next_cursor`.", - "name": "after", - "in": "query" + "schema": { + "enum": [ + "true", + "false" + ], + "type": "string" + } } ], "responses": { "200": { - "description": "Campaign list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1List" + "$ref": "#/components/schemas/ContactListResponse" } } - } + }, + "description": "Contact list" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Rate limit or billing limit exceeded" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } + }, + "description": "Internal server error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "SessionAuth": [] + } + ], + "summary": "List contacts", + "tags": [ + "Contacts" + ] + }, + "post": { + "description": "Create a new contact. Returns 409 on `(projectId, email)` conflict — use `/api/contacts/upsert` for create-or-update semantics.", + "operationId": "createContact", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateContact" + } } }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/Contact" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" + } + } + }, + "description": "Contact created" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unauthorized — missing or invalid auth" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Forbidden — insufficient permissions or project disabled" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Email already exists for this project" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation failed — request body or query parameters did not match the schema" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Rate limit or billing limit exceeded" + }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "post": { - "operationId": "v1CreateCampaign", - "tags": [ - "Campaigns" - ], - "summary": "Create a campaign", - "description": "Create a campaign in `DRAFT`. Creating never sends — `POST /api/v1/campaigns/{id}/send` is the only operation that puts mail on the wire — so a campaign can be built up and reviewed before it costs anything.\n\nThe `from` address is checked against this project's verified domains before the campaign is written, so a campaign never exists with a sender it cannot use.\n\n`segment_id` is required when `audience_type` is `SEGMENT`; `audience_condition` is required when it is `FILTERED`. Both answer 422 when missing.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + }, "security": [ { "ApiKeyAuth": [] @@ -3532,122 +3613,121 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request." - }, - "required": false, - "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", - "name": "Idempotency-Key", - "in": "header" - } - ], + "summary": "Create a contact", + "tags": [ + "Contacts" + ] + } + }, + "/api/contacts/bulk": { + "delete": { + "description": "Delete up to 1000 contacts in one call. Provide either `ids` or `emails`.", + "operationId": "bulkDeleteContacts", "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1Create" + "$ref": "#/components/schemas/ContactBulkDeleteBody" } } - } + }, + "required": true }, "responses": { - "201": { - "description": "Campaign created", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1" - } - } - } - }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" + "properties": { + "data": { + "properties": { + "deleted": { + "type": "integer" + } + }, + "required": [ + "deleted" + ], + "type": "object" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Bulk-delete result" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "404": { - "description": "`resource_not_found` — `segment_id` names a segment that does not belong to this project.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "409": { - "description": "`conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "422": { - "description": "`validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/campaigns/{id}": { - "get": { - "operationId": "v1GetCampaign", - "tags": [ - "Campaigns" - ], - "summary": "Retrieve a campaign", - "description": "Fetch one campaign, including its materialized delivery counters.\n\nRequires the `campaigns:read` scope — View your campaigns and their performance.", + }, "security": [ { "ApiKeyAuth": [] @@ -3656,220 +3736,257 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" - } - ], + "summary": "Bulk-delete contacts", + "tags": [ + "Contacts" + ] + }, + "post": { + "description": "Create up to 1000 contacts in one call. Per-row conflicts are reported as `skipped`.", + "operationId": "bulkCreateContacts", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContactBulkCreateBody" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "The campaign", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1" + "properties": { + "data": { + "properties": { + "created": { + "type": "integer" + }, + "errors": { + "items": { + "properties": { + "index": { + "type": "integer" + }, + "message": { + "type": "string" + } + }, + "required": [ + "index", + "message" + ], + "type": "object" + }, + "type": "array" + }, + "skipped": { + "type": "integer" + } + }, + "required": [ + "created", + "skipped", + "errors" + ], + "type": "object" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Bulk-create result" }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "404": { - "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "patch": { - "operationId": "v1UpdateCampaign", - "tags": [ - "Campaigns" - ], - "summary": "Update a campaign", - "description": "Partial update: an omitted field is left untouched. Only `DRAFT` and `SCHEDULED` campaigns are editable — editing one that is already sending would change what half its recipients receive, so it answers 400.\n\nChanging `from` re-verifies the sender domain. Changing the audience on a `DRAFT` campaign schedules a recipient recount, so `stats.total_recipients` converges shortly after the response.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + }, "security": [ { "ApiKeyAuth": [] - }, - { - "SessionAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" + }, + { + "SessionAuth": [] } ], + "summary": "Bulk-create contacts", + "tags": [ + "Contacts" + ] + } + }, + "/api/contacts/upsert": { + "post": { + "description": "Idempotent contact upsert keyed by email. Always answers 200 — the create-vs-update distinction is not signalled via status code.", + "operationId": "upsertContact", "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1Update" + "$ref": "#/components/schemas/CreateContact" } } - } + }, + "required": true }, "responses": { "200": { - "description": "The updated campaign", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1" + "properties": { + "data": { + "$ref": "#/components/schemas/Contact" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Contact created or updated" }, "400": { - "description": "`validation_error` — the campaign is not in an editable status, or the segment change is not allowed.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "404": { - "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "delete": { - "operationId": "v1DeleteCampaign", - "tags": [ - "Campaigns" - ], - "summary": "Delete a campaign", - "description": "Delete a `DRAFT` campaign. A campaign that has sent is the record of what went out and cannot be deleted (400) — cancel it instead if it is still scheduled or in flight.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + }, "security": [ { "ApiKeyAuth": [] @@ -3878,111 +3995,99 @@ "SessionAuth": [] } ], + "summary": "Create or update a contact by email", + "tags": [ + "Contacts" + ] + } + }, + "/api/contacts/{id}": { + "delete": { + "description": "Hard-delete a contact. Answers 200 with `{ success, data: { id } }` (pre-seam this was 204 No Content).", + "operationId": "deleteContact", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "string", "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" + "type": "string" + } } ], "responses": { "200": { - "description": "Campaign deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1Deleted" + "$ref": "#/components/schemas/IdResponse" } } - } + }, + "description": "Contact deleted" }, "400": { - "description": "`validation_error` — only `DRAFT` campaigns can be deleted.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "404": { - "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/campaigns/{id}/send": { - "post": { - "operationId": "v1SendCampaign", - "tags": [ - "Campaigns" - ], - "summary": "Send or schedule a campaign", - "description": "Start sending immediately, or park the campaign in `SCHEDULED` by passing `scheduled_for` (which must be in the future). The request body may be omitted entirely to send now.\n\n**Send an `Idempotency-Key`.** This is the operation that cannot be undone: a retry without one starts a second fan-out over the same audience. The key is scoped to this campaign, so the same key on a different campaign is a `422 idempotency_key_reused` rather than a replay of the first campaign's response.\n\nAnswers 400 when the campaign is not `DRAFT`/`SCHEDULED` or has no recipients, and 403 when the send would exceed the project's billing limit.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + }, "security": [ { "ApiKeyAuth": [] @@ -3991,143 +4096,111 @@ "SessionAuth": [] } ], + "summary": "Delete a contact", + "tags": [ + "Contacts" + ] + }, + "get": { + "operationId": "getContact", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" - }, - { + "required": true, "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request." - }, - "required": false, - "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", - "name": "Idempotency-Key", - "in": "header" - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CampaignV1Send" - } + "format": "uuid", + "type": "string" } } - }, + ], "responses": { "200": { - "description": "The campaign, now `SENDING` or `SCHEDULED`", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1" + "properties": { + "data": { + "$ref": "#/components/schemas/Contact" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Contact" }, "400": { - "description": "`validation_error` — the campaign has already been sent or is sending, has no recipients, or `scheduled_for` is not in the future.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "404": { - "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "409": { - "description": "`conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "422": { - "description": "`validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/campaigns/{id}/cancel": { - "post": { - "operationId": "v1CancelCampaign", - "tags": [ - "Campaigns" - ], - "summary": "Cancel a campaign", - "description": "Cancel a `SCHEDULED`, `SENDING`, or `PAUSED` campaign. Terminal — a cancelled campaign cannot be resumed or re-sent. Takes no request body.\n\nLike every action on this resource, the response is the campaign itself, so `status` tells you what the transition did without a second request.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + }, "security": [ { "ApiKeyAuth": [] @@ -4136,111 +4209,132 @@ "SessionAuth": [] } ], + "summary": "Get a contact", + "tags": [ + "Contacts" + ] + }, + "patch": { + "description": "Update `data` and/or `subscribed`. `email` is immutable here — use `/api/contacts/upsert` to change addresses.", + "operationId": "updateContact", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "string", "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateContactBody" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "The cancelled campaign", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1" + "properties": { + "data": { + "$ref": "#/components/schemas/Contact" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Updated contact" }, "400": { - "description": "`validation_error` — only `SCHEDULED`, `SENDING`, or `PAUSED` campaigns can be cancelled.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "404": { - "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/campaigns/{id}/pause": { - "post": { - "operationId": "v1PauseCampaign", - "tags": [ - "Campaigns" - ], - "summary": "Pause a sending campaign", - "description": "Pause a `SENDING` campaign. The workers check the flag between batches, so a small number of already-queued emails may still be delivered after this returns. Takes no request body.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + }, "security": [ { "ApiKeyAuth": [] @@ -4249,111 +4343,78 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" - } - ], + "summary": "Update a contact", + "tags": [ + "Contacts" + ] + } + }, + "/api/domains": { + "get": { + "description": "List all domains for the authenticated project.", + "operationId": "listDomains", "responses": { "200": { - "description": "The paused campaign", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1" + "$ref": "#/components/schemas/DomainListResponse" } } - } + }, + "description": "Domain list" }, "400": { - "description": "`validation_error` — only a `SENDING` campaign can be paused.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "404": { - "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/campaigns/{id}/resume": { - "post": { - "operationId": "v1ResumeCampaign", - "tags": [ - "Campaigns" - ], - "summary": "Resume a paused campaign", - "description": "Resume a `PAUSED` campaign from the last entry in its per-contact send ledger. That ledger also dedupes, so a contact already sent to is skipped rather than mailed twice. Takes no request body.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + }, "security": [ { "ApiKeyAuth": [] @@ -4362,111 +4423,101 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "The resumed campaign", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CampaignV1" - } - } - } - }, - "400": { - "description": "`validation_error` — only a `PAUSED` campaign can be resumed.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } + "summary": "List sending domains", + "tags": [ + "Domains" + ] + }, + "post": { + "description": "Register a new domain with SES and persist its DKIM tokens.", + "operationId": "addDomain", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddDomainBody" } } }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "required": true + }, + "responses": { + "201": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "properties": { + "data": { + "$ref": "#/components/schemas/Domain" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Domain added" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "404": { - "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/campaigns/{id}/stats": { - "get": { - "operationId": "v1GetCampaignStats", - "tags": [ - "Campaigns" - ], - "summary": "Retrieve campaign statistics", - "description": "Delivery and engagement counters for one campaign, plus the rates derived from them. Every number is a materialized counter on the campaign row, so this is a single indexed read regardless of how many emails the campaign sent — cheap enough to poll while a campaign is in flight.\n\nRates are percentages (0–100) against `sent`, and are 0 before anything has been sent.\n\nRequires the `campaigns:read` scope — View your campaigns and their performance.", + }, "security": [ { "ApiKeyAuth": [] @@ -4475,101 +4526,99 @@ "SessionAuth": [] } ], + "summary": "Add a sending domain", + "tags": [ + "Domains" + ] + } + }, + "/api/domains/{id}": { + "delete": { + "description": "Removes the domain from the project. The underlying SES identity is also dropped if no other project still uses it.", + "operationId": "deleteDomain", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "string", "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" + "type": "string" + } } ], "responses": { "200": { - "description": "Campaign statistics", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CampaignV1Stats" + "$ref": "#/components/schemas/SuccessEmpty" } } - } + }, + "description": "Domain removed" }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "404": { - "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/segments": { - "get": { - "operationId": "v1ListSegments", - "tags": [ - "Segments" - ], - "summary": "List segments", - "description": "Cursor-paginated list of segments, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.\n\n`member_count` is materialized on each segment, so listing segments never fans out into one count per segment — it is refreshed as membership changes rather than computed on read.\n\nRequires the `segments:read` scope — View your segments and who belongs to them.", + }, "security": [ { "ApiKeyAuth": [] @@ -4578,100 +4627,111 @@ "SessionAuth": [] } ], + "summary": "Remove a sending domain", + "tags": [ + "Domains" + ] + }, + "get": { + "operationId": "getDomain", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "description": "Opaque cursor from a previous response's `next_cursor`." - }, - "required": false, - "description": "Opaque cursor from a previous response's `next_cursor`.", - "name": "after", - "in": "query" + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Segment list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SegmentV1List" + "properties": { + "data": { + "$ref": "#/components/schemas/Domain" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Domain" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "post": { - "operationId": "v1CreateSegment", - "tags": [ - "Segments" - ], - "summary": "Create a segment", - "description": "Create a `DYNAMIC` segment (a saved `condition`, re-evaluated against contacts on every read) or a `STATIC` one (an explicitly managed membership list). `type` is fixed at creation — it decides how membership is computed, so it cannot be changed later.\n\nA `DYNAMIC` segment requires a `condition`, which is validated and evaluated during the request: the response's `member_count` tells you immediately how many contacts the filter actually matches.\n\nRequires the `segments:write` scope — Create, edit, and delete your segments.", + }, "security": [ { "ApiKeyAuth": [] @@ -4680,98 +4740,114 @@ "SessionAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SegmentV1Create" - } + "summary": "Get a sending domain", + "tags": [ + "Domains" + ] + } + }, + "/api/domains/{id}/verify": { + "get": { + "description": "Read the current SES verification status without forcing a refresh.", + "operationId": "getDomainVerification", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Segment created", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SegmentV1" + "properties": { + "data": { + "$ref": "#/components/schemas/DomainVerificationStatus" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Verification status" }, "400": { - "description": "`validation_error` — a `DYNAMIC` segment was submitted without a `condition`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/segments/{id}": { - "get": { - "operationId": "v1GetSegment", - "tags": [ - "Segments" - ], - "summary": "Retrieve a segment", - "description": "Fetch one segment, including its saved `condition` and materialized `member_count`.\n\nRequires the `segments:read` scope — View your segments and who belongs to them.", + }, "security": [ { "ApiKeyAuth": [] @@ -4780,99 +4856,112 @@ "SessionAuth": [] } ], + "summary": "Read SES verification status", + "tags": [ + "Domains" + ] + }, + "post": { + "description": "Force a refresh of the domain's SES verification status.", + "operationId": "verifyDomain", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "string", "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" + "type": "string" + } } ], "responses": { "200": { - "description": "The segment", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SegmentV1" + "properties": { + "data": { + "$ref": "#/components/schemas/DomainVerificationStatus" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Verification status" }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "404": { - "description": "`resource_not_found` — no segment with this id belongs to the authenticated project.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "patch": { - "operationId": "v1UpdateSegment", - "tags": [ - "Segments" - ], - "summary": "Update a segment", - "description": "Partial update: an omitted field is left untouched. Changing a `DYNAMIC` segment's `condition` recomputes `member_count` in the same call, so the returned object never states a size that belongs to the previous filter. `condition` is ignored on a `STATIC` segment, whose membership is the explicit list.\n\n`type` is not accepted here — see the create operation.\n\nRequires the `segments:write` scope — Create, edit, and delete your segments.", + }, "security": [ { "ApiKeyAuth": [] @@ -4881,109 +4970,133 @@ "SessionAuth": [] } ], + "summary": "Trigger SES verification", + "tags": [ + "Domains" + ] + } + }, + "/api/emails": { + "get": { + "description": "List emails for the authenticated project. Cursor-paginated for stable scroll over large result sets.", + "operationId": "listEmails", "parameters": [ { + "in": "query", + "name": "limit", + "required": false, "schema": { - "type": "string", - "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SegmentV1Update" - } + "default": 50, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "in": "query", + "name": "cursor", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "tag", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "enum": [ + "PENDING", + "SENT", + "DELIVERED", + "OPENED", + "CLICKED", + "BOUNCED", + "COMPLAINED", + "FAILED" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "from", + "required": false, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { - "description": "The updated segment", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SegmentV1" - } - } - } - }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/EmailListResponse" } } - } + }, + "description": "Email list" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "404": { - "description": "`resource_not_found` — no segment with this id belongs to the authenticated project.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - } - } - }, - "delete": { - "operationId": "v1DeleteSegment", - "tags": [ - "Segments" - ], - "summary": "Delete a segment", - "description": "Delete a segment. Refused with 409 while any `DRAFT`, `SCHEDULED`, or `SENDING` campaign still targets it — deleting it would leave those campaigns pointing at an audience that no longer exists, and the failure would surface at send time instead of here. Remove the segment from those campaigns first.\n\nRequires the `segments:write` scope — Create, edit, and delete your segments.", + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Internal server error" + } + }, "security": [ { "ApiKeyAuth": [] @@ -4992,111 +5105,130 @@ "SessionAuth": [] } ], + "summary": "List emails", + "tags": [ + "Emails" + ] + }, + "post": { + "description": "Send a single transactional email. Accepts a `template` ID or an inline `subject` + `body`. An optional `Idempotency-Key` request header (1–255 chars, 24h TTL) ensures replay safety: the first request wins and a retry carrying the same key AND the same body replays its response. Reusing a key with a DIFFERENT body answers `422 IDEMPOTENCY_KEY_REUSED` — a key names one request, so it is never silently served another request's result.", + "operationId": "sendEmail", "parameters": [ { + "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", + "in": "header", + "name": "Idempotency-Key", + "required": false, "schema": { - "type": "string", - "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" + "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", + "maxLength": 255, + "minLength": 1, + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendEmail" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Segment deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SegmentV1Deleted" + "$ref": "#/components/schemas/SendEmailResponse" } } - } + }, + "description": "Email accepted / sent" }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows." }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "404": { - "description": "`resource_not_found` — no segment with this id belongs to the authenticated project.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent." }, "409": { - "description": "`conflict` — the segment is still used by one or more active campaigns.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "`CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly." }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "`IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key." }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "`CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay." } - } - } - }, - "/api/v1/segments/{id}/contacts": { - "get": { - "operationId": "v1ListSegmentContacts", - "tags": [ - "Segments" - ], - "summary": "List the contacts in a segment", - "description": "Cursor-paginated members of a segment. For a `STATIC` segment these are the rows of its membership list; for a `DYNAMIC` one the saved `condition` is evaluated against contacts as the page is read, so the result always reflects the contacts as they are now.\n\nCursors from this endpoint are not interchangeable with cursors from other list endpoints — the ordering differs — and pairing one with the wrong endpoint answers 422 rather than silently paging a different set.\n\nRequires the `segments:read` scope — View your segments and who belongs to them.", + }, "security": [ { "ApiKeyAuth": [] @@ -5105,123 +5237,140 @@ "SessionAuth": [] } ], + "summary": "Send a single transactional email", + "tags": [ + "Emails" + ] + } + }, + "/api/emails/batch": { + "post": { + "description": "Send up to 100 emails in one request. Returns 207 Multi-Status if any entry failed, or 200 if all succeeded. Per-entry results are reported in the `data` array.\n\nThe whole batch is ONE idempotent unit: an `Idempotency-Key` replayed with the same entry list replays the same per-index results, and replaying it with an edited list answers `422 IDEMPOTENCY_KEY_REUSED` rather than returning results for indexes the new body no longer has.", + "operationId": "sendEmailBatch", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid", - "description": "Resource id." - }, - "required": true, - "description": "Resource id.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - }, + "in": "header", + "name": "Idempotency-Key", "required": false, - "name": "limit", - "in": "query" - }, - { "schema": { - "type": "string", + "maxLength": 255, "minLength": 1, - "description": "Opaque cursor from a previous response's `next_cursor`." - }, - "required": false, - "description": "Opaque cursor from a previous response's `next_cursor`.", - "name": "after", - "in": "query" + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchSendBody" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Segment member list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SegmentContactV1List" + "$ref": "#/components/schemas/BatchSendResponse" } } - } + }, + "description": "All entries sent" + }, + "207": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchSendResponse" + } + } + }, + "description": "Partial success — at least one entry failed" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows." }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent." }, - "404": { - "description": "`resource_not_found` — no segment with this id belongs to the authenticated project.", + "409": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "`CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly." }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "`IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key." }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "`CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay." } - } - } - }, - "/api/v1/workflows": { - "get": { - "operationId": "v1ListWorkflows", - "tags": [ - "Workflows" - ], - "summary": "List workflows", - "description": "Cursor-paginated list of workflows, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.\n\nRequires the `workflows:read` scope — View your automation workflows and their runs.\n\nUnlike the legacy `/api/*` endpoints, v1 responses are the bare payload (no `{ success, data }` envelope) and errors are RFC 9457 problem documents.", + }, "security": [ { "ApiKeyAuth": [] @@ -5230,100 +5379,99 @@ "SessionAuth": [] } ], + "summary": "Send a batch of emails", + "tags": [ + "Emails" + ] + } + }, + "/api/emails/{id}": { + "get": { + "description": "Fetch one email along with its delivery events.", + "operationId": "getEmail", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "description": "Opaque cursor from a previous response's `next_cursor`." - }, - "required": false, - "description": "Opaque cursor from a previous response's `next_cursor`.", - "name": "after", - "in": "query" + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Workflow list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowV1List" + "$ref": "#/components/schemas/EmailGetResponse" } } - } + }, + "description": "Email" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "post": { - "operationId": "v1CreateWorkflow", - "tags": [ - "Workflows" - ], - "summary": "Create a workflow", - "description": "Creates an event-triggered workflow with a single trigger step. The rest of the graph (emails, delays, conditions) is built in the dashboard, so a workflow is created disabled and stays inert until it has steps to run.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + }, "security": [ { "ApiKeyAuth": [] @@ -5332,88 +5480,109 @@ "SessionAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkflowCreateV1" - } + "summary": "Get a single email", + "tags": [ + "Emails" + ] + } + }, + "/api/emails/{id}/schedule": { + "delete": { + "description": "Mark a still-pending email as FAILED before the worker picks it up. Returns 409 if the email has already left PENDING.", + "operationId": "cancelScheduledEmail", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Workflow created", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowV1" + "$ref": "#/components/schemas/EmailGetResponse" } } - } + }, + "description": "Email cancelled" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Email already past PENDING" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/workflows/{id}": { - "get": { - "operationId": "v1GetWorkflow", - "tags": [ - "Workflows" - ], - "summary": "Retrieve a workflow", - "description": "The workflow itself — its trigger, re-entry policy and rate cap. The step graph is not part of the v1 contract.\n\nRequires the `workflows:read` scope — View your automation workflows and their runs.", + }, "security": [ { "ApiKeyAuth": [] @@ -5422,99 +5591,131 @@ "SessionAuth": [] } ], + "summary": "Cancel a scheduled (still-PENDING) email", + "tags": [ + "Emails" + ] + } + }, + "/api/lists/{id}/subscribe": { + "post": { + "description": "Add a contact to a list, creating the contact if it does not exist. When the list has `doubleOptIn` enabled the membership is created as `PENDING` and the response carries a `confirmToken` — Sendly does NOT send the confirmation email, so the caller must deliver `/api/lists/confirm?token=` to the contact itself.\n\nAccepts SENDING_ONLY (`pk_*`) keys so it can back a public subscribe form.\n\n**Re-subscribing after an opt-out.** If the email already holds an `UNSUBSCRIBED` membership on this list, the call fails with `409 RESUBSCRIBE_CONFIRMATION_REQUIRED` unless the body sets `allowResubscribe: true`. Reversing an opt-out is a consent decision, so it is never the default — set the flag only when the contact themselves asked to be re-subscribed.\n\n`previousStatus` reports the membership's status before the call (`null` when it did not exist); prefer it over `created` when describing what changed, since `created: false` is equally true for an unchanged membership and for a reactivated one.", + "operationId": "subscribeToList", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid", - "description": "Workflow id." - }, - "required": true, - "description": "Workflow id.", + "description": "List id.", + "in": "path", "name": "id", - "in": "path" + "required": true, + "schema": { + "description": "List id.", + "minLength": 1, + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSubscribe" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Workflow", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowV1" + "$ref": "#/components/schemas/ListSubscribeResponse" } } - } + }, + "description": "Contact subscribed, or an existing membership returned unchanged" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "404": { - "description": "`resource_not_found` — no workflow with this id in the authenticated project.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "The contact previously unsubscribed from this list and `allowResubscribe` was not set. `error.code` is `RESUBSCRIBE_CONFIRMATION_REQUIRED` and `error.details.previousStatus` is `UNSUBSCRIBED`. Retry with `allowResubscribe: true` once the contact has consented." }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "patch": { - "operationId": "v1UpdateWorkflow", - "tags": [ - "Workflows" - ], - "summary": "Update a workflow", - "description": "Sparse update — omitted fields are left unchanged.\n\nTwo state rules apply: the trigger (`event_name`) cannot be changed while the workflow has running executions (409), and `enabled: true` is refused while any step is still unconfigured (422), because an enabled workflow accepts contacts immediately and would otherwise fail only once one reached the broken step.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + }, "security": [ { "ApiKeyAuth": [] @@ -5523,119 +5724,121 @@ "SessionAuth": [] } ], + "summary": "Subscribe a contact to a list", + "tags": [ + "Lists" + ] + } + }, + "/api/lists/{id}/unsubscribe": { + "post": { + "description": "Mark the contact's membership on this list as `UNSUBSCRIBED`. Accepts SENDING_ONLY (`pk_*`) keys so it can back a public preference form. Idempotent — unsubscribing an address that is not a member succeeds.\n\nOnce a membership is `UNSUBSCRIBED`, a later `POST /api/lists/{id}/subscribe` needs `allowResubscribe: true` to reverse it.", + "operationId": "unsubscribeFromList", "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid", - "description": "Workflow id." - }, - "required": true, - "description": "Workflow id.", + { + "description": "List id.", + "in": "path", "name": "id", - "in": "path" + "required": true, + "schema": { + "description": "List id.", + "minLength": 1, + "type": "string" + } } ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowUpdateV1" + "$ref": "#/components/schemas/ListUnsubscribe" } } - } + }, + "required": true }, "responses": { "200": { - "description": "Updated workflow", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowV1" + "$ref": "#/components/schemas/ListUnsubscribeResponse" } } - } + }, + "description": "Contact unsubscribed" }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "404": { - "description": "`resource_not_found` — no workflow with this id in the authenticated project.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "409": { - "description": "`conflict` — the trigger cannot be changed while executions are running.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "delete": { - "operationId": "v1DeleteWorkflow", - "tags": [ - "Workflows" - ], - "summary": "Delete a workflow", - "description": "Refused with 409 while executions are still running: deleting a workflow cascades its executions away, and a contact mid-journey disappearing is data loss the caller cannot detect afterwards. Disable the workflow or cancel its runs first.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + }, "security": [ { "ApiKeyAuth": [] @@ -5644,111 +5847,113 @@ "SessionAuth": [] } ], + "summary": "Unsubscribe a contact from a list", + "tags": [ + "Lists" + ] + } + }, + "/api/suppression": { + "get": { + "description": "Cursor-paginated list of suppressed addresses. Filter by `reason`.", + "operationId": "listSuppressions", "parameters": [ { + "in": "query", + "name": "limit", + "required": false, "schema": { - "type": "string", - "format": "uuid", - "description": "Workflow id." - }, - "required": true, - "description": "Workflow id.", - "name": "id", - "in": "path" + "default": 50, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "in": "query", + "name": "cursor", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "reason", + "required": false, + "schema": { + "enum": [ + "HARD_BOUNCE", + "COMPLAINT", + "MANUAL", + "UNSUBSCRIBE" + ], + "type": "string" + } } ], "responses": { "200": { - "description": "Workflow deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowDeletedV1" - } - } - } - }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/SuppressionListResponse" } } - } + }, + "description": "Suppression list" }, - "404": { - "description": "`resource_not_found` — no workflow with this id in the authenticated project.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "409": { - "description": "`conflict` — the workflow still has running executions.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/workflows/{id}/executions": { - "get": { - "operationId": "v1ListWorkflowExecutions", - "tags": [ - "Workflows" - ], - "summary": "List a workflow's executions", - "description": "One row per contact-run, newest first, cursor-paginated on the execution's start time. Filter by `status` to find stuck (`WAITING`) or failed runs.\n\nRequires the `workflows:read` scope — View your automation workflows and their runs.", + }, "security": [ { "ApiKeyAuth": [] @@ -5757,139 +5962,86 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid", - "description": "Workflow id." - }, - "required": true, - "description": "Workflow id.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "description": "Opaque cursor from a previous response's `next_cursor`." - }, - "required": false, - "description": "Opaque cursor from a previous response's `next_cursor`.", - "name": "after", - "in": "query" - }, - { - "schema": { - "type": "string", - "enum": [ - "RUNNING", - "WAITING", - "COMPLETED", - "EXITED", - "FAILED", - "CANCELLED" - ], - "description": "Return only executions in this state." - }, - "required": false, - "description": "Return only executions in this state.", - "name": "status", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Execution list", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkflowExecutionV1List" - } + "summary": "List suppressed emails", + "tags": [ + "Suppression" + ] + }, + "post": { + "description": "The `source` field is auto-derived: `API` for API-key callers, `DASHBOARD` for session callers.", + "operationId": "addSuppression", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddSuppression" } } }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "required": true + }, + "responses": { + "201": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Suppression" } } - } + }, + "description": "Suppression added" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "404": { - "description": "`resource_not_found` — no workflow with this id in the authenticated project.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "post": { - "operationId": "v1StartWorkflowExecution", - "tags": [ - "Workflows" - ], - "summary": "Start a workflow for a contact", - "description": "Enters one contact into an enabled workflow. Step processing runs asynchronously, so a 201 means the run was claimed — not that it finished.\n\n409 when the workflow's re-entry policy already accounts for this contact; 429 when the workflow's own `max_executions_per_hour` cap is reached.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + }, "security": [ { "ApiKeyAuth": [] @@ -5898,121 +6050,83 @@ "SessionAuth": [] } ], + "summary": "Manually add an email to the suppression list", + "tags": [ + "Suppression" + ] + } + }, + "/api/suppression/{email}": { + "delete": { + "description": "Idempotent. Silently no-ops if the suppression doesn't exist.", + "operationId": "removeSuppression", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid", - "description": "Workflow id." - }, + "description": "URL-encoded email address", + "in": "path", + "name": "email", "required": true, - "description": "Workflow id.", - "name": "id", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkflowExecutionStartV1" - } + "schema": { + "description": "URL-encoded email address", + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Execution started", + "204": { + "description": "Suppression removed" + }, + "400": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowExecutionV1" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "404": { - "description": "`resource_not_found` — no such workflow, or no such contact in this project.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "409": { - "description": "`conflict` — the contact already has an execution and re-entry is not allowed.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/workflows/executions/{execution_id}/cancel": { - "post": { - "operationId": "v1CancelWorkflowExecution", - "tags": [ - "Workflows" - ], - "summary": "Cancel a workflow execution", - "description": "Stops one run and stamps it `CANCELLED`. The execution stays queryable — cancelling is a state change, not a delete. Addressed by execution id alone, so a caller holding one from a list does not need to carry the workflow id with it.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + }, "security": [ { "ApiKeyAuth": [] @@ -6021,101 +6135,88 @@ "SessionAuth": [] } ], + "summary": "Remove an email from the suppression list", + "tags": [ + "Suppression" + ] + }, + "get": { + "description": "Returns `{ suppressed, reason?, source?, createdAt? }`. The path parameter must be URL-encoded.", + "operationId": "checkSuppression", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid", - "description": "Workflow execution id." - }, + "description": "URL-encoded email address", + "in": "path", + "name": "email", "required": true, - "description": "Workflow execution id.", - "name": "execution_id", - "in": "path" + "schema": { + "description": "URL-encoded email address", + "type": "string" + } } ], "responses": { "200": { - "description": "Cancelled execution", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowExecutionV1" - } - } - } - }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/SuppressionCheckResponse" } } - } + }, + "description": "Suppression check result" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "404": { - "description": "`resource_not_found` — no execution with this id in the authenticated project.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/workflows/{id}/stats": { - "get": { - "operationId": "v1GetWorkflowStats", - "tags": [ - "Workflows" - ], - "summary": "Retrieve workflow statistics", - "description": "Execution counts by status, average completion time, the emails this workflow sent (with opens and clicks), and per-goal conversion counts. All-time by default — pass `from` to narrow it. Unlike `/api/v1/analytics/*` there is no 90-day ceiling here, because every aggregate is already confined to this one workflow.\n\nRequires the `workflows:read` scope — View your automation workflows and their runs.", + }, "security": [ { "ApiKeyAuth": [] @@ -6124,115 +6225,131 @@ "SessionAuth": [] } ], + "summary": "Check whether an email is suppressed", + "tags": [ + "Suppression" + ] + } + }, + "/api/templates": { + "get": { + "description": "Cursor-paginated list of templates. Use `search` for full-text-ish filtering on name/description/subject.", + "operationId": "listTemplates", "parameters": [ { + "in": "query", + "name": "limit", + "required": false, "schema": { - "type": "string", - "format": "uuid", - "description": "Workflow id." - }, - "required": true, - "description": "Workflow id.", - "name": "id", - "in": "path" + "default": 20, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "in": "query", + "name": "cursor", + "required": false, + "schema": { + "minLength": 1, + "type": "string" + } }, { + "in": "query", + "name": "search", + "required": false, "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Only count executions started at or after this instant (ISO 8601). Defaults to all time." - }, + "type": "string" + } + }, + { + "in": "query", + "name": "type", "required": false, - "description": "Only count executions started at or after this instant (ISO 8601). Defaults to all time.", - "name": "from", - "in": "query" + "schema": { + "enum": [ + "MARKETING", + "TRANSACTIONAL", + "HEADLESS" + ], + "type": "string" + } } ], "responses": { "200": { - "description": "Workflow statistics", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowStatsV1" + "$ref": "#/components/schemas/TemplateListResponse" } } - } + }, + "description": "Template list" }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "404": { - "description": "`resource_not_found` — no workflow with this id in the authenticated project.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/emails": { - "post": { - "operationId": "sendEmail", - "tags": [ - "Emails" - ], - "summary": "Send a single transactional email", - "description": "Send a single transactional email. Accepts a `template` ID or an inline `subject` + `body`. An optional `Idempotency-Key` request header (1–255 chars, 24h TTL) ensures replay safety: the first request wins and a retry carrying the same key AND the same body replays its response. Reusing a key with a DIFFERENT body answers `422 IDEMPOTENCY_KEY_REUSED` — a key names one request, so it is never silently served another request's result.", + }, "security": [ { "ApiKeyAuth": [] @@ -6241,130 +6358,111 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request." - }, - "required": false, - "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", - "name": "Idempotency-Key", - "in": "header" - } - ], + "summary": "List templates", + "tags": [ + "Templates" + ] + }, + "post": { + "description": "Create a new email template. The `from` domain must already be verified for the project.", + "operationId": "createTemplate", "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SendEmail" + "$ref": "#/components/schemas/CreateTemplate" } } - } + }, + "required": true }, "responses": { - "200": { - "description": "Email accepted / sent", + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SendEmailResponse" + "properties": { + "data": { + "$ref": "#/components/schemas/Template" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Template created" }, "400": { - "description": "Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "`CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "422": { - "description": "`IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "503": { - "description": "`CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - }, - "get": { - "operationId": "listEmails", - "tags": [ - "Emails" - ], - "summary": "List emails", - "description": "List emails for the authenticated project. Cursor-paginated for stable scroll over large result sets.", + }, "security": [ { "ApiKeyAuth": [] @@ -6373,133 +6471,109 @@ "SessionAuth": [] } ], + "summary": "Create a template", + "tags": [ + "Templates" + ] + } + }, + "/api/templates/{id}": { + "delete": { + "description": "Answers 200 with `{ success, data: { id } }` (pre-seam this was 204 No Content). Refuses with 409 if the template is still attached to a workflow step or active campaign.", + "operationId": "deleteTemplate", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "cursor", - "in": "query" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "tag", - "in": "query" - }, - { - "schema": { - "type": "string", - "enum": [ - "PENDING", - "SENT", - "DELIVERED", - "OPENED", - "CLICKED", - "BOUNCED", - "COMPLAINED", - "FAILED" - ] - }, - "required": false, - "name": "status", - "in": "query" - }, - { - "schema": { + "format": "uuid", "type": "string" - }, - "required": false, - "name": "from", - "in": "query" + } } ], "responses": { "200": { - "description": "Email list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailListResponse" + "$ref": "#/components/schemas/IdResponse" } } - } + }, + "description": "Template deleted" }, "400": { - "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Resource not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Template still in use" }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/emails/{id}": { - "get": { - "operationId": "getEmail", - "tags": [ - "Emails" - ], - "summary": "Get a single email", - "description": "Fetch one email along with its delivery events.", + }, "security": [ { "ApiKeyAuth": [] @@ -6508,99 +6582,111 @@ "SessionAuth": [] } ], + "summary": "Delete a template", + "tags": [ + "Templates" + ] + }, + "get": { + "operationId": "getTemplate", "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, + { + "in": "path", "name": "id", - "in": "path" + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Email", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailGetResponse" + "properties": { + "data": { + "$ref": "#/components/schemas/Template" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Template" }, "400": { - "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "404": { - "description": "Resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/emails/batch": { - "post": { - "operationId": "sendEmailBatch", - "tags": [ - "Emails" - ], - "summary": "Send a batch of emails", - "description": "Send up to 100 emails in one request. Returns 207 Multi-Status if any entry failed, or 200 if all succeeded. Per-entry results are reported in the `data` array.\n\nThe whole batch is ONE idempotent unit: an `Idempotency-Key` replayed with the same entry list replays the same per-index results, and replaying it with an edited list answers `422 IDEMPOTENCY_KEY_REUSED` rather than returning results for indexes the new body no longer has.", + }, "security": [ { "ApiKeyAuth": [] @@ -6609,140 +6695,132 @@ "SessionAuth": [] } ], + "summary": "Get a template", + "tags": [ + "Templates" + ] + }, + "patch": { + "description": "Update one or more fields. If `from` changes, the new domain must already be verified.", + "operationId": "updateTemplate", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "required": false, - "name": "Idempotency-Key", - "in": "header" + "format": "uuid", + "type": "string" + } } ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BatchSendBody" + "$ref": "#/components/schemas/UpdateTemplate" } } - } + }, + "required": true }, "responses": { "200": { - "description": "All entries sent", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchSendResponse" - } - } - } - }, - "207": { - "description": "Partial success — at least one entry failed", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BatchSendResponse" + "properties": { + "data": { + "$ref": "#/components/schemas/Template" + }, + "success": { + "enum": [ + true + ], + "type": "boolean" + } + }, + "required": [ + "success", + "data" + ], + "type": "object" } } - } + }, + "description": "Updated template" }, "400": { - "description": "Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "409": { - "description": "`CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly.", + "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "422": { - "description": "`IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation failed — request body or query parameters did not match the schema" }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "503": { - "description": "`CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/emails/{id}/schedule": { - "delete": { - "operationId": "cancelScheduledEmail", - "tags": [ - "Emails" - ], - "summary": "Cancel a scheduled (still-PENDING) email", - "description": "Mark a still-pending email as FAILED before the worker picks it up. Returns 409 if the email has already left PENDING.", + }, "security": [ { "ApiKeyAuth": [] @@ -6751,238 +6829,195 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, - "name": "id", - "in": "path" - } - ], + "summary": "Update a template", + "tags": [ + "Templates" + ] + } + }, + "/api/track": { + "post": { + "description": "Record a custom event, creating or updating the contact by email as a side effect. Requires a FULL (`sk_*`) key — SENDING_ONLY (`pk_*`) keys answer 403, since recording events is not sending mail. Reserved system event names (`email.*`, `contact.subscribed`/`unsubscribed`, `segment.*.entry`/`.exit`) are rejected.", + "operationId": "trackEvent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrackEvent" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Email cancelled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailGetResponse" + "$ref": "#/components/schemas/TrackEventResponse" } } - } + }, + "description": "Event tracked" }, "400": { - "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } - }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "Email already past PENDING", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/contacts": { - "get": { - "operationId": "listContacts", - "tags": [ - "Contacts" - ], - "summary": "List contacts", - "description": "Cursor-paginated list of contacts. Supports filter by `search` and `subscribed`.", + }, "security": [ { "ApiKeyAuth": [] - }, - { - "SessionAuth": [] } ], + "summary": "Track a custom event for a contact", + "tags": [ + "Events" + ] + } + }, + "/api/v1/analytics/campaigns": { + "get": { + "description": "Campaign counts plus average open and click rates.\n\n`total` and `active` count campaigns CREATED in the window; `completed` counts campaigns SENT in it — so a campaign created earlier and sent inside the window appears only in `completed`. Rates are percentages to one decimal place, averaged over the campaigns sent in the window.\n\n`from` defaults to 30 days ago and is clamped to at most 90 days back; `to` defaults to now. A wider request is narrowed rather than refused, and the `window` field states the range actually covered — read it before comparing two responses.\n\nRequires the `analytics:read` scope — View your sending analytics and engagement metrics.", + "operationId": "v1GetCampaignAnalytics", "parameters": [ { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - }, + "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", + "in": "query", + "name": "from", "required": false, - "name": "limit", - "in": "query" - }, - { "schema": { - "type": "string" - }, - "required": false, - "name": "cursor", - "in": "query" + "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } }, { - "schema": { - "type": "string" - }, + "description": "End of the window (ISO 8601). Defaults to now.", + "in": "query", + "name": "to", "required": false, - "name": "search", - "in": "query" - }, - { "schema": { - "type": "string", - "enum": [ - "true", - "false" + "description": "End of the window (ISO 8601). Defaults to now.", + "format": "date-time", + "type": [ + "string", + "null" ] - }, - "required": false, - "name": "subscribed", - "in": "query" + } } ], "responses": { "200": { - "description": "Contact list", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ContactListResponse" - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/AnalyticsCampaignStatsV1" } } - } + }, + "description": "Campaign statistics" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "post": { - "operationId": "createContact", - "tags": [ - "Contacts" - ], - "summary": "Create a contact", - "description": "Create a new contact. Returns 409 on `(projectId, email)` conflict — use `/api/contacts/upsert` for create-or-update semantics.", + }, "security": [ { "ApiKeyAuth": [] @@ -6991,123 +7026,108 @@ "SessionAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateContact" - } + "summary": "Retrieve campaign totals and engagement", + "tags": [ + "Analytics" + ] + } + }, + "/api/v1/analytics/timeseries": { + "get": { + "description": "Daily counts of emails created, delivered, opened, clicked and bounced. Every day in the window is present even with zero activity, so the series never needs gap-filling.\n\n`from` defaults to 30 days ago and is clamped to at most 90 days back; `to` defaults to now. A wider request is narrowed rather than refused, and the `window` field states the range actually covered — read it before comparing two responses.\n\nRequires the `analytics:read` scope — View your sending analytics and engagement metrics.", + "operationId": "v1GetAnalyticsTimeseries", + "parameters": [ + { + "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", + "in": "query", + "name": "from", + "required": false, + "schema": { + "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + { + "description": "End of the window (ISO 8601). Defaults to now.", + "in": "query", + "name": "to", + "required": false, + "schema": { + "description": "End of the window (ISO 8601). Defaults to now.", + "format": "date-time", + "type": [ + "string", + "null" + ] } } - }, + ], "responses": { - "201": { - "description": "Contact created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Contact" - } - }, - "required": [ - "success", - "data" - ] - } - } - } - }, - "400": { - "description": "Validation error", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/AnalyticsTimeseriesV1" } } - } + }, + "description": "Daily time series" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "Email already exists for this project", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/contacts/upsert": { - "post": { - "operationId": "upsertContact", - "tags": [ - "Contacts" - ], - "summary": "Create or update a contact by email", - "description": "Idempotent contact upsert keyed by email. Always answers 200 — the create-vs-update distinction is not signalled via status code.", + }, "security": [ { "ApiKeyAuth": [] @@ -7116,113 +7136,119 @@ "SessionAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateContact" - } + "summary": "Retrieve the daily email time series", + "tags": [ + "Analytics" + ] + } + }, + "/api/v1/analytics/top-campaigns": { + "get": { + "description": "Campaigns sent in the window, ranked by open rate, capped at 50 rows. Not cursor-paginated: a leaderboard is a top-N by definition, and paging one would mean re-ranking on every page.\n\n`from` defaults to 30 days ago and is clamped to at most 90 days back; `to` defaults to now. A wider request is narrowed rather than refused, and the `window` field states the range actually covered — read it before comparing two responses.\n\nRequires the `analytics:read` scope — View your sending analytics and engagement metrics.", + "operationId": "v1ListTopCampaigns", + "parameters": [ + { + "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", + "in": "query", + "name": "from", + "required": false, + "schema": { + "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + { + "description": "End of the window (ISO 8601). Defaults to now.", + "in": "query", + "name": "to", + "required": false, + "schema": { + "description": "End of the window (ISO 8601). Defaults to now.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 10, + "maximum": 50, + "minimum": 1, + "type": "integer" } } - }, + ], "responses": { "200": { - "description": "Contact created or updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Contact" - } - }, - "required": [ - "success", - "data" - ] - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/AnalyticsTopCampaignsV1" } } - } + }, + "description": "Ranked campaigns" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/contacts/bulk": { - "post": { - "operationId": "bulkCreateContacts", - "tags": [ - "Contacts" - ], - "summary": "Bulk-create contacts", - "description": "Create up to 1000 contacts in one call. Per-row conflicts are reported as `skipped`.", + }, "security": [ { "ApiKeyAuth": [] @@ -7231,142 +7257,102 @@ "SessionAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ContactBulkCreateBody" - } + "summary": "List the best-performing campaigns", + "tags": [ + "Analytics" + ] + } + }, + "/api/v1/campaigns": { + "get": { + "description": "Cursor-paginated list of campaigns, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.\n\nRequires the `campaigns:read` scope — View your campaigns and their performance.\n\nUnlike the legacy `/api/*` endpoints, v1 responses are the bare payload (no `{ success, data }` envelope) and errors are RFC 9457 problem documents.", + "operationId": "v1ListCampaigns", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 20, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "in": "query", + "name": "after", + "required": false, + "schema": { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "minLength": 1, + "type": "string" } } - }, + ], "responses": { "200": { - "description": "Bulk-create result", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "type": "object", - "properties": { - "created": { - "type": "integer" - }, - "skipped": { - "type": "integer" - }, - "errors": { - "type": "array", - "items": { - "type": "object", - "properties": { - "index": { - "type": "integer" - }, - "message": { - "type": "string" - } - }, - "required": [ - "index", - "message" - ] - } - } - }, - "required": [ - "created", - "skipped", - "errors" - ] - } - }, - "required": [ - "success", - "data" - ] - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/CampaignV1List" } } - } + }, + "description": "Campaign list" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "delete": { - "operationId": "bulkDeleteContacts", - "tags": [ - "Contacts" - ], - "summary": "Bulk-delete contacts", - "description": "Delete up to 1000 contacts in one call. Provide either `ids` or `emails`.", + }, "security": [ { "ApiKeyAuth": [] @@ -7375,120 +7361,120 @@ "SessionAuth": [] } ], + "summary": "List campaigns", + "tags": [ + "Campaigns" + ] + }, + "post": { + "description": "Create a campaign in `DRAFT`. Creating never sends — `POST /api/v1/campaigns/{id}/send` is the only operation that puts mail on the wire — so a campaign can be built up and reviewed before it costs anything.\n\nThe `from` address is checked against this project's verified domains before the campaign is written, so a campaign never exists with a sender it cannot use.\n\n`segment_id` is required when `audience_type` is `SEGMENT`; `audience_condition` is required when it is `FILTERED`. Both answer 422 when missing.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + "operationId": "v1CreateCampaign", + "parameters": [ + { + "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", + "maxLength": 255, + "minLength": 1, + "type": "string" + } + } + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ContactBulkDeleteBody" + "$ref": "#/components/schemas/CampaignV1Create" } } - } + }, + "required": true }, "responses": { - "200": { - "description": "Bulk-delete result", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "type": "object", - "properties": { - "deleted": { - "type": "integer" - } - }, - "required": [ - "deleted" - ] - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/CampaignV1" } } - } + }, + "description": "Campaign created" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — `segment_id` names a segment that does not belong to this project." + }, + "409": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/contacts/{id}": { - "get": { - "operationId": "getContact", - "tags": [ - "Contacts" - ], - "summary": "Get a contact", + }, "security": [ { "ApiKeyAuth": [] @@ -7497,112 +7483,111 @@ "SessionAuth": [] } ], + "summary": "Create a campaign", + "tags": [ + "Campaigns" + ] + } + }, + "/api/v1/campaigns/{id}": { + "delete": { + "description": "Delete a `DRAFT` campaign. A campaign that has sent is the record of what went out and cannot be deleted (400) — cancel it instead if it is still scheduled or in flight.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + "operationId": "v1DeleteCampaign", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, + "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Contact", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Contact" - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/CampaignV1Deleted" } } - } + }, + "description": "Campaign deleted" }, "400": { - "description": "Validation error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — only `DRAFT` campaigns can be deleted." }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "404": { - "description": "Resource not found", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project." + }, + "422": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "patch": { - "operationId": "updateContact", - "tags": [ - "Contacts" - ], - "summary": "Update a contact", - "description": "Update `data` and/or `subscribed`. `email` is immutable here — use `/api/contacts/upsert` to change addresses.", + }, "security": [ { "ApiKeyAuth": [] @@ -7611,132 +7596,99 @@ "SessionAuth": [] } ], + "summary": "Delete a campaign", + "tags": [ + "Campaigns" + ] + }, + "get": { + "description": "Fetch one campaign, including its materialized delivery counters.\n\nRequires the `campaigns:read` scope — View your campaigns and their performance.", + "operationId": "v1GetCampaign", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, + "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateContactBody" - } + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" } } - }, + ], "responses": { "200": { - "description": "Updated contact", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Contact" - } - }, - "required": [ - "success", - "data" - ] - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/CampaignV1" } } - } + }, + "description": "The campaign" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "404": { - "description": "Resource not found", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "delete": { - "operationId": "deleteContact", - "tags": [ - "Contacts" - ], - "summary": "Delete a contact", - "description": "Hard-delete a contact. Answers 200 with `{ success, data: { id } }` (pre-seam this was 204 No Content).", + }, "security": [ { "ApiKeyAuth": [] @@ -7745,99 +7697,119 @@ "SessionAuth": [] } ], + "summary": "Retrieve a campaign", + "tags": [ + "Campaigns" + ] + }, + "patch": { + "description": "Partial update: an omitted field is left untouched. Only `DRAFT` and `SCHEDULED` campaigns are editable — editing one that is already sending would change what half its recipients receive, so it answers 400.\n\nChanging `from` re-verifies the sender domain. Changing the audience on a `DRAFT` campaign schedules a recipient recount, so `stats.total_recipients` converges shortly after the response.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + "operationId": "v1UpdateCampaign", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, + "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignV1Update" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Contact deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IdResponse" + "$ref": "#/components/schemas/CampaignV1" } } - } + }, + "description": "The updated campaign" }, "400": { - "description": "Validation error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — the campaign is not in an editable status, or the segment change is not allowed." }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "404": { - "description": "Resource not found", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project." }, - "429": { - "description": "Rate limit or billing limit exceeded", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, - "500": { - "description": "Internal server error", + "429": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } - } - } - } - }, - "/api/lists/{id}/subscribe": { - "post": { - "operationId": "subscribeToList", - "tags": [ - "Lists" - ], - "summary": "Subscribe a contact to a list", - "description": "Add a contact to a list, creating the contact if it does not exist. When the list has `doubleOptIn` enabled the membership is created as `PENDING` and the response carries a `confirmToken` — Sendly does NOT send the confirmation email, so the caller must deliver `/api/lists/confirm?token=` to the contact itself.\n\nAccepts SENDING_ONLY (`pk_*`) keys so it can back a public subscribe form.\n\n**Re-subscribing after an opt-out.** If the email already holds an `UNSUBSCRIBED` membership on this list, the call fails with `409 RESUBSCRIBE_CONFIRMATION_REQUIRED` unless the body sets `allowResubscribe: true`. Reversing an opt-out is a consent decision, so it is never the default — set the flag only when the contact themselves asked to be re-subscribed.\n\n`previousStatus` reports the membership's status before the call (`null` when it did not exist); prefer it over `created` when describing what changed, since `created: false` is equally true for an unchanged membership and for a reactivated one.", + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." + }, + "500": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`internal_error`." + } + }, "security": [ { "ApiKeyAuth": [] @@ -7846,131 +7818,111 @@ "SessionAuth": [] } ], + "summary": "Update a campaign", + "tags": [ + "Campaigns" + ] + } + }, + "/api/v1/campaigns/{id}/cancel": { + "post": { + "description": "Cancel a `SCHEDULED`, `SENDING`, or `PAUSED` campaign. Terminal — a cancelled campaign cannot be resumed or re-sent. Takes no request body.\n\nLike every action on this resource, the response is the campaign itself, so `status` tells you what the transition did without a second request.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + "operationId": "v1CancelCampaign", "parameters": [ { - "schema": { - "type": "string", - "minLength": 1, - "description": "List id." - }, - "required": true, - "description": "List id.", + "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListSubscribe" - } + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" } } - }, + ], "responses": { "200": { - "description": "Contact subscribed, or an existing membership returned unchanged", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListSubscribeResponse" + "$ref": "#/components/schemas/CampaignV1" } } - } + }, + "description": "The cancelled campaign" }, "400": { - "description": "Validation error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — only `SCHEDULED`, `SENDING`, or `PAUSED` campaigns can be cancelled." }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "The contact previously unsubscribed from this list and `allowResubscribe` was not set. `error.code` is `RESUBSCRIBE_CONFIRMATION_REQUIRED` and `error.details.previousStatus` is `UNSUBSCRIBED`. Retry with `allowResubscribe: true` once the contact has consented.", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/lists/{id}/unsubscribe": { - "post": { - "operationId": "unsubscribeFromList", - "tags": [ - "Lists" - ], - "summary": "Unsubscribe a contact from a list", - "description": "Mark the contact's membership on this list as `UNSUBSCRIBED`. Accepts SENDING_ONLY (`pk_*`) keys so it can back a public preference form. Idempotent — unsubscribing an address that is not a member succeeds.\n\nOnce a membership is `UNSUBSCRIBED`, a later `POST /api/lists/{id}/subscribe` needs `allowResubscribe: true` to reverse it.", + }, "security": [ { "ApiKeyAuth": [] @@ -7979,121 +7931,111 @@ "SessionAuth": [] } ], + "summary": "Cancel a campaign", + "tags": [ + "Campaigns" + ] + } + }, + "/api/v1/campaigns/{id}/pause": { + "post": { + "description": "Pause a `SENDING` campaign. The workers check the flag between batches, so a small number of already-queued emails may still be delivered after this returns. Takes no request body.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + "operationId": "v1PauseCampaign", "parameters": [ { - "schema": { - "type": "string", - "minLength": 1, - "description": "List id." - }, - "required": true, - "description": "List id.", + "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListUnsubscribe" - } + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" } } - }, + ], "responses": { "200": { - "description": "Contact unsubscribed", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListUnsubscribeResponse" + "$ref": "#/components/schemas/CampaignV1" } } - } + }, + "description": "The paused campaign" }, "400": { - "description": "Validation error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — only a `SENDING` campaign can be paused." }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "404": { - "description": "Resource not found", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/domains": { - "get": { - "operationId": "listDomains", - "tags": [ - "Domains" - ], - "summary": "List sending domains", - "description": "List all domains for the authenticated project.", + }, "security": [ { "ApiKeyAuth": [] @@ -8102,76 +8044,111 @@ "SessionAuth": [] } ], + "summary": "Pause a sending campaign", + "tags": [ + "Campaigns" + ] + } + }, + "/api/v1/campaigns/{id}/resume": { + "post": { + "description": "Resume a `PAUSED` campaign from the last entry in its per-contact send ledger. That ledger also dedupes, so a contact already sent to is skipped rather than mailed twice. Takes no request body.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + "operationId": "v1ResumeCampaign", + "parameters": [ + { + "description": "Resource id.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" + } + } + ], "responses": { "200": { - "description": "Domain list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DomainListResponse" + "$ref": "#/components/schemas/CampaignV1" } } - } + }, + "description": "The resumed campaign" }, "400": { - "description": "Validation error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — only a `PAUSED` campaign can be resumed." }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." + }, + "404": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project." + }, + "422": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "post": { - "operationId": "addDomain", - "tags": [ - "Domains" - ], - "summary": "Add a sending domain", - "description": "Register a new domain with SES and persist its DKIM tokens.", + }, "security": [ { "ApiKeyAuth": [] @@ -8180,102 +8157,143 @@ "SessionAuth": [] } ], + "summary": "Resume a paused campaign", + "tags": [ + "Campaigns" + ] + } + }, + "/api/v1/campaigns/{id}/send": { + "post": { + "description": "Start sending immediately, or park the campaign in `SCHEDULED` by passing `scheduled_for` (which must be in the future). The request body may be omitted entirely to send now.\n\n**Send an `Idempotency-Key`.** This is the operation that cannot be undone: a retry without one starts a second fan-out over the same audience. The key is scoped to this campaign, so the same key on a different campaign is a `422 idempotency_key_reused` rather than a replay of the first campaign's response.\n\nAnswers 400 when the campaign is not `DRAFT`/`SCHEDULED` or has no recipients, and 403 when the send would exceed the project's billing limit.\n\nRequires the `campaigns:write` scope — Create, edit, schedule, and send your campaigns.", + "operationId": "v1SendCampaign", + "parameters": [ + { + "description": "Resource id.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" + } + }, + { + "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "description": "Replay-safety key (24h TTL). Reuse it only to retry the identical request.", + "maxLength": 255, + "minLength": 1, + "type": "string" + } + } + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddDomainBody" + "$ref": "#/components/schemas/CampaignV1Send" } } - } + }, + "required": false }, "responses": { - "201": { - "description": "Domain added", + "200": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Domain" - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/CampaignV1" } } - } + }, + "description": "The campaign, now `SENDING` or `SCHEDULED`" }, "400": { - "description": "Validation error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — the campaign has already been sent or is sending, has no recipients, or `scheduled_for` is not in the future." }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." + }, + "404": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project." + }, + "409": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly." + }, + "422": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/domains/{id}": { - "get": { - "operationId": "getDomain", - "tags": [ - "Domains" - ], - "summary": "Get a sending domain", + }, "security": [ { "ApiKeyAuth": [] @@ -8284,112 +8302,101 @@ "SessionAuth": [] } ], + "summary": "Send or schedule a campaign", + "tags": [ + "Campaigns" + ] + } + }, + "/api/v1/campaigns/{id}/stats": { + "get": { + "description": "Delivery and engagement counters for one campaign, plus the rates derived from them. Every number is a materialized counter on the campaign row, so this is a single indexed read regardless of how many emails the campaign sent — cheap enough to poll while a campaign is in flight.\n\nRates are percentages (0–100) against `sent`, and are 0 before anything has been sent.\n\nRequires the `campaigns:read` scope — View your campaigns and their performance.", + "operationId": "v1GetCampaignStats", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, + "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Domain", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Domain" - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/CampaignV1Stats" } } - } + }, + "description": "Campaign statistics" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no campaign with this id belongs to the authenticated project." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "delete": { - "operationId": "deleteDomain", - "tags": [ - "Domains" - ], - "summary": "Remove a sending domain", - "description": "Removes the domain from the project. The underlying SES identity is also dropped if no other project still uses it.", + }, "security": [ { "ApiKeyAuth": [] @@ -8398,99 +8405,114 @@ "SessionAuth": [] } ], + "summary": "Retrieve campaign statistics", + "tags": [ + "Campaigns" + ] + } + }, + "/api/v1/events": { + "get": { + "description": "Cursor-paginated list of recorded events, newest first. Filter by `event_name` to follow a single series.\n\nA cursor is bound to the filters it was issued under: pairing page 2's `next_cursor` with a different `event_name` answers 422 rather than returning a page that belongs to neither query.\n\nRequires the `events:read` scope — View the custom events your application has recorded.", + "operationId": "v1ListEvents", "parameters": [ { + "in": "query", + "name": "limit", + "required": false, "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, - "name": "id", - "in": "path" + "default": 20, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "in": "query", + "name": "after", + "required": false, + "schema": { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "minLength": 1, + "type": "string" + } + }, + { + "description": "Return only events with this exact name.", + "in": "query", + "name": "event_name", + "required": false, + "schema": { + "description": "Return only events with this exact name.", + "maxLength": 200, + "minLength": 1, + "type": "string" + } } ], "responses": { "200": { - "description": "Domain removed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessEmpty" - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/EventV1List" } } - } + }, + "description": "Event list" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/domains/{id}/verify": { - "post": { - "operationId": "verifyDomain", - "tags": [ - "Domains" - ], - "summary": "Trigger SES verification", - "description": "Force a refresh of the domain's SES verification status.", + }, "security": [ { "ApiKeyAuth": [] @@ -8499,112 +8521,96 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Verification status", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/DomainVerificationStatus" - } - }, - "required": [ - "success", - "data" - ] - } + "summary": "List events", + "tags": [ + "Events" + ] + }, + "post": { + "description": "Records a custom event, optionally attached to a contact. Events drive segment membership and workflow triggers, so a matching enabled workflow starts as a result of this call.\n\n`contact_id` must already exist in this project — unlike `POST /api/track`, this endpoint never creates contacts. Omit it for a project-level event.\n\nReserved system event names (`email.*`, `contact.subscribed`/`unsubscribed`, `segment.*.entry`/`.exit`) are rejected with 422: they are written by Sendly's own pipeline and accepting them from a caller would corrupt the series segments read.\n\nThis endpoint does NOT require an `Idempotency-Key`. Events are append-only and the highest-volume write on the surface; a duplicate is a data-quality question for the caller, not a money-path hazard.\n\nRequires the `events:write` scope — Record custom events for your contacts. Sending-only (`pk_*`) keys do NOT hold it.", + "operationId": "v1TrackEvent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTrackV1" } } }, - "400": { - "description": "Validation error", + "required": true + }, + "responses": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/EventV1" } } - } + }, + "description": "Event recorded" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "404": { - "description": "Resource not found", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no contact with this id in the authenticated project." + }, + "422": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "get": { - "operationId": "getDomainVerification", - "tags": [ - "Domains" - ], - "summary": "Read SES verification status", - "description": "Read the current SES verification status without forcing a refresh.", + }, "security": [ { "ApiKeyAuth": [] @@ -8613,114 +8619,78 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, - "name": "id", - "in": "path" - } - ], + "summary": "Record an event", + "tags": [ + "Events" + ] + } + }, + "/api/v1/events/names": { + "get": { + "description": "Every distinct event name in the project, most frequent first — the vocabulary a caller needs before filtering events or pointing a workflow trigger at one. Unpaginated: the set is bounded by what the integration emits, not by event volume.\n\nRequires the `events:read` scope — View the custom events your application has recorded.", + "operationId": "v1ListEventNames", "responses": { "200": { - "description": "Verification status", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/DomainVerificationStatus" - } - }, - "required": [ - "success", - "data" - ] - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/EventNamesV1" } } - } + }, + "description": "Event names" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/templates": { - "get": { - "operationId": "listTemplates", - "tags": [ - "Templates" - ], - "summary": "List templates", - "description": "Cursor-paginated list of templates. Use `search` for full-text-ish filtering on name/description/subject.", + }, "security": [ { "ApiKeyAuth": [] @@ -8729,129 +8699,108 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - }, - "required": false, - "name": "limit", - "in": "query" - }, + "summary": "List event names", + "tags": [ + "Events" + ] + } + }, + "/api/v1/events/stats": { + "get": { + "description": "Per-name event counts over a bounded window, most frequent first.\n\nThe window defaults to the last 30 days and never reaches further back than 90: this is a GROUP BY over the highest-volume table in the system, and an all-time answer is not one it can keep giving at scale. A wider request is narrowed rather than refused, and the `window` field states the range actually covered.\n\nRequires the `events:read` scope — View the custom events your application has recorded.", + "operationId": "v1GetEventStats", + "parameters": [ { - "schema": { - "type": "string", - "minLength": 1 - }, + "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", + "in": "query", + "name": "from", "required": false, - "name": "cursor", - "in": "query" - }, - { "schema": { - "type": "string" - }, - "required": false, - "name": "search", - "in": "query" + "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } }, { + "description": "End of the window (ISO 8601). Defaults to now.", + "in": "query", + "name": "to", + "required": false, "schema": { - "type": "string", - "enum": [ - "MARKETING", - "TRANSACTIONAL", - "HEADLESS" + "description": "End of the window (ISO 8601). Defaults to now.", + "format": "date-time", + "type": [ + "string", + "null" ] - }, - "required": false, - "name": "type", - "in": "query" + } } ], "responses": { "200": { - "description": "Template list", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TemplateListResponse" - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/EventStatsV1" } } - } + }, + "description": "Event counts" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "post": { - "operationId": "createTemplate", - "tags": [ - "Templates" - ], - "summary": "Create a template", - "description": "Create a new email template. The `from` domain must already be verified for the project.", + }, "security": [ { "ApiKeyAuth": [] @@ -8860,112 +8809,102 @@ "SessionAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTemplate" - } + "summary": "Retrieve event counts", + "tags": [ + "Events" + ] + } + }, + "/api/v1/segments": { + "get": { + "description": "Cursor-paginated list of segments, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.\n\n`member_count` is materialized on each segment, so listing segments never fans out into one count per segment — it is refreshed as membership changes rather than computed on read.\n\nRequires the `segments:read` scope — View your segments and who belongs to them.", + "operationId": "v1ListSegments", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 20, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "in": "query", + "name": "after", + "required": false, + "schema": { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "minLength": 1, + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Template created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Template" - } - }, - "required": [ - "success", - "data" - ] - } - } - } - }, - "400": { - "description": "Validation error", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/SegmentV1List" } } - } + }, + "description": "Segment list" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/templates/{id}": { - "get": { - "operationId": "getTemplate", - "tags": [ - "Templates" - ], - "summary": "Get a template", + }, "security": [ { "ApiKeyAuth": [] @@ -8974,112 +8913,96 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, - "name": "id", - "in": "path" - } - ], + "summary": "List segments", + "tags": [ + "Segments" + ] + }, + "post": { + "description": "Create a `DYNAMIC` segment (a saved `condition`, re-evaluated against contacts on every read) or a `STATIC` one (an explicitly managed membership list). `type` is fixed at creation — it decides how membership is computed, so it cannot be changed later.\n\nA `DYNAMIC` segment requires a `condition`, which is validated and evaluated during the request: the response's `member_count` tells you immediately how many contacts the filter actually matches.\n\nRequires the `segments:write` scope — Create, edit, and delete your segments.", + "operationId": "v1CreateSegment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SegmentV1Create" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "Template", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Template" - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/SegmentV1" } } - } + }, + "description": "Segment created" }, "400": { - "description": "Validation error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — a `DYNAMIC` segment was submitted without a `condition`." }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "patch": { - "operationId": "updateTemplate", - "tags": [ - "Templates" - ], - "summary": "Update a template", - "description": "Update one or more fields. If `from` changes, the new domain must already be verified.", + }, "security": [ { "ApiKeyAuth": [] @@ -9088,132 +9011,111 @@ "SessionAuth": [] } ], + "summary": "Create a segment", + "tags": [ + "Segments" + ] + } + }, + "/api/v1/segments/{id}": { + "delete": { + "description": "Delete a segment. Refused with 409 while any `DRAFT`, `SCHEDULED`, or `SENDING` campaign still targets it — deleting it would leave those campaigns pointing at an audience that no longer exists, and the failure would surface at send time instead of here. Remove the segment from those campaigns first.\n\nRequires the `segments:write` scope — Create, edit, and delete your segments.", + "operationId": "v1DeleteSegment", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, + "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTemplate" - } + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" } } - }, + ], "responses": { "200": { - "description": "Updated template", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Template" - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/SegmentV1Deleted" } } - } + }, + "description": "Segment deleted" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no segment with this id belongs to the authenticated project." }, - "404": { - "description": "Resource not found", + "409": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`conflict` — the segment is still used by one or more active campaigns." }, "422": { - "description": "Validation failed — request body or query parameters did not match the schema", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "delete": { - "operationId": "deleteTemplate", - "tags": [ - "Templates" - ], - "summary": "Delete a template", - "description": "Answers 200 with `{ success, data: { id } }` (pre-seam this was 204 No Content). Refuses with 409 if the template is still attached to a workflow step or active campaign.", + }, "security": [ { "ApiKeyAuth": [] @@ -9222,109 +9124,99 @@ "SessionAuth": [] } ], + "summary": "Delete a segment", + "tags": [ + "Segments" + ] + }, + "get": { + "description": "Fetch one segment, including its saved `condition` and materialized `member_count`.\n\nRequires the `segments:read` scope — View your segments and who belongs to them.", + "operationId": "v1GetSegment", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, + "description": "Resource id.", + "in": "path", "name": "id", - "in": "path" + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Template deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IdResponse" - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/SegmentV1" } } - } + }, + "description": "The segment" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, "404": { - "description": "Resource not found", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no segment with this id belongs to the authenticated project." }, - "409": { - "description": "Template still in use", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/webhooks": { - "get": { - "operationId": "listWebhooks", - "tags": [ - "Webhooks" - ], - "summary": "List user webhooks", - "description": "List all user-managed outbound webhooks for the auth'd project (secrets are not returned).", + }, "security": [ { "ApiKeyAuth": [] @@ -9333,76 +9225,109 @@ "SessionAuth": [] } ], + "summary": "Retrieve a segment", + "tags": [ + "Segments" + ] + }, + "patch": { + "description": "Partial update: an omitted field is left untouched. Changing a `DYNAMIC` segment's `condition` recomputes `member_count` in the same call, so the returned object never states a size that belongs to the previous filter. `condition` is ignored on a `STATIC` segment, whose membership is the explicit list.\n\n`type` is not accepted here — see the create operation.\n\nRequires the `segments:write` scope — Create, edit, and delete your segments.", + "operationId": "v1UpdateSegment", + "parameters": [ + { + "description": "Resource id.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SegmentV1Update" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Webhook list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookListResponse" + "$ref": "#/components/schemas/SegmentV1" } } - } + }, + "description": "The updated segment" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." + }, + "404": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`resource_not_found` — no segment with this id belongs to the authenticated project." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "post": { - "operationId": "createWebhook", - "tags": [ - "Webhooks" - ], - "summary": "Create a webhook", - "description": "Register a new outbound webhook. The plaintext signing secret is returned exactly once — store it securely.", + }, "security": [ { "ApiKeyAuth": [] @@ -9411,87 +9336,123 @@ "SessionAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWebhook" - } + "summary": "Update a segment", + "tags": [ + "Segments" + ] + } + }, + "/api/v1/segments/{id}/contacts": { + "get": { + "description": "Cursor-paginated members of a segment. For a `STATIC` segment these are the rows of its membership list; for a `DYNAMIC` one the saved `condition` is evaluated against contacts as the page is read, so the result always reflects the contacts as they are now.\n\nCursors from this endpoint are not interchangeable with cursors from other list endpoints — the ordering differs — and pairing one with the wrong endpoint answers 422 rather than silently paging a different set.\n\nRequires the `segments:read` scope — View your segments and who belongs to them.", + "operationId": "v1ListSegmentContacts", + "parameters": [ + { + "description": "Resource id.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "Resource id.", + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 20, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "in": "query", + "name": "after", + "required": false, + "schema": { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "minLength": 1, + "type": "string" } } - }, + ], "responses": { - "201": { - "description": "Webhook created", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookCreateResponse" + "$ref": "#/components/schemas/SegmentContactV1List" } } - } + }, + "description": "Segment member list" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no segment with this id belongs to the authenticated project." + }, + "422": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/webhooks/{id}": { - "get": { - "operationId": "getWebhook", - "tags": [ - "Webhooks" - ], - "summary": "Get a webhook", + }, "security": [ { "ApiKeyAuth": [] @@ -9500,96 +9461,78 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, - "name": "id", - "in": "path" - } - ], + "summary": "List the contacts in a segment", + "tags": [ + "Segments" + ] + } + }, + "/api/v1/usage": { + "get": { + "description": "Email usage against the limits that are actually enforced: the current month's counts per source category, the monthly cap applied to their total, and today's sends against the trust-tier daily ceiling.\n\nEvery figure is read from an enforcement path, so what this reports and what refuses a send cannot disagree. Correspondingly, nothing else is published — there is no billing period, invoice total or non-email meter here, because the platform meters none of those.\n\nTwo caveats worth reading before you alert on these numbers:\n\n- The windows differ. The monthly counters roll over on the SERVER's calendar month; the daily counter buckets on the UTC date. The two therefore reset at different instants.\n- `monthly.limit` is null once a subscription makes sending metered rather than capped, and also when an operator has set per-category limits — in that case the caps live in `monthly.categories[*].limit`.\n\nRequires the `usage:read` scope — View your usage totals and billing limits.", + "operationId": "v1GetUsage", "responses": { "200": { - "description": "Webhook", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WebhookGetResponse" - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/UsageV1" } } - } + }, + "description": "Current usage" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "patch": { - "operationId": "updateWebhook", - "tags": [ - "Webhooks" - ], - "summary": "Update a webhook", + }, "security": [ { "ApiKeyAuth": [] @@ -9598,107 +9541,102 @@ "SessionAuth": [] } ], + "summary": "Retrieve current usage and limits", + "tags": [ + "Usage" + ] + } + }, + "/api/v1/workflows": { + "get": { + "description": "Cursor-paginated list of workflows, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.\n\nRequires the `workflows:read` scope — View your automation workflows and their runs.\n\nUnlike the legacy `/api/*` endpoints, v1 responses are the bare payload (no `{ success, data }` envelope) and errors are RFC 9457 problem documents.", + "operationId": "v1ListWorkflows", "parameters": [ { + "in": "query", + "name": "limit", + "required": false, "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, - "name": "id", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWebhook" - } + "default": 20, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "in": "query", + "name": "after", + "required": false, + "schema": { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "minLength": 1, + "type": "string" } } - }, + ], "responses": { "200": { - "description": "Webhook updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WebhookGetResponse" - } - } - } - }, - "400": { - "description": "Validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/WorkflowV1List" } } - } + }, + "description": "Workflow list" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "delete": { - "operationId": "deleteWebhook", - "tags": [ - "Webhooks" - ], - "summary": "Delete a webhook", - "description": "Hard-delete a webhook. Cascades to all WebhookCall rows.", + }, "security": [ { "ApiKeyAuth": [] @@ -9707,99 +9645,86 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Webhook deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessEmpty" - } + "summary": "List workflows", + "tags": [ + "Workflows" + ] + }, + "post": { + "description": "Creates an event-triggered workflow with a single trigger step. The rest of the graph (emails, delays, conditions) is built in the dashboard, so a workflow is created disabled and stays inert until it has steps to run.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + "operationId": "v1CreateWorkflow", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowCreateV1" } } }, - "400": { - "description": "Validation error", + "required": true + }, + "responses": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/WorkflowV1" } } - } + }, + "description": "Workflow created" }, "401": { - "description": "Unauthorized — missing or invalid auth", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "Forbidden — insufficient permissions or project disabled", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/webhooks/{id}/rotate-secret": { - "post": { - "operationId": "rotateWebhookSecret", - "tags": [ - "Webhooks" - ], - "summary": "Rotate the webhook signing secret", - "description": "Generate a new shared secret. Returns the new plaintext secret exactly once.", + }, "security": [ { "ApiKeyAuth": [] @@ -9808,99 +9733,101 @@ "SessionAuth": [] } ], + "summary": "Create a workflow", + "tags": [ + "Workflows" + ] + } + }, + "/api/v1/workflows/executions/{execution_id}/cancel": { + "post": { + "description": "Stops one run and stamps it `CANCELLED`. The execution stays queryable — cancelling is a state change, not a delete. Addressed by execution id alone, so a caller holding one from a list does not need to carry the workflow id with it.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + "operationId": "v1CancelWorkflowExecution", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid" - }, + "description": "Workflow execution id.", + "in": "path", + "name": "execution_id", "required": true, - "name": "id", - "in": "path" + "schema": { + "description": "Workflow execution id.", + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Secret rotated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookRotateSecretResponse" + "$ref": "#/components/schemas/WorkflowExecutionV1" } } - } + }, + "description": "Cancelled execution" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no execution with this id in the authenticated project." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/webhooks/{id}/calls": { - "get": { - "operationId": "listWebhookCalls", - "tags": [ - "Webhooks" - ], - "summary": "List recent webhook calls", - "description": "Cursor-paginated list of recent delivery attempts for a single webhook.", + }, "security": [ { "ApiKeyAuth": [] @@ -9909,118 +9836,111 @@ "SessionAuth": [] } ], + "summary": "Cancel a workflow execution", + "tags": [ + "Workflows" + ] + } + }, + "/api/v1/workflows/{id}": { + "delete": { + "description": "Refused with 409 while executions are still running: deleting a workflow cascades its executions away, and a contact mid-journey disappearing is data loss the caller cannot detect afterwards. Disable the workflow or cancel its runs first.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + "operationId": "v1DeleteWorkflow", "parameters": [ { - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true, + "description": "Workflow id.", + "in": "path", "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { + "required": true, "schema": { + "description": "Workflow id.", + "format": "uuid", "type": "string" - }, - "required": false, - "name": "cursor", - "in": "query" + } } ], "responses": { "200": { - "description": "Webhook call history", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookCallsListResponse" + "$ref": "#/components/schemas/WorkflowDeletedV1" } } - } + }, + "description": "Workflow deleted" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`resource_not_found` — no workflow with this id in the authenticated project." + }, + "409": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`conflict` — the workflow still has running executions." }, - "404": { - "description": "Resource not found", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/suppression": { - "get": { - "operationId": "listSuppressions", - "tags": [ - "Suppression" - ], - "summary": "List suppressed emails", - "description": "Cursor-paginated list of suppressed addresses. Filter by `reason`.", + }, "security": [ { "ApiKeyAuth": [] @@ -10029,111 +9949,99 @@ "SessionAuth": [] } ], + "summary": "Delete a workflow", + "tags": [ + "Workflows" + ] + }, + "get": { + "description": "The workflow itself — its trigger, re-entry policy and rate cap. The step graph is not part of the v1 contract.\n\nRequires the `workflows:read` scope — View your automation workflows and their runs.", + "operationId": "v1GetWorkflow", "parameters": [ { + "description": "Workflow id.", + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { + "description": "Workflow id.", + "format": "uuid", "type": "string" - }, - "required": false, - "name": "cursor", - "in": "query" - }, - { - "schema": { - "type": "string", - "enum": [ - "HARD_BOUNCE", - "COMPLAINT", - "MANUAL", - "UNSUBSCRIBE" - ] - }, - "required": false, - "name": "reason", - "in": "query" + } } ], "responses": { "200": { - "description": "Suppression list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionListResponse" + "$ref": "#/components/schemas/WorkflowV1" } } - } + }, + "description": "Workflow" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no workflow with this id in the authenticated project." + }, + "422": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "post": { - "operationId": "addSuppression", - "tags": [ - "Suppression" - ], - "summary": "Manually add an email to the suppression list", - "description": "The `source` field is auto-derived: `API` for API-key callers, `DASHBOARD` for session callers.", + }, "security": [ { "ApiKeyAuth": [] @@ -10142,88 +10050,119 @@ "SessionAuth": [] } ], + "summary": "Retrieve a workflow", + "tags": [ + "Workflows" + ] + }, + "patch": { + "description": "Sparse update — omitted fields are left unchanged.\n\nTwo state rules apply: the trigger (`event_name`) cannot be changed while the workflow has running executions (409), and `enabled: true` is refused while any step is still unconfigured (422), because an enabled workflow accepts contacts immediately and would otherwise fail only once one reached the broken step.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + "operationId": "v1UpdateWorkflow", + "parameters": [ + { + "description": "Workflow id.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "Workflow id.", + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddSuppression" + "$ref": "#/components/schemas/WorkflowUpdateV1" } } - } + }, + "required": true }, "responses": { - "201": { - "description": "Suppression added", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Suppression" + "$ref": "#/components/schemas/WorkflowV1" } } - } + }, + "description": "Updated workflow" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no workflow with this id in the authenticated project." + }, + "409": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`conflict` — the trigger cannot be changed while executions are running." + }, + "422": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/suppression/{email}": { - "get": { - "operationId": "checkSuppression", - "tags": [ - "Suppression" - ], - "summary": "Check whether an email is suppressed", - "description": "Returns `{ suppressed, reason?, source?, createdAt? }`. The path parameter must be URL-encoded.", + }, "security": [ { "ApiKeyAuth": [] @@ -10232,88 +10171,141 @@ "SessionAuth": [] } ], + "summary": "Update a workflow", + "tags": [ + "Workflows" + ] + } + }, + "/api/v1/workflows/{id}/executions": { + "get": { + "description": "One row per contact-run, newest first, cursor-paginated on the execution's start time. Filter by `status` to find stuck (`WAITING`) or failed runs.\n\nRequires the `workflows:read` scope — View your automation workflows and their runs.", + "operationId": "v1ListWorkflowExecutions", "parameters": [ { + "description": "Workflow id.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "Workflow id.", + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 20, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "in": "query", + "name": "after", + "required": false, + "schema": { + "description": "Opaque cursor from a previous response's `next_cursor`.", + "minLength": 1, + "type": "string" + } + }, + { + "description": "Return only executions in this state.", + "in": "query", + "name": "status", + "required": false, "schema": { - "type": "string", - "description": "URL-encoded email address" - }, - "required": true, - "description": "URL-encoded email address", - "name": "email", - "in": "path" + "description": "Return only executions in this state.", + "enum": [ + "RUNNING", + "WAITING", + "COMPLETED", + "EXITED", + "FAILED", + "CANCELLED" + ], + "type": "string" + } } ], "responses": { "200": { - "description": "Suppression check result", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionCheckResponse" + "$ref": "#/components/schemas/WorkflowExecutionV1List" } } - } + }, + "description": "Execution list" }, - "400": { - "description": "Validation error", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no workflow with this id in the authenticated project." + }, + "422": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "delete": { - "operationId": "removeSuppression", - "tags": [ - "Suppression" - ], - "summary": "Remove an email from the suppression list", - "description": "Idempotent. Silently no-ops if the suppression doesn't exist.", + }, "security": [ { "ApiKeyAuth": [] @@ -10322,170 +10314,119 @@ "SessionAuth": [] } ], + "summary": "List a workflow's executions", + "tags": [ + "Workflows" + ] + }, + "post": { + "description": "Enters one contact into an enabled workflow. Step processing runs asynchronously, so a 201 means the run was claimed — not that it finished.\n\n409 when the workflow's re-entry policy already accounts for this contact; 429 when the workflow's own `max_executions_per_hour` cap is reached.\n\nRequires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.", + "operationId": "v1StartWorkflowExecution", "parameters": [ { - "schema": { - "type": "string", - "description": "URL-encoded email address" - }, + "description": "Workflow id.", + "in": "path", + "name": "id", "required": true, - "description": "URL-encoded email address", - "name": "email", - "in": "path" + "schema": { + "description": "Workflow id.", + "format": "uuid", + "type": "string" + } } ], - "responses": { - "204": { - "description": "Suppression removed" - }, - "400": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "401": { - "description": "Unauthorized — missing or invalid auth", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowExecutionStartV1" } } }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "required": true + }, + "responses": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/WorkflowExecutionV1" } } - } + }, + "description": "Execution started" }, - "429": { - "description": "Rate limit or billing limit exceeded", + "401": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/api/track": { - "post": { - "operationId": "trackEvent", - "tags": [ - "Events" - ], - "summary": "Track a custom event for a contact", - "description": "Record a custom event, creating or updating the contact by email as a side effect. Requires a FULL (`sk_*`) key — SENDING_ONLY (`pk_*`) keys answer 403, since recording events is not sending mail. Reserved system event names (`email.*`, `contact.subscribed`/`unsubscribed`, `segment.*.entry`/`.exit`) are rejected.", - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TrackEvent" - } - } - } - }, - "responses": { - "200": { - "description": "Event tracked", + "403": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/TrackEventResponse" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." }, - "400": { - "description": "Validation error", + "404": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`resource_not_found` — no such workflow, or no such contact in this project." }, - "401": { - "description": "Unauthorized — missing or invalid auth", + "409": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`conflict` — the contact already has an execution and re-entry is not allowed." }, - "403": { - "description": "Forbidden — insufficient permissions or project disabled", + "422": { "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "Rate limit or billing limit exceeded", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "Internal server error", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - } - }, - "/api/v1/events": { - "get": { - "operationId": "v1ListEvents", - "tags": [ - "Events" - ], - "summary": "List events", - "description": "Cursor-paginated list of recorded events, newest first. Filter by `event_name` to follow a single series.\n\nA cursor is bound to the filters it was issued under: pairing page 2's `next_cursor` with a different `event_name` answers 422 rather than returning a page that belongs to neither query.\n\nRequires the `events:read` scope — View the custom events your application has recorded.", + }, "security": [ { "ApiKeyAuth": [] @@ -10494,112 +10435,115 @@ "SessionAuth": [] } ], + "summary": "Start a workflow for a contact", + "tags": [ + "Workflows" + ] + } + }, + "/api/v1/workflows/{id}/stats": { + "get": { + "description": "Execution counts by status, average completion time, the emails this workflow sent (with opens and clicks), and per-goal conversion counts. All-time by default — pass `from` to narrow it. Unlike `/api/v1/analytics/*` there is no 90-day ceiling here, because every aggregate is already confined to this one workflow.\n\nRequires the `workflows:read` scope — View your automation workflows and their runs.", + "operationId": "v1GetWorkflowStats", "parameters": [ { + "description": "Workflow id.", + "in": "path", + "name": "id", + "required": true, "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "description": "Opaque cursor from a previous response's `next_cursor`." - }, - "required": false, - "description": "Opaque cursor from a previous response's `next_cursor`.", - "name": "after", - "in": "query" + "description": "Workflow id.", + "format": "uuid", + "type": "string" + } }, { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Return only events with this exact name." - }, + "description": "Only count executions started at or after this instant (ISO 8601). Defaults to all time.", + "in": "query", + "name": "from", "required": false, - "description": "Return only events with this exact name.", - "name": "event_name", - "in": "query" + "schema": { + "description": "Only count executions started at or after this instant (ISO 8601). Defaults to all time.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } } ], "responses": { "200": { - "description": "Event list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EventV1List" + "$ref": "#/components/schemas/WorkflowStatsV1" } } - } + }, + "description": "Workflow statistics" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials." }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`scope_missing`, `project_access_denied`, or `project_disabled`." + }, + "404": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "`resource_not_found` — no workflow with this id in the authenticated project." }, "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`validation_error` — query, path, or body parameters did not match the schema." }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers." }, "500": { - "description": "`internal_error`.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } - } + }, + "description": "`internal_error`." } - } - }, - "post": { - "operationId": "v1TrackEvent", - "tags": [ - "Events" - ], - "summary": "Record an event", - "description": "Records a custom event, optionally attached to a contact. Events drive segment membership and workflow triggers, so a matching enabled workflow starts as a result of this call.\n\n`contact_id` must already exist in this project — unlike `POST /api/track`, this endpoint never creates contacts. Omit it for a project-level event.\n\nReserved system event names (`email.*`, `contact.subscribed`/`unsubscribed`, `segment.*.entry`/`.exit`) are rejected with 422: they are written by Sendly's own pipeline and accepting them from a caller would corrupt the series segments read.\n\nThis endpoint does NOT require an `Idempotency-Key`. Events are append-only and the highest-volume write on the surface; a duplicate is a data-quality question for the caller, not a money-path hazard.\n\nRequires the `events:write` scope — Record custom events for your contacts. Sending-only (`pk_*`) keys do NOT hold it.", + }, "security": [ { "ApiKeyAuth": [] @@ -10608,178 +10552,131 @@ "SessionAuth": [] } ], + "summary": "Retrieve workflow statistics", + "tags": [ + "Workflows" + ] + } + }, + "/api/verify": { + "post": { + "description": "Open endpoint (no auth required) that checks an email for syntax, MX records, disposable domains, and plus-addressing. Used by the marketing site verifier.", + "operationId": "verifyEmailAddress", "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EventTrackV1" + "$ref": "#/components/schemas/VerifyEmail" } } - } + }, + "required": true }, "responses": { - "201": { - "description": "Event recorded", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EventV1" - } - } - } - }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "404": { - "description": "`resource_not_found` — no contact with this id in the authenticated project.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" - } - } - } - }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/VerifyEmailResponse" } } - } + }, + "description": "Verification result" }, - "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } + }, + "security": [], + "summary": "Validate an email address", + "tags": [ + "Verify" + ] } }, - "/api/v1/events/names": { + "/api/webhooks": { "get": { - "operationId": "v1ListEventNames", - "tags": [ - "Events" - ], - "summary": "List event names", - "description": "Every distinct event name in the project, most frequent first — the vocabulary a caller needs before filtering events or pointing a workflow trigger at one. Unpaginated: the set is bounded by what the integration emits, not by event volume.\n\nRequires the `events:read` scope — View the custom events your application has recorded.", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "SessionAuth": [] - } - ], + "description": "List all user-managed outbound webhooks for the auth'd project (secrets are not returned).", + "operationId": "listWebhooks", "responses": { "200": { - "description": "Event names", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EventNamesV1" + "$ref": "#/components/schemas/WebhookListResponse" } } - } + }, + "description": "Webhook list" }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/events/stats": { - "get": { - "operationId": "v1GetEventStats", - "tags": [ - "Events" - ], - "summary": "Retrieve event counts", - "description": "Per-name event counts over a bounded window, most frequent first.\n\nThe window defaults to the last 30 days and never reaches further back than 90: this is a GROUP BY over the highest-volume table in the system, and an all-time answer is not one it can keep giving at scale. A wider request is narrowed rather than refused, and the `window` field states the range actually covered.\n\nRequires the `events:read` scope — View the custom events your application has recorded.", + }, "security": [ { "ApiKeyAuth": [] @@ -10788,108 +10685,86 @@ "SessionAuth": [] } ], - "parameters": [ - { - "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back." - }, - "required": false, - "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", - "name": "from", - "in": "query" + "summary": "List user webhooks", + "tags": [ + "Webhooks" + ] + }, + "post": { + "description": "Register a new outbound webhook. The plaintext signing secret is returned exactly once — store it securely.", + "operationId": "createWebhook", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhook" + } + } }, - { - "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "End of the window (ISO 8601). Defaults to now." - }, - "required": false, - "description": "End of the window (ISO 8601). Defaults to now.", - "name": "to", - "in": "query" - } - ], + "required": true + }, "responses": { - "200": { - "description": "Event counts", + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EventStatsV1" + "$ref": "#/components/schemas/WebhookCreateResponse" } } - } + }, + "description": "Webhook created" }, - "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", + "400": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, - "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", + "401": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "403": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/analytics/timeseries": { - "get": { - "operationId": "v1GetAnalyticsTimeseries", - "tags": [ - "Analytics" - ], - "summary": "Retrieve the daily email time series", - "description": "Daily counts of emails created, delivered, opened, clicked and bounced. Every day in the window is present even with zero activity, so the series never needs gap-filling.\n\n`from` defaults to 30 days ago and is clamped to at most 90 days back; `to` defaults to now. A wider request is narrowed rather than refused, and the `window` field states the range actually covered — read it before comparing two responses.\n\nRequires the `analytics:read` scope — View your sending analytics and engagement metrics.", + }, "security": [ { "ApiKeyAuth": [] @@ -10898,108 +10773,99 @@ "SessionAuth": [] } ], + "summary": "Create a webhook", + "tags": [ + "Webhooks" + ] + } + }, + "/api/webhooks/{id}": { + "delete": { + "description": "Hard-delete a webhook. Cascades to all WebhookCall rows.", + "operationId": "deleteWebhook", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back." - }, - "required": false, - "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", - "name": "from", - "in": "query" - }, - { - "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "End of the window (ISO 8601). Defaults to now." - }, - "required": false, - "description": "End of the window (ISO 8601). Defaults to now.", - "name": "to", - "in": "query" + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Daily time series", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AnalyticsTimeseriesV1" + "$ref": "#/components/schemas/SuccessEmpty" } } - } + }, + "description": "Webhook deleted" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/analytics/campaigns": { - "get": { - "operationId": "v1GetCampaignAnalytics", - "tags": [ - "Analytics" - ], - "summary": "Retrieve campaign totals and engagement", - "description": "Campaign counts plus average open and click rates.\n\n`total` and `active` count campaigns CREATED in the window; `completed` counts campaigns SENT in it — so a campaign created earlier and sent inside the window appears only in `completed`. Rates are percentages to one decimal place, averaged over the campaigns sent in the window.\n\n`from` defaults to 30 days ago and is clamped to at most 90 days back; `to` defaults to now. A wider request is narrowed rather than refused, and the `window` field states the range actually covered — read it before comparing two responses.\n\nRequires the `analytics:read` scope — View your sending analytics and engagement metrics.", + }, "security": [ { "ApiKeyAuth": [] @@ -11008,108 +10874,96 @@ "SessionAuth": [] } ], + "summary": "Delete a webhook", + "tags": [ + "Webhooks" + ] + }, + "get": { + "operationId": "getWebhook", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back." - }, - "required": false, - "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", - "name": "from", - "in": "query" - }, - { - "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "End of the window (ISO 8601). Defaults to now." - }, - "required": false, - "description": "End of the window (ISO 8601). Defaults to now.", - "name": "to", - "in": "query" + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { - "description": "Campaign statistics", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AnalyticsCampaignStatsV1" + "$ref": "#/components/schemas/WebhookGetResponse" } } - } + }, + "description": "Webhook" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/analytics/top-campaigns": { - "get": { - "operationId": "v1ListTopCampaigns", - "tags": [ - "Analytics" - ], - "summary": "List the best-performing campaigns", - "description": "Campaigns sent in the window, ranked by open rate, capped at 50 rows. Not cursor-paginated: a leaderboard is a top-N by definition, and paging one would mean re-ranking on every page.\n\n`from` defaults to 30 days ago and is clamped to at most 90 days back; `to` defaults to now. A wider request is narrowed rather than refused, and the `window` field states the range actually covered — read it before comparing two responses.\n\nRequires the `analytics:read` scope — View your sending analytics and engagement metrics.", + }, "security": [ { "ApiKeyAuth": [] @@ -11118,119 +10972,106 @@ "SessionAuth": [] } ], + "summary": "Get a webhook", + "tags": [ + "Webhooks" + ] + }, + "patch": { + "operationId": "updateWebhook", "parameters": [ { + "in": "path", + "name": "id", + "required": true, "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back." - }, - "required": false, - "description": "Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back.", - "name": "from", - "in": "query" - }, - { - "schema": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "End of the window (ISO 8601). Defaults to now." - }, - "required": false, - "description": "End of the window (ISO 8601). Defaults to now.", - "name": "to", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "default": 10 - }, - "required": false, - "name": "limit", - "in": "query" + "format": "uuid", + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhook" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Ranked campaigns", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AnalyticsTopCampaignsV1" + "$ref": "#/components/schemas/WebhookGetResponse" + } + } + }, + "description": "Webhook updated" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" } - } - } - }, - "/api/v1/usage": { - "get": { - "operationId": "v1GetUsage", - "tags": [ - "Usage" - ], - "summary": "Retrieve current usage and limits", - "description": "Email usage against the limits that are actually enforced: the current month's counts per source category, the monthly cap applied to their total, and today's sends against the trust-tier daily ceiling.\n\nEvery figure is read from an enforcement path, so what this reports and what refuses a send cannot disagree. Correspondingly, nothing else is published — there is no billing period, invoice total or non-email meter here, because the platform meters none of those.\n\nTwo caveats worth reading before you alert on these numbers:\n\n- The windows differ. The monthly counters roll over on the SERVER's calendar month; the daily counter buckets on the UTC date. The two therefore reset at different instants.\n- `monthly.limit` is null once a subscription makes sending metered rather than capped, and also when an operator has set per-category limits — in that case the caps live in `monthly.categories[*].limit`.\n\nRequires the `usage:read` scope — View your usage totals and billing limits.", + }, "security": [ { "ApiKeyAuth": [] @@ -11239,123 +11080,296 @@ "SessionAuth": [] } ], + "summary": "Update a webhook", + "tags": [ + "Webhooks" + ] + } + }, + "/api/webhooks/{id}/calls": { + "get": { + "description": "Cursor-paginated list of recent delivery attempts for a single webhook.", + "operationId": "listWebhookCalls", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + { + "in": "query", + "name": "cursor", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Current usage", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UsageV1" + "$ref": "#/components/schemas/WebhookCallsListResponse" } } - } + }, + "description": "Webhook call history" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Validation error" }, "401": { - "description": "`invalid_api_key` or `invalid_session` — missing or invalid credentials.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Unauthorized — missing or invalid auth" }, "403": { - "description": "`scope_missing`, `project_access_denied`, or `project_disabled`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Forbidden — insufficient permissions or project disabled" }, - "422": { - "description": "`validation_error` — query, path, or body parameters did not match the schema.", + "404": { "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Resource not found" }, "429": { - "description": "`rate_limited` — see `Retry-After` and the `RateLimit` headers.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "`internal_error`.", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Problem" + "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "SessionAuth": [] } - } + ], + "summary": "List recent webhook calls", + "tags": [ + "Webhooks" + ] } }, - "/api/verify": { + "/api/webhooks/{id}/rotate-secret": { "post": { - "operationId": "verifyEmailAddress", - "tags": [ - "Verify" - ], - "summary": "Validate an email address", - "description": "Open endpoint (no auth required) that checks an email for syntax, MX records, disposable domains, and plus-addressing. Used by the marketing site verifier.", - "security": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyEmail" - } + "description": "Generate a new shared secret. Returns the new plaintext secret exactly once.", + "operationId": "rotateWebhookSecret", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } } - }, + ], "responses": { "200": { - "description": "Verification result", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerifyEmailResponse" + "$ref": "#/components/schemas/WebhookRotateSecretResponse" } } - } + }, + "description": "Secret rotated" }, "400": { - "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unauthorized — missing or invalid auth" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Forbidden — insufficient permissions or project disabled" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Resource not found" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Rate limit or billing limit exceeded" }, "500": { - "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } - } + }, + "description": "Internal server error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "SessionAuth": [] } - } + ], + "summary": "Rotate the webhook signing secret", + "tags": [ + "Webhooks" + ] } } }, + "servers": [ + { + "url": "https://api.sendly.now" + } + ], + "tags": [ + { + "description": "Send transactional email and inspect deliveries.", + "name": "Emails" + }, + { + "description": "Bulk sends to a list, segment, or filtered audience. Served by the `/api/v1` surface (bare payloads, RFC 9457 errors).", + "name": "Campaigns" + }, + { + "description": "Saved audiences — a `DYNAMIC` filter re-evaluated on read, or a `STATIC` membership list. Served by the `/api/v1` surface (bare payloads, RFC 9457 errors).", + "name": "Segments" + }, + { + "description": "Manage subscribers and per-contact custom data.", + "name": "Contacts" + }, + { + "description": "Self-serve list membership. These two endpoints accept sending-only keys so they can back a public subscribe/unsubscribe form; list management itself is dashboard-only.", + "name": "Lists" + }, + { + "description": "Register sending domains and manage SES verification.", + "name": "Domains" + }, + { + "description": "Reusable email templates referenced by send operations.", + "name": "Templates" + }, + { + "description": "User-managed outbound webhooks for email and contact events.", + "name": "Webhooks" + }, + { + "description": "Project-scoped suppression list. Hard bounces and complaints land here automatically.", + "name": "Suppression" + }, + { + "description": "Event-triggered automations and the contact runs through them. Served by the `/api/v1` surface (bare payloads, RFC 9457 errors).", + "name": "Workflows" + }, + { + "description": "Track custom contact events from your application.", + "name": "Events" + }, + { + "description": "Aggregate sending and engagement metrics. Every read is bounded to a window of at most 90 days and cached for 15 minutes.", + "name": "Analytics" + }, + { + "description": "Current email usage against the monthly and daily limits the platform enforces.", + "name": "Usage" + }, + { + "description": "Open email-validation endpoint (no auth required). Used by the marketing-site verifier.", + "name": "Verify" + } + ], "webhooks": {} }