Deliveries
Each send attempt creates a delivery. A delivery is sent then delivered, bounced or complained, or it is failed, a typed skipped_*, or a dry-run would_*. Status precedence ensures that a late delivered status never overwrites bounced, and a complaint outranks a bounce. For webhooks, a successful response confirms delivery, making delivered terminal. Public routes for delivery feedback and RFC 8058 one-click unsubscribe verify signatures instead of sessions, answering per the caller’s convention rather than the standard envelope. Address verification is on the emails page.
GET /v1/deliveries
Section titled “GET /v1/deliveries”List deliveries, newest first
Auth: Session token.
Query parameters
{ "cursor": "string", "limit": 50, "direction": "desc", "q": "string", "status": "sent", "automation": "string", "step": "string", "channel": "email", "kind": "automation", "appId": "string", "profileId": "string", "executionId": "string", "since": "2026-01-15T09:30:00.000Z"}| Field | Type | Required | Constraints |
|---|---|---|---|
cursor |
string | no | |
limit |
integer | no | default 50; 1 ≤ x ≤ 100 |
direction |
“asc” | “desc” | no | default "desc" |
q |
string | no | length 0–200 |
status |
“sent” | “delivered” | “bounced” | “complained” | “failed” | “skipped_disabled” | “skipped_paused” | “skipped_suspended” | “skipped_suppressed” | “skipped_quota” | “skipped_unknown_purpose” | “skipped_frequency_cap” | “skipped_consent” | “skipped_domain” | “skipped_ssrf” | “skipped_spam” | “would_send” | “would_skip_disabled” | “would_skip_paused” | “would_skip_suspended” | “would_skip_suppressed” | “would_skip_quota” | “would_skip_unknown_purpose” | “would_skip_frequency_cap” | “would_skip_consent” | “would_skip_domain” | “would_skip_ssrf” | “would_skip_spam” | no | |
automation |
string | no | length 1–200 |
step |
string | no | length 1–200 |
channel |
“email” | “webhook” | no | |
kind |
“automation” | “test” | “api” | “broadcast” | no | |
appId |
string | no | length 0–200 |
profileId |
string | no | length 1–200 |
executionId |
string | no | length 1–200 |
since |
string (date-time) | no |
Response 200
{ "data": [ { "id": "string", "orgId": "string", "profileId": "string", "automation": "string", "executionId": "string", "appId": "string", "broadcastId": "string", "kind": "automation", "step": "string", "channel": "email", "status": "sent", "dryRun": true, "recipient": "string", "providerId": "string", "error": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "sentAt": "2026-01-15T09:30:00.000Z", "settledAt": "2026-01-15T09:30:00.000Z", "openedAt": "2026-01-15T09:30:00.000Z", "openCount": 42, "spamScore": 42, "spamSymbols": [ "string" ] } ], "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object[] | yes | |
data[] |
object | yes | |
data[].id |
string | yes | |
data[].orgId |
string | yes | |
data[].profileId |
string | null | yes | |
data[].automation |
string | null | yes | |
data[].executionId |
string | null | yes | |
data[].appId |
string | null | yes | |
data[].broadcastId |
string | null | yes | |
data[].kind |
“automation” | “test” | “api” | “broadcast” | yes | |
data[].step |
string | yes | |
data[].channel |
“email” | “webhook” | yes | |
data[].status |
“sent” | “delivered” | “bounced” | “complained” | “failed” | “skipped_disabled” | “skipped_paused” | “skipped_suspended” | “skipped_suppressed” | “skipped_quota” | “skipped_unknown_purpose” | “skipped_frequency_cap” | “skipped_consent” | “skipped_domain” | “skipped_ssrf” | “skipped_spam” | “would_send” | “would_skip_disabled” | “would_skip_paused” | “would_skip_suspended” | “would_skip_suppressed” | “would_skip_quota” | “would_skip_unknown_purpose” | “would_skip_frequency_cap” | “would_skip_consent” | “would_skip_domain” | “would_skip_ssrf” | “would_skip_spam” | yes | |
data[].dryRun |
boolean | yes | |
data[].recipient |
string | null | yes | |
data[].providerId |
string | null | yes | |
data[].error |
string | null | yes | |
data[].createdAt |
string (date-time) | yes | |
data[].updatedAt |
string (date-time) | yes | |
data[].sentAt |
string (date-time) | null | yes | |
data[].settledAt |
string (date-time) | null | yes | |
data[].openedAt |
string (date-time) | null | yes | |
data[].openCount |
integer | yes | -2147483648 ≤ x ≤ 2147483647 |
data[].spamScore |
number | null | yes | |
data[].spamSymbols |
string[] | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/deliveries/stats
Section titled “GET /v1/deliveries/stats”Delivery volume per app, and per day by outcome
Auth: Session token.
Query parameters
{ "days": 30, "appId": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
days |
integer | no | default 30; 1 ≤ x ≤ 90 |
appId |
string | no | length 0–200 |
Response 200
{ "data": { "days": 1, "volume": [ { "day": "string", "delivered": 42, "bounced": 42, "failed": 42 } ], "total": 42, "byApp": [ { "appId": "string", "name": "string", "count": 42 } ] }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.days |
integer | yes | 1 ≤ x ≤ 90 |
data.volume |
object[] | yes | |
data.volume[] |
object | yes | |
data.volume[].day |
string | yes | |
data.volume[].delivered |
integer | yes | 0 ≤ x ≤ ∞ |
data.volume[].bounced |
integer | yes | 0 ≤ x ≤ ∞ |
data.volume[].failed |
integer | yes | 0 ≤ x ≤ ∞ |
data.total |
integer | yes | 0 ≤ x ≤ ∞ |
data.byApp |
object[] | yes | |
data.byApp[] |
object | yes | |
data.byApp[].appId |
string | yes | |
data.byApp[].name |
string | yes | |
data.byApp[].count |
integer | yes | 0 ≤ x ≤ ∞ |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/deliveries/{id}
Section titled “GET /v1/deliveries/{id}”Fetch one delivery with its payload
Auth: Session token.
Response 200
{ "data": { "id": "string", "orgId": "string", "profileId": "string", "automation": "string", "executionId": "string", "appId": "string", "broadcastId": "string", "kind": "automation", "step": "string", "channel": "email", "status": "sent", "dryRun": true, "recipient": "string", "providerId": "string", "error": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "sentAt": "2026-01-15T09:30:00.000Z", "settledAt": "2026-01-15T09:30:00.000Z", "openedAt": "2026-01-15T09:30:00.000Z", "openCount": 42, "spamScore": 42, "spamSymbols": [ "string" ], "payload": { "rendered": { "subject": "string", "html": "string", "text": "string", } } }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.profileId |
string | null | yes | |
data.automation |
string | null | yes | |
data.executionId |
string | null | yes | |
data.appId |
string | null | yes | |
data.broadcastId |
string | null | yes | |
data.kind |
“automation” | “test” | “api” | “broadcast” | yes | |
data.step |
string | yes | |
data.channel |
“email” | “webhook” | yes | |
data.status |
“sent” | “delivered” | “bounced” | “complained” | “failed” | “skipped_disabled” | “skipped_paused” | “skipped_suspended” | “skipped_suppressed” | “skipped_quota” | “skipped_unknown_purpose” | “skipped_frequency_cap” | “skipped_consent” | “skipped_domain” | “skipped_ssrf” | “skipped_spam” | “would_send” | “would_skip_disabled” | “would_skip_paused” | “would_skip_suspended” | “would_skip_suppressed” | “would_skip_quota” | “would_skip_unknown_purpose” | “would_skip_frequency_cap” | “would_skip_consent” | “would_skip_domain” | “would_skip_ssrf” | “would_skip_spam” | yes | |
data.dryRun |
boolean | yes | |
data.recipient |
string | null | yes | |
data.providerId |
string | null | yes | |
data.error |
string | null | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.sentAt |
string (date-time) | null | yes | |
data.settledAt |
string (date-time) | null | yes | |
data.openedAt |
string (date-time) | null | yes | |
data.openCount |
integer | yes | -2147483648 ≤ x ≤ 2147483647 |
data.spamScore |
number | null | yes | |
data.spamSymbols |
string[] | null | yes | |
data.payload |
record<string, any> | yes | |
data.payload.rendered |
object | no | |
data.payload.rendered.subject |
string | yes | |
data.payload.rendered.html |
string | yes | |
data.payload.rendered.text |
string | yes | |
data.payload.rendered.replyTo |
string (email) | no | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/feedback/ses
Section titled “POST /v1/feedback/ses”SES delivery feedback via SNS (public)
Updates delivery statuses, adds bounced and complained addresses to your suppression list, and revokes consent on complaints. Open tracking events are rejected because open tracking is disabled. Subscription confirmation requests from the platform’s email provider are confirmed automatically.
Auth: None: verifies the caller’s own signature/token.
Errors follow the shared envelope and code table.
POST /v1/unsubscribe
Section titled “POST /v1/unsubscribe”One-click unsubscribe (public)
Revokes consent for the purpose the email was sent under on the recipient’s profile, which turns off every name under it when the purpose is a root. Setting all to 1 revokes both roots, marketing and transactional, and so every name under them; bare transactional mail such as receipts still arrives.
Auth: None: verifies the caller’s own signature/token.
Query parameters
{ "token": "string", "all": "1"}| Field | Type | Required | Constraints |
|---|---|---|---|
token |
string | no | |
all |
“1” | no |
Errors follow the shared envelope and code table.
GET /v1/unsubscribe
Section titled “GET /v1/unsubscribe”Unsubscribe confirmation page (public)
Changes nothing: it names what the link would turn off, and its Unsubscribe button POSTs to the same URL. Mail scanners follow links, so only the POST writes. Its Keep me subscribed link sets keep to 1, which shows that nothing changed.
Auth: None: verifies the caller’s own signature/token.
Query parameters
{ "token": "string", "all": "1", "keep": "1"}| Field | Type | Required | Constraints |
|---|---|---|---|
token |
string | no | |
all |
“1” | no | |
keep |
“1” | no |
Errors follow the shared envelope and code table.
POST /v1/unsubscribe/undo
Section titled “POST /v1/unsubscribe/undo”Undo an unsubscribe (public)
The Resubscribe button on the page after an unsubscribe. Its signed token expires after 48 hours and names exactly what that unsubscribe turned off, which this puts back and nothing else: a purpose that was off before stays off. Pressing it again changes nothing.
Auth: None: verifies the caller’s own signature/token.
Query parameters
{ "token": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
token |
string | no |
Errors follow the shared envelope and code table.
GET /v1/preferences
Section titled “GET /v1/preferences”Email preferences page (public)
One recipient’s switches, one for each purpose their app declares, with its label and description. Changes nothing. The token comes from a preferencesUrl prop or footer link in an email (30 days) or from POST /v1/preferences-links (one hour).
Auth: None: verifies the caller’s own signature/token.
Query parameters
{ "token": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
token |
string | no |
Errors follow the shared envelope and code table.
POST /v1/preferences
Section titled “POST /v1/preferences”Save email preferences (public)
Saves the switches the page showed: each one turned off writes system.consent_revoked and each one turned on writes system.consent_granted, both with reason preferences. Setting all to 1 leaves everything instead, exactly like the unsubscribe link’s, with the same Resubscribe undo.
Auth: None: verifies the caller’s own signature/token.
Query parameters
{ "token": "string", "all": "1"}| Field | Type | Required | Constraints |
|---|---|---|---|
token |
string | no | |
all |
“1” | no |
Errors follow the shared envelope and code table.
POST /v1/unsubscribe/reason
Section titled “POST /v1/unsubscribe/reason”Say why you unsubscribed (public)
The optional Tell us why form on the page after an unsubscribe, under the same signed token as its Resubscribe button. Records the answer as a system.unsubscribe_feedback event on the recipient’s profile, once per unsubscribe, and never changes consent.
Auth: None: verifies the caller’s own signature/token.
Query parameters
{ "token": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
token |
string | no |
Errors follow the shared envelope and code table.
GET /v1/o/{token}
Section titled “GET /v1/o/{token}”Broadcast email open (public)
The image a broadcast email loads when it is opened. The first load records the open and writes system.email_opened; later loads only add to the count. Always answers a 1x1 transparent GIF that is never cached, whatever the link.
Auth: None: verifies the caller’s own signature/token.
Errors follow the shared envelope and code table.