# List a blast's sends

`GET /v1/blasts/{blast_id}/sends`

Returns one row per recipient of a blast, the most recently added first.

**Scope:** `blasts:read`

Every recipient is listed, sent or not, ordered by when it was added to the blast (then `id`), newest first, inside the window the first page fixes. The order never uses `sent_at`, so a row does not move between pages while the blast sends; each row carries its own `sent_at`.

`email` finds one recipient. When the member does not see email addresses in full in the blast's campaign it matches nothing: an empty page, never `403`. The cursor does not store the address, so send the same `email` with every `cursor` of that walk: leaving it out or changing it is `422 invalid_cursor`.

Bounced rows carry `bounce_code` and a short `bounce_diagnostic`. Email bodies are never returned.

## Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `blast_id` | `UUID` | Yes | The blast. |

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | `string` | No | Only sends with this status. One of `pending`, `processing`, `sent`, `delivered`, `failed`, `bounced`, `suppressed`, `skipped`, `cancelled`. |
| `email` | `string` | No | Only the recipient with this address, compared after trimming and lowercasing. |
| `since` | `timestamp` | No | Only sends whose `sent_at` is at or after this time, which leaves out recipients not sent yet. ISO 8601; a time without an offset is read as UTC (URL-encode `+` as `%2B`). |
| `limit` | `integer` | No | Sends per page. 1 to 500. Default: `100`. |
| `cursor` | `string` | No | The `next_cursor` of the previous page, unchanged. The cursor does not store the address, so send the same `email` with it. Up to 4,096 characters. |

## Response

**Status:** `200 OK`

| Name | Type | Description |
| --- | --- | --- |
| `data` | `array of objects` | Sends, the most recently added first. |
| `data.id` | `UUID` | The send: one recipient of one blast. |
| `data.blast_id` | `UUID` | Its blast. |
| `data.campaign_id` | `UUID` | The blast's campaign. |
| `data.email` | `string \| null` | The recipient address; `null` when the member's role hides email, masked (`p***@example.net`) when it masks it. |
| `data.lead_id` | `UUID \| null` | The lead it went to, when the recipient is a lead. |
| `data.status` | `string` | The send status. `pending` and `processing` have not gone out yet; `suppressed`, `skipped` and `cancelled` never will. One of `pending`, `processing`, `sent`, `delivered`, `failed`, `bounced`, `suppressed`, `skipped`, `cancelled`. |
| `data.sent_at` | `timestamp \| null` | When it went out (RFC 3339, UTC); `null` while not sent. |
| `data.from_email` | `string \| null` | The mailbox it was sent from, with the same email visibility as `email`. |
| `data.subject` | `string \| null` | The subject as the recipient got it; `Restricted` when the member's role masks messages. |
| `data.bounce_code` | `string \| null` | Only on `bounced` sends: the bounce code, for example `5.1.1`. |
| `data.bounce_diagnostic` | `string \| null` | Only on `bounced` sends: the receiving server's message, whitespace collapsed and cut to 200 characters. It quotes the recipient address, so it follows the stricter of email and messages visibility: `null` when either is hidden, `Restricted` when either is masked. |
| `data.replied` | `boolean` | `true` when DialEcho matched a reply to this send. |
| `data.first_reply_at` | `timestamp \| null` | When the first matched reply came. |
| `data.opted_out` | `boolean` | `true` when the recipient opted out in a reply to this send or the provider reported an unsubscribe. |
| `next_cursor` | `string \| null` | Pass it back as `cursor` for the next page; `null` when there is none. |
| `has_more` | `boolean` | `true` while `next_cursor` is set. |

## Example request

**curl**

```bash
curl "https://development.agencydevworks.ai/v1/blasts/22222222-0000-4000-8000-000000000001/sends?status=bounced&limit=1" \
  -H "Authorization: Bearer dei_your_key"
```

**JavaScript**

```js
const response = await fetch("https://development.agencydevworks.ai/v1/blasts/22222222-0000-4000-8000-000000000001/sends?status=bounced&limit=1", {
  headers: {
    "Authorization": "Bearer dei_your_key",
  },
});
const data = await response.json();
```

## Example response

**Status:** `200 OK`

```json
{
  "data": [
    {
      "id": "11111111-0000-4000-8000-000000000002",
      "blast_id": "22222222-0000-4000-8000-000000000001",
      "campaign_id": "cccccccc-0000-4000-8000-000000000001",
      "email": "gone@example.net",
      "lead_id": "aaaaaaaa-0000-4000-8000-000000000002",
      "status": "bounced",
      "sent_at": "2026-11-03T15:04:10Z",
      "from_email": "jordan@outreach.example.com",
      "subject": "Sam, your solar quote is ready",
      "bounce_code": "5.1.1",
      "bounce_diagnostic": "550 5.1.1 <gone@example.net>: Recipient address rejected: User unknown",
      "replied": false,
      "first_reply_at": null,
      "opted_out": false
    }
  ],
  "next_cursor": "v2.exampleOpaqueCursor",
  "has_more": true
}
```

## Errors

| HTTP | Code | When |
| --- | --- | --- |
| 403 | `insufficient_scope` | The key does not have `blasts:read`. Checked before the query is validated. |
| 404 | `not_found` | The blast does not exist or the key cannot read it: another organization, a campaign outside the key, or one where the key's member cannot use email blasts. |
| 409 | `resync_required` | The cursor expired (after 30 days) or the key's or its member's access changed since it was issued. Start again without a cursor. |
| 422 | `invalid_cursor` | The cursor was not issued to this key for this blast, was changed, or was sent with different filters or without its `email`. |
| 422 | `invalid_request` | `blast_id` is not a UUID, a filter is invalid (`error.fields` names the first: `status`, `since`, `query.limit`, `query.cursor`), or `email` is not an email address (then `error.fields` is empty). |
| 503 | `dependency_unavailable` | Blast data or the access check is temporarily unavailable. Retry with backoff. |

Every endpoint can also answer `401 invalid_api_key`, `429 rate_limited` and `503 dependency_unavailable`. See [Errors](/api/#errors) in the overview.

## Related endpoints

- `GET /v1/blasts/{blast_id}` · [Retrieve a blast](/api/blasts/get)
- `GET /v1/sends` · [Look up sends](/api/blasts/lookup-sends)
