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

# Messages

> List a folder, read one message in full, and mark, star or move it — the read and write half of the mail API.

Everything about mail that already exists. Sending is a
[separate resource](/api/sending), on purpose.

<Note>
  **Folders are named by role, not by path.** Use `inbox`, `archive`, `sent`,
  `drafts`, `junk` or `trash`.

  Path names are not portable and are not even stable for us — Junk is "Junk" on
  Dovecot and "Spam" elsewhere, Trash is "Deleted Items" on Exchange, and any of
  them can sit under a namespace prefix. Roles are resolved against the server's
  own SPECIAL-USE attributes on every request, so `folder=junk` keeps meaning the
  junk folder whatever it is called. Custom folders are not addressable yet.
</Note>

## List messages

```
GET https://app.ruber.me/api/v1/messages
```

**Required permission:** Messages → Read (`messages:read`)

| Parameter |              | Description                                            |
| --------- | ------------ | ------------------------------------------------------ |
| `mailbox` | **required** | An address from [`GET /v1/mailboxes`](/api/mailboxes). |
| `folder`  | optional     | Defaults to `inbox`.                                   |
| `filter`  | optional     | `all` (default), `unread`, or `starred`.               |
| `limit`   | optional     | 1–100. Defaults to 25.                                 |
| `page`    | optional     | 0-based. Page 0 is the newest messages.                |

```bash theme={null}
curl "https://app.ruber.me/api/v1/messages?mailbox=you@yourdomain.com&folder=inbox&limit=25" \
  -H "Authorization: Bearer rk_your_key_here"
```

```json theme={null}
{
  "data": [
    {
      "uid": 55,
      "folder": "inbox",
      "subject": "Your invoice for August",
      "from": { "name": "Accounts", "address": "billing@supplier.com" },
      "to": ["you@yourdomain.com"],
      "cc": [],
      "reply_to": [],
      "message_id": "<178786157357.1014.795@supplier.com>",
      "references": [],
      "received_at": "2026-08-27T20:12:53.000Z",
      "unread": true,
      "starred": false,
      "has_attachment": true,
      "labels": [],
      "authenticated": true,
      "delivered_to": "you@yourdomain.com",
      "preview": "Hi — your invoice for August is attached…"
    }
  ],
  "total": 214,
  "page": 0,
  "has_more": true
}
```

`total` is how many messages the **folder** holds, not how many came back, so
you can tell whether another page exists without fetching it. `has_more` says
the same thing directly.

<Warning>
  **A `uid` is unique within one folder, not within the mailbox.** It is IMAP's
  identifier, and the same number can legitimately name a different message in a
  different folder. Always carry the folder alongside it — every endpoint below
  requires both.

  A uid is also not permanent: moving a message gives it a new one, and a mailbox
  rebuild can reset the whole range. If you need a durable identifier across
  time, store `message_id`, which is the sender's and never changes.
</Warning>

Bodies are **not** included in the list — a page of 100 messages with full HTML
would be megabytes. Use `preview`, or fetch the message.

## Get one message

```
GET https://app.ruber.me/api/v1/messages/{uid}
```

**Required permission:** Messages → Read (`messages:read`)

Takes the same `mailbox` (required) and `folder` parameters as the list.

```bash theme={null}
curl "https://app.ruber.me/api/v1/messages/55?mailbox=you@yourdomain.com&folder=inbox" \
  -H "Authorization: Bearer rk_your_key_here"
```

```json theme={null}
{
  "uid": 55,
  "folder": "inbox",
  "subject": "Your invoice for August",
  "from": { "name": "Accounts", "address": "billing@supplier.com" },
  "to": ["you@yourdomain.com"],
  "cc": [],
  "reply_to": [],
  "message_id": "<178786157357.1014.795@supplier.com>",
  "in_reply_to": null,
  "references": [],
  "received_at": "2026-08-27T20:12:53.000Z",
  "text": "Hi — your invoice for August is attached.",
  "html": "<p>Hi — your invoice for August is attached.</p>",
  "attachments": [
    { "index": 0, "filename": "invoice-august.pdf", "content_type": "application/pdf", "size": 48213 }
  ]
}
```

<Note>
  **`html` is sanitised before it reaches you.** The stored bytes are whatever a
  stranger sent us; scripts, event handlers and other active content are removed.
  `text` is the message's own plain-text part, untouched.

  `html` is `null` for a plain-text message, so you can tell "no HTML" from
  "empty HTML".
</Note>

Reading a message does **not** mark it read. That is a separate, deliberate
call — an integration that indexes a mailbox should not silently change what
its owner sees as new.

## Update a message

```
PATCH https://app.ruber.me/api/v1/messages/{uid}
```

**Required permission:** Messages → Write (`messages:write`)

Takes `mailbox` (required) and `folder` as query parameters — naming where the
message is **now** — and the change as a JSON body. Send at least one of:

| Field     | Type    | Effect                               |
| --------- | ------- | ------------------------------------ |
| `read`    | boolean | Adds or removes the `\Seen` flag.    |
| `starred` | boolean | Adds or removes the `\Flagged` flag. |
| `move_to` | string  | Moves it to another folder role.     |

```bash theme={null}
curl -X PATCH "https://app.ruber.me/api/v1/messages/55?mailbox=you@yourdomain.com&folder=inbox" \
  -H "Authorization: Bearer rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"read": true, "move_to": "archive"}'
```

```json theme={null}
{ "folder": "archive", "moved": true, "read": true, "starred": null }
```

Flags are applied **before** any move, so a single call can mark a message read
and file it in one step.

<Warning>
  **After a move, the response carries no `uid`.** The message has a different one
  in its new folder and this endpoint does not learn it. Returning the old number
  beside the new folder would be a value you might store and later use to address
  the wrong message — `"moved": true` says "look it up again" instead.

  When nothing moved, `uid` is returned unchanged.
</Warning>

## Errors

| Code               | HTTP  | Cause                                                                                                                   |
| ------------------ | ----- | ----------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`  | `400` | Missing `mailbox`, an unknown `folder` or `filter`, a `limit` outside 1–100, a non-numeric `uid`, or a Private mailbox. |
| `unauthorized`     | `401` | Missing, malformed, unknown or revoked key.                                                                             |
| `forbidden`        | `403` | The key lacks `messages:read` or `messages:write`.                                                                      |
| `not_found`        | `404` | No such mailbox on this account, or no message with that uid in that folder.                                            |
| `rate_limited`     | `429` | Over 600 requests in a minute.                                                                                          |
| `mail_unavailable` | `502` | The mail host did not answer. Transient; retry with backoff.                                                            |

See [Errors](/api/introduction#errors) for the full response shape.
