# Deliveries

> Part of the Cowliss docs. Markdown for every page: append `index.md` to its URL. Site map: /llms.txt

<!-- GENERATED by apps/docs/scripts/generate-api-reference.ts. Do not edit. Regenerate with `pnpm -F cowliss-docs generate`. -->

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](/reference/api/emails/) page.

## GET /v1/deliveries

List deliveries, newest first

**Auth:** Session token.

**Query parameters**

```json
{
  "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**

```json
{
  "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](/api-conventions/#errors).

## GET /v1/deliveries/stats

Delivery volume per app, and per day by outcome

**Auth:** Session token.

**Query parameters**

```json
{
  "days": 30,
  "appId": "string"
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `days` | integer | no | default `30`; 1 ≤ x ≤ 90 |
| `appId` | string | no | length 0–200 |

**Response 200**

```json
{
  "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](/api-conventions/#errors).

## GET /v1/deliveries/{id}

Fetch one delivery with its payload

**Auth:** Session token.

**Response 200**

```json
{
  "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": "ada@example.com"
      }
    }
  },
  "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](/api-conventions/#errors).

## 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](/api-conventions/#errors).

## 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**

```json
{
  "token": "string",
  "all": "1"
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | string | no |  |
| `all` | "1" | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## 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**

```json
{
  "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](/api-conventions/#errors).

## 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**

```json
{
  "token": "string"
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## 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**

```json
{
  "token": "string"
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## 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**

```json
{
  "token": "string",
  "all": "1"
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | string | no |  |
| `all` | "1" | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## 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**

```json
{
  "token": "string"
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## 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](/api-conventions/#errors).
