# Long-poll a temporary inbox for mail

`GET /v1/inboxes/{id}/messages`

- Authentication: required (Bearer token)
- Required scope: `api.inboxes.view`

Returns the messages that arrived after `after`, optionally waiting up
to `wait` seconds for something new.

Each message is the SAME object the inbound webhook POSTs
(`schema_version: 1`, documented at
`https://developers.sendops.dev/api-reference/inbound-webhook`) with
`inbox_id` added — so an agent written against one reads the other
without a second parser.

**YOUR CLIENT'S REQUEST DEADLINE MUST EXCEED `wait`.** A 20-second
poll issued on a client with a 10-second timeout hangs up before the
server answers, every time, and looks exactly like an inbox that never
receives anything. Set the client timeout to `wait` plus a few seconds.
There is nothing the server can do about this from its side.

**Every poll costs one request** against your per-key rate limit,
counted on entry and not refunded for the time spent waiting. A
20-second loop is 3 requests a minute, which is nothing for a live key.
A `sk_test_` key's budget can be as low as 1 a minute, which makes a
tight loop unusable from a test credential — use a live key, or poll
less often.

**The cursor.** `cursor` in the response is the number of messages you
have now seen; send it as `after` next time. It is an integer rather
than an opaque token on purpose: if you lose it, send `0` and you get
the whole inbox back rather than an error.

**Presigned URLs expire with the inbox.** `raw_url`,
`message.attachments[].url`, `message.text_body_url` and
`message.html_body_url` are signed for whatever is left of the inbox's
life and are minted fresh on every response. Fetch what you need before
`expires_at`; afterwards the objects are deleted, not merely
unreachable.

**Bodies may be out of line.** A body over 256 KB is stored as an
object and `text_body` / `html_body` is empty with `text_body_url` /
`html_body_url` set instead. Handle both.

**The sender check is address-only.** A restricted inbox compares the
`From:` header address and the SES envelope sender against
`allowed_senders`; either matching passes. It is NOT an authentication
check — SPF and DKIM can be forged by anyone controlling the headers.
`verdicts.spf`, `verdicts.dkim` and `verdicts.dmarc` are in every
payload precisely so you can judge that yourself. Treat the address as
a filter and the verdicts as the evidence.

**Statuses.** `410` when the inbox has expired — you have had
`expires_at` in every response for this id, so this is the expected end
of a poll loop rather than an error to investigate. `404` when the
inbox was deleted, belongs to somebody else, or never existed: three
facts deliberately not distinguished.

**PII.** This route returns whole messages in the clear: the sender's
address, every recipient, the subject, the body text and HTML, and
links to every attachment. Nothing is masked and no further scope
unmasks anything.

## Path parameters

- `id` (string<uuid>, required) — The inbox's UUID, exactly as returned by `POST /v1/inboxes`. A value that is not a UUID is a 404 rather than a 422 — it names nothing, and saying "that is not a valid UUID" would confirm the format of ids that do exist.

## Query parameters

- `after` (integer<int64>, optional) — The `cursor` from your previous page. `0`, or omitted, returns everything the inbox holds. Negative is 422.
- `wait` (integer, optional) — Seconds to hold the connection open waiting for a new message, 0 to 25. `0` (the default) answers immediately with whatever is there. Out of range is 422 rather than clamped — a wait silently shortened is indistinguishable from mail that never arrived. The hold also stops at `expires_at`, whichever comes first.

## Example request

```bash
curl 'https://api.sendops.dev/v1/inboxes/00000000-0000-0000-0000-000000000000/messages' \
  -H "Authorization: Bearer $SENDOPS_API_KEY"
```

## Responses

### 200 — The messages after the cursor. An EMPTY list is the ordinary answer to a poll that timed out and is not an error — poll again with the returned cursor.

Content type: `application/json`

```json
{
  "messages": [
    {
      "schema_version": 0,
      "message_id": "string",
      "inbox_id": "00000000-0000-0000-0000-000000000000",
      "domain": "string",
      "recipients": [
        "string"
      ],
      "received_at": "2026-05-17T20:00:00Z",
      "verdicts": {
        "spam": "",
        "virus": "",
        "spf": "",
        "dkim": "",
        "dmarc": ""
      },
      "raw_url": "https://example.com",
      "message": {
        "from": {
          "name": "string",
          "address": "string"
        },
        "to": [
          {
            "name": "string",
            "address": "string"
          }
        ],
        "cc": [
          {
            "name": "string",
            "address": "string"
          }
        ],
        "reply_to": [
          {
            "name": "string",
            "address": "string"
          }
        ],
        "subject": "string",
        "date": "string",
        "message_id": "string",
        "in_reply_to": "string",
        "reply_to_message_ids": [
          "string"
        ],
        "is_auto_reply": true,
        "text_body": "string",
        "html_body": "string",
        "text_body_url": "https://example.com",
        "html_body_url": "https://example.com",
        "attachments": [
          {
            "index": 0,
            "filename": "string",
            "content_type": "string",
            "size": 0,
            "content_id": "string",
            "inline": true,
            "url": "https://example.com"
          }
        ],
        "headers": {},
        "verdicts": {
          "spam": "",
          "virus": "",
          "spf": "",
          "dkim": "",
          "dmarc": ""
        },
        "defects": [
          "string"
        ]
      }
    }
  ],
  "cursor": 0,
  "expires_at": "2026-05-17T20:00:00Z"
}
```

### 401 — Missing, malformed, or unknown API key

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 403 — Key lacks the required scope or plan limit violated

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 404 — Resource not found

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 410 — The inbox expired and its messages have been deleted. The `code` is
`gone`.

This is the expected end of a poll loop, not an error to investigate:
`expires_at` has been in every response for this id since it was
minted. Mint another inbox to receive more mail.

A DELETED inbox answers `404` instead — deletion is the owner asking us
to forget it, so we do not then confirm it existed.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 422 — A query parameter, path value or body field failed validation.

The body is a `validation_failed` Problem. When the refusal is about a
reference — a segment key, template slug, topic or attribute name the
organization does not have — it additionally carries `validation_code`,
`field`, `line`/`column`, `missing`, `candidates` and `next_step`, so a
client can correct the call without a second round of guessing. See
`ValidationProblem`.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ],
  "validation_code": "invalid_syntax",
  "field": "string",
  "line": 0,
  "column": 0,
  "missing": [
    {
      "kind": "segment",
      "key": "string"
    }
  ],
  "candidates": {},
  "next_step": "string"
}
```

### 429 — Per-org rate limit exceeded

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 500 — Unexpected server-side failure. The `code` is `internal_error`. The
`request_id` field can be quoted to SendOps support to investigate.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```
