Skip to content
Cowliss is in beta: features and pricing may change.

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.

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.

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.

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",
"replyTo": "[email protected]"
}
}
},
"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.

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.

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.

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.

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.

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.

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.

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.

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.