# Mailboxes

> Every mailboxes endpoint, generated from the API's own schemas.

Base URL `https://pidgeon.ai/api/v1`. Every request carries `Authorization: Bearer pgn_live_…`. The machine-readable version of this page is https://pidgeon.ai/openapi.json.

## GET /mailboxes

List the folders of one mailbox

The shape of a sidebar. `identity_id` is required — there is no cross-mailbox view, because merging the folders of several addresses would invent a mailbox nobody has.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `identity_id` | string | yes |  |

**Responses**

- `200` The folders.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /mailboxes/changes

What changed since you last looked

Per mailbox, not per folder: a message moving from the inbox to the archive is one event, and the row says where it went. Arrivals are included, so this is the only thing to poll. `since` is the `sync_cursor` from a listing or the `next_cursor` from a previous delta — **not** a page cursor, which walks a different order and is refused rather than guessed at. A cursor older than the window in which we keep the record of what was deleted returns `410 resync`, because an empty delta would be indistinguishable from "nothing changed".

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `identity_id` | uuid | yes |  |
| `since` | string | yes |  |
| `limit` | number | no |  |

**Responses**

- `200` Everything that happened, oldest first.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `410` `resync`: the cursor is older than the record of what was deleted. List the mailbox again and take its `sync_cursor`.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /mailboxes/{folder}

List one folder

A row is a conversation — the newest message in each thread — not a message. Paged by an opaque `cursor`, which is the `next_cursor` of the page before it: it carries a sort key and an id together, because several messages can share a millisecond. Do not construct one.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `folder` | string | yes |  |
| `identity_id` | uuid | yes |  |
| `limit` | number | no |  |
| `cursor` | string | no |  |
| `label_id` | uuid | no |  |

**Responses**

- `200` A page of conversations.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## DELETE /mailboxes/{folder}

Empty the trash

Only `trash`; every other folder refuses. Permanent, and it takes attachments and archived originals with it — there is no undo, because the snapshot `POST /messages/undo` replays is a set of column values and these rows are gone. `delete_forever` on `POST /messages/actions` does the same to a list you name, and caps at 200; this needs no ids.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `folder` | string | yes |  |
| `identity_id` | uuid | yes |  |

**Responses**

- `200` What went.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.
