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

# The account's inbox

> Read, send, reply to, and archive mail on the address behind an account — one identity's mailbox, or every mailbox in your org as one feed.

Every account Sente holds is backed by an identity, and every identity owns a real mailbox on
`sente.run`. It receives what the third-party app sends — verification codes, security notices,
invoices, replies from humans — and it can send as that address. This page covers the mailbox
itself; for "block until the code arrives", see
[Receive verification codes](/email/verification-codes).

## The address

An identity's address is `<local-part>@sente.run`, and mail is live from the moment the identity
exists.

* **Auto-provisioned** (you called connect/register without an `identityId`): the local part is
  derived from the app's hostname — `https://github.com/login` → `github@sente.run`, with a short
  random suffix if that address is taken (`github-x7k2@sente.run`).
* **Chosen**: `POST /v1/identities` with `localPart`. A taken address returns `409` unless you pass
  `onConflict: "suffix"`.

<Note>
  Role addresses (`support`, `admin`, `security`, `abuse`, `postmaster`, and the rest of the RFC-2142
  set) are reserved for Sente's own mail and can't be allocated — `409 LOCAL_PART_RESERVED`. Only the
  exact word is reserved: `support-bot` is fine. Mail to an address with no matching identity, or to
  an identity you've deleted, is dropped.
</Note>

## List messages

Newest first, archived hidden, 50 by default.

<CodeGroup>
  ```bash CLI theme={null}
  sente inbox --identity github@sente.run --limit 20
  sente inbox --identity github@sente.run --direction inbound --since 2026-08-01T00:00:00Z
  ```

  ```ts TypeScript theme={null}
  const msgs = await sente.messages.list(identityId, {
    direction: "inbound",
    kind: "otp",              // "otp" | "magic_link" | "other"
    since: "2026-08-01T00:00:00Z",
    limit: 20,
  });

  for (const m of msgs) {
    console.log(m.createdAt, m.fromAddr, m.subject, m.annotation?.kind);
  }
  ```

  ```python Python theme={null}
  msgs = sente.messages.list(
      identity_id,
      direction="inbound",
      kind="otp",              # "otp" | "magic_link" | "other"
      since="2026-08-01T00:00:00Z",
      limit=20,
  )

  for m in msgs:
      print(m.created_at, m.from_addr, m.subject, m.annotation.kind if m.annotation else None)
  ```
</CodeGroup>

| Filter            | Values                           | Notes                                                                     |
| ----------------- | -------------------------------- | ------------------------------------------------------------------------- |
| `identityId`      | an identity id                   | **Omit it and the listing is org-wide** — every identity you own, merged. |
| `direction`       | `inbound` \| `outbound`          | Outbound rows are the mail this identity sent.                            |
| `kind`            | `otp` \| `magic_link` \| `other` | Matches the LLM annotation on inbound mail.                               |
| `since`           | ISO timestamp                    | Strictly after.                                                           |
| `limit`           | 1–200                            | Default 50.                                                               |
| `includeArchived` | the literal `"true"`             | Anything else is treated as false.                                        |

<Note>
  The org-wide feed is REST-only: both SDKs and the CLI take an identity as a required argument. For
  a dashboard or an audit sweep, call `GET /v1/messages` directly and group by the `identityId` each
  message carries.

  ```bash theme={null}
  curl "https://api.sente.run/v1/messages?kind=otp&limit=100" \
    -H "Authorization: Bearer $SENTE_API_TOKEN"
  ```
</Note>

## Read one message

A list row is already the full object, but you'll usually fetch by id after a
[`message.received` webhook](/events/webhooks) — that payload carries only the envelope, never the
body.

<CodeGroup>
  ```bash CLI theme={null}
  # --json is a global flag: it goes BEFORE the subcommand
  sente --json inbox --identity github@sente.run --limit 1
  ```

  ```ts TypeScript theme={null}
  const msg = await sente.messages.get("msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b");
  const { text, html } = msg.parsed as { text?: string; html?: string };
  ```

  ```python Python theme={null}
  msg = sente.messages.get("msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b")
  text = (msg.parsed or {}).get("text")
  html = (msg.parsed or {}).get("html")
  ```
</CodeGroup>

Bodies live under `parsed`: `text` and `html` for inbound mail, plus `messageId` (the sender's own
`Message-ID` header, used for threading). Outbound rows carry `{ text, html, inReplyTo }`.

<Warning>
  **Attachments are not exposed.** Only the text and HTML parts are parsed out; there is no
  attachment download endpoint. And email bodies are untrusted input — never hand a raw body to an
  agent that holds tools or credentials.
</Warning>

## Send

`text`, `html`, or both — at least one is required. One recipient per call; there is no cc, bcc, or
attachment support.

<CodeGroup>
  ```bash CLI theme={null}
  sente send \
    --identity github@sente.run \
    --to person@example.com \
    --subject "Build 4821 finished" \
    --text "All green. Report: https://…"
  ```

  ```ts TypeScript theme={null}
  const sent = await sente.messages.send(identityId, {
    to: "person@example.com",
    subject: "Build 4821 finished",
    text: "All green. Report: https://…",
  });
  console.log(sent.deliveryStatus); // "sent"
  ```

  ```python Python theme={null}
  sent = sente.messages.send(
      identity_id,
      to="person@example.com",
      subject="Build 4821 finished",
      text="All green. Report: https://…",
  )
  print(sent.delivery_status)  # "sent"
  ```
</CodeGroup>

Mail leaves as the identity's own address, DKIM-signed on `sente.run`, and is persisted as an
outbound message.

## Reply in-thread

Pass the Sente message id of the **inbound** message you're answering. Sente looks up that email's
original `Message-ID` and sets `In-Reply-To` and `References`, so the reply threads properly in the
recipient's client.

<CodeGroup>
  ```bash CLI theme={null}
  sente send \
    --identity github@sente.run \
    --to person@example.com \
    --subject "Re: your question" \
    --text "Confirmed — rerunning now." \
    --reply-to msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b
  ```

  ```ts TypeScript theme={null}
  await sente.messages.send(identityId, {
    to: inbound.fromAddr,
    subject: `Re: ${inbound.subject}`,
    text: "Confirmed — rerunning now.",
    inReplyTo: inbound.id,
  });
  ```

  ```python Python theme={null}
  sente.messages.send(
      identity_id,
      to=inbound.from_addr,
      subject=f"Re: {inbound.subject}",
      text="Confirmed — rerunning now.",
      in_reply_to=inbound.id,
  )
  ```
</CodeGroup>

<Note>
  The `inReplyTo` id must belong to the **same identity**. If it doesn't — or the original email
  carried no `Message-ID` — the mail still sends, just unthreaded. The recipient address is not
  derived from it either: set `to` yourself.
</Note>

## Delivery status

Outbound messages start at `sent` and are updated from the provider's delivery events. Poll the
message by id, or list with `direction=outbound`.

| `deliveryStatus` | Meaning                                                                                |
| ---------------- | -------------------------------------------------------------------------------------- |
| `sent`           | Accepted by the sending provider.                                                      |
| `delivered`      | Accepted by the recipient's server.                                                    |
| `bounced`        | Rejected. Repeated bounces to the same address hurt the shared domain — stop retrying. |
| `complained`     | Marked as spam by the recipient. Stop mailing that address.                            |
| `rejected`       | The provider refused to send it.                                                       |

Inbound messages have `deliveryStatus: null`.

## Archive

Archiving is a dismissal, not a delete: the message stays fetchable by id, drops out of listings
unless you pass `includeArchived=true`, and is **never matched by `messages/wait`**. Both calls are
idempotent and return the updated message.

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

curl -X POST https://api.sente.run/v1/messages/msg_5b0e8d7c6f5a4e3d2c1b0a9f8e7d6c5b/unarchive \
  -H "Authorization: Bearer $SENTE_API_TOKEN"
```

<Note>
  Archive/unarchive are REST-only — neither SDK nor the CLI wraps them yet. Archiving a message that
  a `wait` loop is expecting will make that wait time out; archive after you've handled it, not
  before.
</Note>

## React to new mail

| You want                                    | Use                                                                                                                                                                                         |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| One specific verification email             | [`waitForOtp` / `waitForMagicLink`](/email/verification-codes)                                                                                                                              |
| Every inbound email, pushed to your service | [Webhooks](/events/webhooks) — `message.received`                                                                                                                                           |
| Every inbound email, no public URL          | `sente listen --identity <ref>` (add `--exec` to run a command per message, or `--forward <url>` to replay the real webhook shape at localhost), or the SDKs' `messages.stream(identityId)` |

## Failure paths

| Status | Body                                                                    | What it means                                                                                  | What to do                                                                                |
| ------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `400`  | a list of validation issues                                             | No `text` and no `html`; empty or >998-char subject; malformed `to`; `limit` outside 1–200.    | Fix the field named in the issue list.                                                    |
| `404`  | `{"error": "identity not found"}`                                       | The `identityId` isn't in your org.                                                            | Check `GET /v1/identities`. Only sent when you pass one — the org-wide listing can't 404. |
| `404`  | `{"error": "not found"}`                                                | That message id isn't your org's.                                                              | Check the id.                                                                             |
| `429`  | `{"code": "SEND_LIMIT_EXCEEDED", "limit": 100}`                         | The rolling 24-hour send brake.                                                                | Wait for the window to roll. See [Limits](/trust/limits).                                 |
| `402`  | `{"code": "PLAN_EMAIL_LIMIT_EXCEEDED", "limit": 150, "planId": "free"}` | Your plan's monthly email cap. `402` because waiting won't clear it inside the billing window. | Upgrade, or email [support@sente.run](mailto:support@sente.run).                          |

<Warning>
  Every identity sends from the same shared domain. Deliverability is a shared resource: one tenant
  sending unsolicited bulk mail gets the domain flagged, and a flagged domain means verification
  emails stop landing for **everyone**. Sending is for the account's own correspondence — see the
  [Acceptable use policy](/trust/acceptable-use).
</Warning>

## Next steps

<CardGroup cols={3}>
  <Card title="Verification codes" icon="key-round" href="/email/verification-codes">
    Wait for an OTP or magic link and get just the value.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/events/webhooks">
    Push `message.received` to your service instead of polling.
  </Card>

  <Card title="Messages API" icon="terminal" href="/api-reference/messages">
    Every parameter, response, and error shape.
  </Card>
</CardGroup>
