# Inboxes

An inbox is a mailbox address (`local_part@domain`) that receives inbound mail and sends
outbound replies on behalf of your organization.

## The inbox object

```json
{
  "id": "inb_01k2n9v8qf3m7d5b0y6xz4rt1c",
  "object": "inbox",
  "address": "orders-bot@agentmail.ackuna.com",
  "local_part": "orders-bot",
  "domain": "agentmail.ackuna.com",
  "display_name": "Support desk",
  "status": "active",
  "thread_count": 42,
  "created_at": "2026-08-14T09:00:00.000Z"
}
```

`status` is `active` or `paused`. A paused inbox means your agent is not processing it:

- Inbound mail is **held**, not dropped. It is stored and threaded as usual, but no
  `inbox.message.received` webhook fires for it while the inbox is paused. When you
  resume the inbox, each held message gets its `inbox.message.received` webhook once and
  `held` becomes `false`. The webhooks are dispatched in arrival order, but deliveries
  are retried independently and mail that arrives just after the resume is not held, so
  they can reach you out of order: use `message.created_at`, which is when the mail
  arrived, to order them. The `thread` in such a webhook is the thread as it is at
  release, so its message count and `last_message_at` can include later mail.
- Held mail is readable through the API while the inbox is paused: thread and message
  endpoints return it with `held: true`, and the thread's `last_message_at` moves. If you
  poll instead of using webhooks and want pause to mean "not yet", skip messages with
  `held: true`.
- Held mail follows your plan's retention windows like any other mail. A message held past
  the body window is released without its body and attachments, and one held past the
  record window is deleted without being released.
- `inbox.message.failed` still fires while the inbox is paused: mail that cannot be parsed
  or stored is not held.
- Sending stops: `POST /v1/inboxes/{inbox}/messages` fails with 409 `inbox_paused`.

An archived inbox receives nothing; mail to its address is not delivered.

## List inboxes

```
GET /v1/inboxes?limit=25
Authorization: Bearer ak_...
```

```json
{
  "data": [
    {"id": "inb_01k2n9v8qf3m7d5b0y6xz4rt1c", "object": "inbox", "address": "orders-bot@agentmail.ackuna.com", "local_part": "orders-bot", "domain": "agentmail.ackuna.com", "display_name": null, "status": "active", "thread_count": 42, "created_at": "2026-08-14T09:00:00.000Z"}
  ],
  "next_cursor": "eyJpZCI6MTIzfQ",
  "has_more": false
}
```

Requires scope `inboxes:read`. Pass `?cursor=` from a previous page's `next_cursor` to
continue; `?limit=` is 1–100, default 25.

## Create an inbox

```
POST /v1/inboxes
Authorization: Bearer ak_...
Content-Type: application/json
Idempotency-Key: create-billing-inbox-1

{"username": "ledger-bot"}
```

```json
{
  "data": {
    "id": "inb_01k2n9v8qf3m7d5b0y6xz4rt2d",
    "object": "inbox",
    "address": "ledger-bot@agentmail.ackuna.com",
    "local_part": "ledger-bot",
    "domain": "agentmail.ackuna.com",
    "display_name": null,
    "status": "active",
    "thread_count": 0,
    "created_at": "2026-08-20T14:00:00.000Z"
  }
}
```

Requires scope `inboxes:write`. Fields:

| Field | Required | Notes |
|---|---|---|
| `username` | yes | Lowercased local part. `[a-z0-9]`, `.`, `_` or `-` internally, up to 64 characters. |
| `domain` | no | A `dom_...` id of an `active` [receiving domain](domains) you own, or the shared platform domain's name (`agentmail.ackuna.com`). Omitted → the inbox is hosted on `agentmail.ackuna.com`. The earlier shared domain `agents.ackuna.com` can no longer be chosen for new inboxes (422 `domain_not_allowed`); inboxes already on it keep sending and receiving. |

Failure modes specific to this endpoint: 422 `address_taken` if the address already
exists; 422 `local_part_not_allowed` if the username is reserved or contains a brand,
payment or sign-in term and the inbox would be on the shared platform domain (see below);
409 `domain_not_ready` if
`domain` names a domain that is not yet `active`; 422 `domain_not_allowed` if `domain`
names an older shared domain such as `agents.ackuna.com`, which keeps its existing inboxes
but takes no new ones;
403 `quota_exceeded` if the organization's inbox quota is exhausted. An inbox keeps the
domain it was created on.

On the shared platform domain some names are not available, because mail from
`agentmail.ackuna.com` goes out under Ackuna's own identity. A role address is refused as the
whole name (`postmaster`, `abuse`, `support`, `billing`, `noreply`, ...). A few role words
(`admin`, `root`, `postmaster`, `abuse`, `security`, `official`, `no-reply`, ...) are
refused anywhere in the name, as a word of their own, so `security-team` is refused while
`acme-support` is not. A name that contains a brand, payment or sign-in term (`paypal`,
`invoice`, `login`, ...) is refused, digit look-alikes such as `paypa1` included. The
error does not say which rule matched. On a [custom domain](domains) you own, any name is
allowed.

## Fetch an inbox

```
GET /v1/inboxes/inb_01k2n9v8qf3m7d5b0y6xz4rt1c
Authorization: Bearer ak_...
```

Requires scope `inboxes:read`. Returns the inbox object, or 404 `not_found` for an unknown
or cross-organization id.

## Pause, resume, or rename an inbox

```
PATCH /v1/inboxes/inb_01k2n9v8qf3m7d5b0y6xz4rt1c
Authorization: Bearer ak_...
Content-Type: application/json
Idempotency-Key: pause-billing-inbox-1

{"status": "paused"}
```

```json
{
  "data": {
    "id": "inb_01k2n9v8qf3m7d5b0y6xz4rt2d",
    "object": "inbox",
    "address": "ledger-bot@agentmail.ackuna.com",
    "local_part": "ledger-bot",
    "domain": "agentmail.ackuna.com",
    "display_name": null,
    "status": "paused",
    "thread_count": 0,
    "created_at": "2026-08-20T14:00:00.000Z"
  }
}
```

Requires scope `inboxes:write`. Both fields are optional and independent — send only what
you want to change:

| Field | Notes |
|---|---|
| `display_name` | Nullable string, up to 255 characters. It becomes the From name of the inbox's mail, so on the shared platform domain a name that contains a brand, payment or sign-in term is refused with 422 `display_name_not_allowed`. Role words are fine here ("Support desk"). On a custom domain any name is allowed. |
| `status` | `active` or `paused`. Pausing holds inbound mail and stops sending; resuming releases the held mail's webhooks (see [the inbox object](#the-inbox-object)). |

## Archive an inbox

```
DELETE /v1/inboxes/inb_01k2n9v8qf3m7d5b0y6xz4rt1c
Authorization: Bearer ak_...
Idempotency-Key: archive-billing-inbox-1
```

Responds `204 No Content`. Requires scope `inboxes:write`.

Archiving frees the inbox's quota slot and hides it everywhere — it stops receiving mail,
no longer appears in `GET /v1/inboxes`, and no longer resolves by id (a later `GET`,
`PATCH`, or `DELETE` for it returns 404 `not_found`). Its threads, messages, and
attachments follow the same retention as any other inbox: message bodies and attachments are
removed after 90 days and the messages themselves after 180 days, unless your plan sets
other windows.

The address is **not** released. The shared platform domain hosts inboxes for many
organizations, so a retired address stays reserved: it can never be re-created (a later
`POST /v1/inboxes` for the same address returns 422 `address_taken`), which stops a
different organization from claiming it and receiving the prior owner's mail.

Returns 404 `not_found` for an unknown or cross-organization id.

Next: [Threads](threads) group an inbox's messages into conversations, and
[Messages](messages) covers reading and sending.
