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

# API overview

> Authenticate with a restricted API key, understand the permission model, and read Ruber's errors — everything shared by every endpoint.

Ruber's HTTP API lets your own software work with mail in the mailboxes your
organization owns. It is a normal JSON API over HTTPS: no SDK required, no
websocket, nothing to install.

## What you can do

| Endpoint                                                                                | Permission         |
| --------------------------------------------------------------------------------------- | ------------------ |
| [`GET /v1/mailboxes`](/api/mailboxes)                                                   | Mailboxes → Read   |
| [`GET /v1/messages`](/api/messages#list-messages)                                       | Messages → Read    |
| [`GET /v1/messages/{uid}`](/api/messages#get-one-message)                               | Messages → Read    |
| [`PATCH /v1/messages/{uid}`](/api/messages#update-a-message)                            | Messages → Write   |
| [`POST /v1/messages`](/api/sending)                                                     | Sending → Write    |
| [`GET /v1/messages/{uid}/attachments`](/api/attachments#list-attachments)               | Attachments → Read |
| [`GET /v1/messages/{uid}/attachments/{index}`](/api/attachments#download-an-attachment) | Attachments → Read |
| [`GET /v1/scheduled`](/api/scheduled#list-scheduled-messages)                           | Scheduled → Read   |
| [`POST /v1/scheduled`](/api/scheduled#schedule-a-message)                               | Scheduled → Write  |
| [`DELETE /v1/scheduled/{id}`](/api/scheduled#cancel-a-scheduled-message)                | Scheduled → Write  |

<Note>
  **Rolling out.** `GET /v1/mailboxes` is live now. The rest are shipping
  shortly — until they land, a call may answer `404`. Nothing about the request
  or response shapes below will change when they do.
</Note>

## Base URL

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

Every request must be HTTPS. There is no HTTP port to redirect from.

## Authentication

Send your key as a bearer token:

```bash theme={null}
curl https://app.ruber.me/api/v1/mailboxes \
  -H "Authorization: Bearer rk_your_key_here"
```

Keys are created under **Settings → API keys** in the dashboard. They begin
`rk_`, and the full key is shown **exactly once**, in the dialog that creates
it. Ruber stores only a SHA-256 hash — we cannot show it to you again, and we
cannot recover it. If a key is lost, revoke it and create another.

<Warning>
  **A key is a password for your mail.** Keep it out of source control, out of
  front-end code, and out of anything a browser downloads. If a key is exposed,
  revoke it in the dashboard — revocation takes effect immediately, on the next
  request.
</Warning>

### Every authentication failure looks the same

A missing header, a malformed header, a key that never existed and a key you
revoked all answer the same `401` with the same sentence. That is deliberate:
distinguishing them would let someone holding a list of guesses learn which
ones are real keys that happen to be revoked.

A **missing permission** is different, and is deliberately specific — see below.

## Permissions

Every key carries a set of permissions, and a key can only do what you granted
it when you created it. There is no "full access" key, no `admin`, and no
wildcard. **Permissions cannot be changed after creation** — to change what a
key may do, create a new one and revoke the old.

When you create a key you choose one level per resource:

| Resource               | Levels | Scopes granted                      |
| ---------------------- | ------ | ----------------------------------- |
| **Mailboxes**          | Read   | `mailboxes:read`                    |
| **Messages**           | Read   | `messages:read`                     |
|                        | Write  | `messages:read`, `messages:write`   |
| **Sending**            | Write  | `messages:send`                     |
| **Scheduled messages** | Read   | `scheduled:read`                    |
|                        | Write  | `scheduled:read`, `scheduled:write` |
| **Attachments**        | Read   | `attachments:read`                  |

Two things about that table are worth reading twice.

**Write includes Read.** Choosing Write on a resource grants its read scope
too. A key that may move a message between folders but cannot see which folder
it is in describes no real integration.

**Sending is its own resource.** `messages:write` covers changing mail that
already exists — marking it read, starring it, filing it. It does **not** let a
key send anything. Sending creates mail that leaves under your domain's
reputation and spends your plan's daily allowance, so it is a separate,
deliberate choice.

### When a key lacks a permission

You get a `403` that names exactly what was missing, because by then the caller
has proved they hold a real key and there is nothing left to protect:

```json theme={null}
{
  "error": {
    "code": "forbidden",
    "message": "This API key does not have the `messages:send` scope. Add it to the key in Settings → API keys, or create a new key.",
    "required_scope": "messages:send"
  }
}
```

## Private mailboxes are not readable

A Private mailbox is encrypted to a key held only in its owner's browser. Ruber
holds no copy, so there is nothing the API could return but ciphertext and
nothing it could send.

Private mailboxes **do** appear in [`GET /v1/mailboxes`](/api/mailboxes), with
`"mode": "private"` — hiding them would make an integration report a mailbox as
missing when it simply cannot be read by anyone but its owner. Any endpoint that
takes an address will refuse a Private one with `invalid_request` and say why.

Smart mailboxes work normally.

## Errors

Every error has the same shape. The `code` is stable and is what your software
should branch on; the `message` is written for a human reading a log and may be
reworded at any time.

```json theme={null}
{
  "error": {
    "code": "not_found",
    "message": "No such mailbox on this account."
  }
}
```

| Code               | HTTP  | What it means                                                                                                 |
| ------------------ | ----- | ------------------------------------------------------------------------------------------------------------- |
| `invalid_request`  | `400` | The request is malformed, or asks for something that cannot be done — including addressing a Private mailbox. |
| `unauthorized`     | `401` | No key, a malformed key, or a key that is not valid.                                                          |
| `quota_exceeded`   | `402` | Your plan's allowance for this action is used up for the day.                                                 |
| `forbidden`        | `403` | Valid key, but it lacks the required permission. Carries `required_scope`.                                    |
| `not_found`        | `404` | No such resource — or it is not yours. The same answer either way.                                            |
| `rate_limited`     | `429` | Too many requests. Carries a `Retry-After` header, in seconds.                                                |
| `internal_error`   | `500` | Something broke on our side. Safe to retry.                                                                   |
| `mail_unavailable` | `502` | The mail host did not answer. Transient; retry with backoff.                                                  |

<Note>
  **`404` does not mean "does not exist".** A mailbox belonging to another
  organization and a mailbox that was never created both answer `not_found`.
  Telling them apart would turn the API into a way to test whether an address
  exists on the platform, one guess at a time.
</Note>

## Rate limits

Requests are limited **per key**, at **600 per minute**. That is ten a second
sustained — comfortably above any integration doing real work.

Going over returns `429` with a `Retry-After` header giving the seconds to wait.
There is no lockout: a client that misjudges its own rate recovers on the next
window.

```
HTTP/1.1 429 Too Many Requests
Retry-After: 37
```

Narrower limits still apply underneath. Sending, in particular, also passes a
per-account hourly limit, a short burst ceiling, and your plan's daily
allowance — so this ceiling is never a way around them. A send refused by the
daily allowance returns `quota_exceeded`, not `rate_limited`.

## Caching

Every response carries `Cache-Control: no-store`. Responses are scoped to one
organization's mail, and a shared cache keyed on the URL alone would serve one
customer's inbox to another. Do not cache these responses in shared
infrastructure.
