> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sente.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Messages

> Read an identity's inbox, wait for verification codes, and send email.

Messages are the emails an [identity](/api-reference/identities) receives and sends. Every inbound message is annotated server-side as `otp`, `magic_link`, or `other`, with the extracted code or link — so an agent can wait for exactly the verification email it needs. See [Receive verification codes](/guides/receive-verification-codes).

## The message object

```json theme={null}
{
  "id": "msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b",
  "identityId": "idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5",
  "direction": "inbound",
  "fromAddr": "no-reply@example-app.com",
  "toAddr": "acme-qa@sente.run",
  "subject": "Your verification code",
  "s3Key": "inbound/abc123",
  "parsed": {
    "text": "Your code is 481920. It expires in 10 minutes.",
    "html": "<p>Your code is <b>481920</b>…</p>",
    "messageId": "<20260720T091500.123@example-app.com>"
  },
  "annotation": {
    "kind": "otp",
    "code": "481920",
    "link": null,
    "confidence": 0.98
  },
  "deliveryStatus": null,
  "sesMessageId": "0100019...",
  "archived": false,
  "createdAt": "2026-07-20T09:15:02.114Z"
}
```

| Field            | Description                                                                                                                                                                                                                |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `direction`      | `inbound` or `outbound`.                                                                                                                                                                                                   |
| `parsed`         | Parsed email content. For outbound messages: `{ text, html, inReplyTo }`.                                                                                                                                                  |
| `annotation`     | Inbound only. `kind` is `otp`, `magic_link`, or `other`; `code`/`link` hold the extracted value or `null`. Annotation runs asynchronously right after receipt; a just-arrived message can briefly have `annotation: null`. |
| `deliveryStatus` | Outbound only — `sent`, then updated from provider delivery/bounce/complaint events.                                                                                                                                       |
| `archived`       | Archived messages are hidden from lists unless `includeArchived=true`.                                                                                                                                                     |

## List messages

`GET /v1/messages`

| Query param       | Type                             | Required | Description                          |
| ----------------- | -------------------------------- | -------- | ------------------------------------ |
| `identityId`      | string                           | yes      | The identity whose messages to list. |
| `since`           | ISO date                         | no       | Only messages after this timestamp.  |
| `direction`       | `inbound` \| `outbound`          | no       | Filter by direction.                 |
| `kind`            | `otp` \| `magic_link` \| `other` | no       | Filter by annotation kind.           |
| `limit`           | int 1–200                        | no       | Default 50. Newest first.            |
| `includeArchived` | `"true"`                         | no       | Include archived messages.           |

```bash theme={null}
curl "https://api.sente.run/v1/messages?identityId=idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5&kind=otp&limit=5" \
  -H "Authorization: Bearer sk_sente_..."
```

Response `200`: an array of message objects, newest first.

`404 {"error": "identity not found"}` if `identityId` isn't your org's.

## Wait for a message (long-poll)

`GET /v1/messages/wait`

Holds the HTTP request open until a matching message arrives, then returns it. This is the primitive behind "wait for the OTP".

| Query param  | Type                             | Required | Description                                                                                                                                   |
| ------------ | -------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `identityId` | string                           | yes      | The identity to watch.                                                                                                                        |
| `since`      | ISO date                         | no       | Only match messages after this timestamp. **Defaults to now − 60 s** so a verification email that landed just before you called isn't missed. |
| `kind`       | `otp` \| `magic_link` \| `other` | no       | Only match messages annotated with this kind. With `kind` set, only annotated messages match — you never race the annotator.                  |
| `timeout`    | int 1–60                         | no       | Seconds to hold the poll. Default 25.                                                                                                         |

Returns `200` with the oldest matching message, or **`204` with no body** when the timeout elapses with no match. On `204`, poll again — reuse the same `since` so nothing is skipped.

<Tip>
  The robust pattern: stamp `since` yourself **before** triggering the action that sends the email, then pass it explicitly. The 60-second default lookback covers most timing gaps, but an explicit `since` is exact — and lets you re-poll without matching an old code.
</Tip>

```bash theme={null}
SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# ...trigger the signup/login that sends the code...
curl "https://api.sente.run/v1/messages/wait?identityId=idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5&kind=otp&since=$SINCE&timeout=60" \
  -H "Authorization: Bearer sk_sente_..."
```

Response `200` (a message object; the code is at `annotation.code`):

```json theme={null}
{
  "id": "msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b",
  "identityId": "idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5",
  "direction": "inbound",
  "fromAddr": "no-reply@example-app.com",
  "toAddr": "acme-qa@sente.run",
  "subject": "Your verification code",
  "annotation": { "kind": "otp", "code": "481920", "link": null, "confidence": 0.98 },
  "archived": false,
  "createdAt": "2026-07-20T09:15:02.114Z"
}
```

## Get a message

`GET /v1/messages/:id`

```bash theme={null}
curl https://api.sente.run/v1/messages/msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b \
  -H "Authorization: Bearer sk_sente_..."
```

Response `200`: the full message object. `404 {"error": "not found"}` otherwise.

## Send an email

`POST /v1/messages`

Sends as the identity's address (DKIM-signed) and persists an outbound message.

| Field        | Type         | Required     | Description                                                                                                                           |
| ------------ | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `identityId` | string       | yes          | The sending identity.                                                                                                                 |
| `to`         | email        | yes          | Single recipient.                                                                                                                     |
| `subject`    | string 1–998 | yes          | Subject line.                                                                                                                         |
| `text`       | string       | text or html | Plain-text body.                                                                                                                      |
| `html`       | string       | text or html | HTML body.                                                                                                                            |
| `inReplyTo`  | string       | no           | The Sente message id of an inbound email to reply to. Sets `In-Reply-To`/`References` so the reply threads in the recipient's client. |

```bash theme={null}
curl -X POST https://api.sente.run/v1/messages \
  -H "Authorization: Bearer sk_sente_..." \
  -H "content-type: application/json" \
  -d '{
    "identityId": "idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5",
    "to": "person@example.com",
    "subject": "Re: your question",
    "text": "Confirmed — the report is attached to the dashboard.",
    "inReplyTo": "msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b"
  }'
```

Response `201`:

```json theme={null}
{
  "id": "msg_7a2b3c4d5e6f708192a3b4c5d6e7f809",
  "identityId": "idt_9f1c2ab34d5e46f7a8b9c0d1e2f3a4b5",
  "direction": "outbound",
  "fromAddr": "acme-qa@sente.run",
  "toAddr": "person@example.com",
  "subject": "Re: your question",
  "parsed": { "text": "Confirmed — the report is attached to the dashboard.", "html": null, "inReplyTo": "<20260720T091500.123@example-app.com>" },
  "annotation": null,
  "deliveryStatus": "sent",
  "sesMessageId": "0100019...",
  "archived": false,
  "createdAt": "2026-07-20T10:02:41.007Z"
}
```

Errors:

| Status | Body                                                                                    | When                                                       |
| ------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `429`  | `{"error": "...", "code": "SEND_LIMIT_EXCEEDED", "limit": 100}`                         | Daily per-org send cap. Retry after the 24 h window rolls. |
| `402`  | `{"error": "...", "code": "PLAN_EMAIL_LIMIT_EXCEEDED", "limit": 150, "planId": "free"}` | Monthly plan cap — fixed by upgrading.                     |
| `404`  | `{"error": "identity not found"}`                                                       | Not your org's identity.                                   |

## Archive / unarchive

`POST /v1/messages/:id/archive` · `POST /v1/messages/:id/unarchive`

Archived messages drop out of `GET /v1/messages` unless `includeArchived=true`, and are never matched by `wait`. Idempotent; returns the updated message object.

```bash theme={null}
curl -X POST https://api.sente.run/v1/messages/msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b/archive \
  -H "Authorization: Bearer sk_sente_..."
```

Response `200`: the message object with `"archived": true`.
