# Look up sends

`GET /v1/sends`

Lists every blast email sent to one address, oldest first.

**Scope:** `blasts:read`

Answers "did we email this person, and with what": every send to the address with a `sent_at`, in any campaign whose blasts the key can read and where its member sees email addresses in full (elsewhere nothing matches, never `403`), ordered by `sent_at` (then `id`), oldest first, inside the window the first page fixes. Recipients not sent yet, suppressed or skipped were never emailed and are not listed. Rows have the same shape as a blast's sends.

Send the same `email` with every `cursor`; the cursor does not store the address.

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | `string` | Yes | The address, compared after trimming and lowercasing. |
| `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 to the address, oldest 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/sends?email=pat@example.com" \
  -H "Authorization: Bearer dei_your_key"
```

**JavaScript**

```js
const response = await fetch("https://development.agencydevworks.ai/v1/sends?email=pat@example.com", {
  headers: {
    "Authorization": "Bearer dei_your_key",
  },
});
const data = await response.json();
```

## Example response

**Status:** `200 OK`

```json
{
  "data": [
    {
      "id": "11111111-0000-4000-8000-000000000001",
      "blast_id": "22222222-0000-4000-8000-000000000001",
      "campaign_id": "cccccccc-0000-4000-8000-000000000001",
      "email": "pat@example.com",
      "lead_id": "aaaaaaaa-0000-4000-8000-000000000001",
      "status": "delivered",
      "sent_at": "2026-11-03T15:02:55Z",
      "from_email": "jordan@outreach.example.com",
      "subject": "Pat, your solar quote is ready",
      "bounce_code": null,
      "bounce_diagnostic": null,
      "replied": true,
      "first_reply_at": "2026-11-03T17:41:09Z",
      "opted_out": false
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

## Errors

| HTTP | Code | When |
| --- | --- | --- |
| 403 | `insufficient_scope` | The key does not have `blasts:read`. Checked before the query is validated. |
| 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 lookup, was changed, or was sent without its `email`. |
| 422 | `invalid_request` | `email` is missing (`query.email`, `missing`) or is not an email address (then `error.fields` is empty), or `limit` or `cursor` is invalid. |
| 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}/sends` · [List a blast's sends](/api/blasts/list-sends)
- `GET /v1/blasts/{blast_id}` · [Retrieve a blast](/api/blasts/get)
