# Rapidmail API

The Rapidmail API lets a script or an AI assistant manage your aliases and profiles and read your mail. The base address is `https://api.rapidmail.work/v1`. The MCP server is at `https://api.rapidmail.work/mcp`.

## Quick start

Three steps take you from no key to reading mail.

1. Create a key in Settings, API. Give it a name and the scopes `aliases:write` and `messages:read`. Copy the key when it shows: it shows once.

2. Create an alias on a random open domain. Leave `domain` out, and the server picks a domain where the address is free.

```bash
export RAPIDMAIL_KEY=rm_live_7Hq2_Zx81kLp0QvT4mNa9RcW3eYb6Pq5sTu2w
curl -s -X POST https://api.rapidmail.work/v1/aliases \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"local":"alice.ter"}'
```

The answer names the address it got, for example `alice.ter@rmail.ink`.

3. Read the mail that arrives at it, newest first.

```bash
curl -s "https://api.rapidmail.work/v1/messages?to=alice.ter@rmail.ink" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

Each key may send 60 requests per minute. To follow new mail, see Pages and cursors.

## Base address and keys

Every route starts with `https://api.rapidmail.work/v1`. Every request body and every answer is JSON.

Send your key in the `Authorization` header of every request:

```
Authorization: Bearer rm_live_7Hq2_Zx81kLp0QvT4mNa9RcW3eYb6Pq5sTu2w
```

A key has the form `rm_live_<4>_<32>`: letters and digits, 45 characters in all. The first part, `rm_live_<4>`, is the prefix. Settings shows the prefix so you can tell your keys apart.

Create a key in Settings, API. Give it a name of 1 to 60 characters and at least one scope. The key shows once. Rapidmail keeps only its SHA-256 hash and cannot show it again.

You can hold 25 keys that are not revoked, unless your account has another limit. Revoke a key to free a slot: it stops working on the next request. Rotate a key to get a new secret with the same name and scopes: the old secret stops working at once.

The API never reads cookies. A page on any origin can call it with a key.

A new account waits for an operator to approve it. Until then it creates keys and calls every route but the forwarding writes, with the same limits.

The API sends no mail of yours. `POST /v1/forward-targets` makes Rapidmail send one confirmation mail to that address, and a pending account cannot call it.

## Scopes

A key holds one or more scopes. A route needs at most one scope.

| Scope | Routes |
|---|---|
| `aliases:read` | `GET /v1/aliases`, `GET /v1/aliases/:id/blocks`, `GET /v1/subdomains` |
| `aliases:write` | `POST /v1/aliases`, `PATCH /v1/aliases/:id`, `DELETE /v1/aliases/:id`, `POST /v1/aliases/:id/blocks`, `DELETE /v1/aliases/:id/blocks/:blockId`, `POST /v1/subdomains`, `PATCH /v1/subdomains/:id` |
| `profiles:read` | `GET /v1/profiles` |
| `profiles:write` | `POST /v1/profiles`, `DELETE /v1/profiles/:id` |
| `messages:read` | `GET /v1/messages`, `GET /v1/messages/wait`, `GET /v1/messages/latest`, `GET /v1/messages/:id`, `GET /v1/folders` |
| `forwards:read` | `GET /v1/forward-targets`, `GET /v1/forwards` |
| `forwards:write` | `POST /v1/forward-targets`, `POST /v1/forward-targets/:id/resend`, `DELETE /v1/forward-targets/:id`, `PUT /v1/forwards`, `POST /v1/forwards/:id/resume` |
| `autodelete:read` | `GET /v1/auto-delete` |
| `autodelete:write` | `POST /v1/auto-delete`, `PATCH /v1/auto-delete/:id`, `DELETE /v1/auto-delete/:id` |
| `webhooks:read` | `GET /v1/webhook` |
| `webhooks:write` | `PUT /v1/webhook`, `DELETE /v1/webhook`, `POST /v1/webhook/rotate`, `POST /v1/webhook/test` |
| `keys:read` | `GET /v1/keys` |

`GET /v1/domains` needs a valid key and no scope.

Give a key only the scopes its script needs. A key without the scope of a route gets 403 `missing_scope`.

## Limits and errors

Each key may send 60 requests per minute. Each key has its own count. The request past the limit gets 429 `rate_limited` with a `Retry-After` header: the seconds to wait.

Invalid keys are counted per IP: after 30 invalid keys in 15 minutes, every request from that IP gets 429 `rate_limited` with `Retry-After`, a valid key included.

Every refusal has this body, with the HTTP status:

```json
{ "error": { "code": "address_taken", "message": "alice.ter@rmail.ink is taken" } }
```

Read `code` in your script. `message` is for people and can change.

Any route can answer these codes. Each route lists its own codes too.

| Status | Code | What to do |
|---|---|---|
| 400 | `bad_request` | The body or the query is not valid: a missing field, an unknown field, or a value out of bounds. `message` names the problem. Fix the request and send it again. |
| 401 | `invalid_key` | The key is missing, wrong or revoked, or its account is deleted. Check the `Authorization` header. |
| 403 | `account_suspended` | The account of the key is suspended. Contact support; a new key does not help. |
| 403 | `missing_scope` | The key lacks the scope of this route. Create a key with that scope in Settings, API. |
| 429 | `rate_limited` | Too many requests with this key, or too many invalid keys from your IP. Wait `Retry-After` seconds. |
| 503 | `api_disabled` | The operators turned the API off for maintenance. Try again later. |

The checks run in this order: `api_disabled`, the invalid-key limit per IP, `invalid_key`, `account_suspended`, the per-key `rate_limited`, `missing_scope`, `account_pending`, then the route’s own checks.

Your account may hold 100 aliases over every mailbox and 10 profiles, unless it has other limits. A deleted address stays reserved for 365 days. A local part is 3 to 32 letters, digits and single dots, starting with a letter.

## Pages and cursors

`GET /v1/aliases` and `GET /v1/profiles` answer one page, oldest first. `limit` is the page size, 1 to 100, and 100 when left out. The answer carries `next`: send it back as `cursor` for the next page. `next` is null on the last page.

`GET /v1/messages` answers newest first, with a `cursor`. To follow new mail, poll with it:

1. Call `GET /v1/messages` once without `since`. Handle the messages and store `cursor`.
2. Wait, for example 30 seconds.
3. Call `GET /v1/messages?since=<cursor>`. Handle the messages and store the new `cursor`, also when the list is empty.
4. Go back to step 2.

A cursor is opaque. Store it as it is, and send it with the same `to` or `profile`. When more than `limit` messages arrived, one answer holds only the newest `limit` of them.

```bash
#!/usr/bin/env bash
# Prints the subject of each new message. Needs curl and jq.
set -euo pipefail
API=https://api.rapidmail.work/v1
CURSOR=""
while true; do
  PAGE=$(curl -sf "$API/messages?limit=100${CURSOR:+&since=$CURSOR}" \
    -H "Authorization: Bearer $RAPIDMAIL_KEY")
  echo "$PAGE" | jq -r '.messages[] | "\(.receivedAt) \(.alias // "-") \(.subject)"'
  CURSOR=$(echo "$PAGE" | jq -r '.cursor | @uri')
  sleep 30
done
```

## MCP server

An AI assistant can use this API through the MCP server at `https://api.rapidmail.work/mcp`. It speaks MCP over Streamable HTTP and answers JSON.

Connect it with OAuth: the assistant opens a Rapidmail page where you pick the account and allow "Read only" or "Read and write". An assistant without OAuth can send an API key in the `Authorization` header instead.

Each route is one tool, with the same input, the same scopes and the same limits. A tool your access does not allow is not listed.

| Tool | Route | Scope |
|---|---|---|
| `domains_list` | `GET /v1/domains` | any |
| `aliases_list` | `GET /v1/aliases` | `aliases:read` |
| `aliases_create` | `POST /v1/aliases` | `aliases:write` |
| `aliases_update` | `PATCH /v1/aliases/:id` | `aliases:write` |
| `aliases_delete` | `DELETE /v1/aliases/:id` | `aliases:write` |
| `aliases_blocks_list` | `GET /v1/aliases/:id/blocks` | `aliases:read` |
| `aliases_blocks_create` | `POST /v1/aliases/:id/blocks` | `aliases:write` |
| `aliases_blocks_delete` | `DELETE /v1/aliases/:id/blocks/:blockId` | `aliases:write` |
| `subdomains_list` | `GET /v1/subdomains` | `aliases:read` |
| `subdomains_claim` | `POST /v1/subdomains` | `aliases:write` |
| `subdomains_update` | `PATCH /v1/subdomains/:id` | `aliases:write` |
| `profiles_list` | `GET /v1/profiles` | `profiles:read` |
| `profiles_create` | `POST /v1/profiles` | `profiles:write` |
| `profiles_delete` | `DELETE /v1/profiles/:id` | `profiles:write` |
| `messages_list` | `GET /v1/messages` | `messages:read` |
| `messages_wait` | `GET /v1/messages/wait` | `messages:read` |
| `messages_latest` | `GET /v1/messages/latest` | `messages:read` |
| `messages_get` | `GET /v1/messages/:id` | `messages:read` |
| `folders_list` | `GET /v1/folders` | `messages:read` |
| `forward_targets_list` | `GET /v1/forward-targets` | `forwards:read` |
| `forward_targets_create` | `POST /v1/forward-targets` | `forwards:write` |
| `forward_targets_resend` | `POST /v1/forward-targets/:id/resend` | `forwards:write` |
| `forward_targets_delete` | `DELETE /v1/forward-targets/:id` | `forwards:write` |
| `forwards_list` | `GET /v1/forwards` | `forwards:read` |
| `forwards_set` | `PUT /v1/forwards` | `forwards:write` |
| `forwards_resume` | `POST /v1/forwards/:id/resume` | `forwards:write` |
| `autodelete_list` | `GET /v1/auto-delete` | `autodelete:read` |
| `autodelete_create` | `POST /v1/auto-delete` | `autodelete:write` |
| `autodelete_update` | `PATCH /v1/auto-delete/:id` | `autodelete:write` |
| `autodelete_delete` | `DELETE /v1/auto-delete/:id` | `autodelete:write` |
| `webhook_get` | `GET /v1/webhook` | `webhooks:read` |
| `webhook_set` | `PUT /v1/webhook` | `webhooks:write` |
| `webhook_delete` | `DELETE /v1/webhook` | `webhooks:write` |
| `webhook_rotate` | `POST /v1/webhook/rotate` | `webhooks:write` |
| `webhook_test` | `POST /v1/webhook/test` | `webhooks:write` |
| `keys_list` | `GET /v1/keys` | `keys:read` |

## For AI agents

The same reference comes in forms a program reads. None of them needs a key.

- `https://rapidmail.work/llms.txt`: an index in the llms.txt format, with links to the forms below.
- `https://rapidmail.work/llms-full.txt`: this whole reference in one Markdown file.
- `https://api.rapidmail.work/v1/docs.md`: this whole reference in Markdown.
- `https://api.rapidmail.work/v1/openapi.json`: an OpenAPI 3.1 document with a schema for every answer.
- `https://api.rapidmail.work/v1/docs.json`: this reference as JSON.
- `https://api.rapidmail.work/mcp`: the MCP server, one tool per route.

## Domains

### List the domains

`GET /v1/domains`

Lists the domains where you can create aliases and profiles, the primary first.

Any valid key may call it. Name one of these domains in `POST /v1/aliases` or `POST /v1/profiles`. To let the server pick, leave `domain` out of `POST /v1/aliases`.

Scope: any valid key. MCP tool: `domains_list`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `domains` | object[] | Every domain open for new aliases and profiles, the primary first. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/domains" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "domains": [
    {
      "name": "rapidmail.work",
      "primary": true,
      "autoAliases": false
    },
    {
      "name": "rmail.ink",
      "primary": false,
      "autoAliases": true
    }
  ]
}
```

## Aliases

### List your aliases

`GET /v1/aliases`

Lists your aliases, one page at a time, oldest first.

Each alias carries `received`, the messages delivered to it outside Blocked, and `blocked`, the messages from a blocked sender. `lastReceived` is the newest message: `at` always, `from` and `subject` only when the key also holds `messages:read`. `forward` and `autoDeleteDays` show the rules of the alias. `used` and `limit` say how many more you can create. Send `next` back as `cursor` for the next page.

Scope: `aliases:read`. MCP tool: `aliases_list`.

**Query**

| Field | Type | Required | Description |
|---|---|---|---|
| `limit` | string | no | The page size, a whole number from 1 to 100. Left out: 100. |
| `cursor` | string | no | The `next` of the page before. Left out: the first page. |
| `profile` | string | no | Only the aliases of this profile. Left out: every alias. |
| `origin` | "manual" \| "auto" | no | `auto`: only the aliases that made themselves on first mail. `manual`: only the others. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `aliases` | object[] | One page of aliases, oldest first. |
| `used` | integer | Every alias of the account, in every mailbox, receiving or not. |
| `limit` | integer \| null | The most aliases your account may hold. Null: no limit. |
| `releaseAfterDays` | integer | Days a deleted alias address stays reserved before anyone can take it. |
| `next` | string \| null | Send it back as `cursor` for the next page. Null on the last page. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 400 | `invalid_cursor` | The cursor is not one this API gave. Start without it. |
| 404 | `not_found` | The profile is not yours. List your profiles first. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/aliases?limit=20" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "aliases": [
    {
      "id": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
      "address": "alice.ter@rmail.ink",
      "local": "alice.ter",
      "domain": "rmail.ink",
      "profile": null,
      "label": "Shopping",
      "note": null,
      "receives": true,
      "createdAt": "2026-10-19T09:12:44.000Z",
      "origin": "manual",
      "firstSender": null,
      "received": 12,
      "blocked": 1,
      "lastReceived": {
        "at": "2026-10-19T09:30:02.000Z",
        "from": "orders@store.example",
        "subject": "Your order shipped"
      },
      "forward": null,
      "autoDeleteDays": 30
    }
  ],
  "used": 1,
  "limit": 100,
  "releaseAfterDays": 365,
  "next": null
}
```

### Create an alias

`POST /v1/aliases`

Creates an alias: an extra address whose mail lands in your mailbox.

Name a `domain` to get that exact address. Leave `domain` out and the server picks one of the open domains at random where the address is free, in the same request, so no other caller can take it in between. `address` and `domain` in the answer name the domain it got.

Scope: `aliases:write`. MCP tool: `aliases_create`.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `local` | string | yes | The part before the @. Letters, digits and single dots, starting with a letter, 3 to 32 characters. Upper case is stored as lower case. |
| `domain` | string | no | A domain from `GET /v1/domains`. Left out: the server picks an open domain at random where the address is free. |
| `label` | string \| null | no | A note for you, at most 60 characters. Left out, null or "": no label. |
| `profile` | string | no | The id of one of your profiles: that profile's mailbox receives the mail. Left out: your own mailbox. |

**Answer (201)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The alias id. |
| `address` | string | The whole address: `alice.ter@rmail.ink`. |
| `local` | string | The part before the @, in lower case. |
| `domain` | string | The part after the @. |
| `profile` | uuid \| null | The profile whose mailbox receives the mail. Null: your own mailbox. |
| `label` | string \| null | A short name for the alias, or null. |
| `note` | string \| null | Your note on the alias, up to 500 characters, or null. |
| `receives` | boolean | On. True: the alias receives and sends mail. False: new mail to it bounces, it cannot send, and the address stays yours. |
| `createdAt` | datetime | When the alias was created, ISO 8601 in UTC. |
| `origin` | "manual" \| "auto" | `manual`: you created it. `auto`: it made itself on its first mail to your subdomain. |
| `firstSender` | string \| null | For an auto alias: the domain of its first sender, never the address. Else null. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | The profile is not yours. List your profiles first. |
| 409 | `address_taken` | Someone holds the address, or, without `domain`, it is taken on every open domain. Try another local part. |
| 422 | `alias_limit` | Your account holds its most aliases. Delete one first. |
| 422 | `invalid_local` | The local part breaks the format rule. Use letters, digits and single dots, 3 to 32 characters. |
| 422 | `domain_closed` | The domain is not in `GET /v1/domains`, or no domain is open. Pick one from that list. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/aliases" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"local":"alice.ter","label":"Shopping"}'
```

```json
{
  "id": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
  "address": "alice.ter@rmail.ink",
  "local": "alice.ter",
  "domain": "rmail.ink",
  "profile": null,
  "label": "Shopping",
  "note": null,
  "receives": true,
  "createdAt": "2026-10-19T09:12:44.000Z",
  "origin": "manual",
  "firstSender": null
}
```

### Change an alias

`PATCH /v1/aliases/:id`

Turns an alias on or off, or changes its label or note.

Send `receives`, `label`, `note`, or any of them. `receives` is On: it turns receiving and sending on and off together. With `receives` false, new mail to the alias bounces with 550 5.1.1, or reaches the operators’ catch-all mailbox when one is set, and nobody can send as the alias. The address stays yours and still counts against your limit. A note is at most 500 characters; null or "" removes it.

Scope: `aliases:write`. MCP tool: `aliases_update`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The alias id, as the list answers it. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `receives` | boolean | no | On. False: new mail to the alias bounces and it cannot send; the address stays yours. True: it receives and sends again. Left out: unchanged. |
| `label` | string \| null | no | A short name for you, at most 60 characters. Null or "" removes it. Left out: unchanged. |
| `note` | string \| null | no | A note for you, at most 500 characters. Null or "" removes it. Left out: unchanged. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The alias id. |
| `address` | string | The whole address: `alice.ter@rmail.ink`. |
| `local` | string | The part before the @, in lower case. |
| `domain` | string | The part after the @. |
| `profile` | uuid \| null | The profile whose mailbox receives the mail. Null: your own mailbox. |
| `label` | string \| null | A short name for the alias, or null. |
| `note` | string \| null | Your note on the alias, up to 500 characters, or null. |
| `receives` | boolean | On. True: the alias receives and sends mail. False: new mail to it bounces, it cannot send, and the address stays yours. |
| `createdAt` | datetime | When the alias was created, ISO 8601 in UTC. |
| `origin` | "manual" \| "auto" | `manual`: you created it. `auto`: it made itself on its first mail to your subdomain. |
| `firstSender` | string \| null | For an auto alias: the domain of its first sender, never the address. Else null. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No alias of yours has this id. List them first. |
| 409 | `address_taken` | The mail server holds the address for another mailbox, so it cannot turn on. Contact support. |
| 502 | `mailcore_unavailable` | The mail server refused to turn the alias on or off. Nothing changed. Try again later. |

**Example**

```bash
curl -s -X PATCH "https://api.rapidmail.work/v1/aliases/4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"receives":false,"note":"Used for the store account"}'
```

```json
{
  "id": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
  "address": "alice.ter@rmail.ink",
  "local": "alice.ter",
  "domain": "rmail.ink",
  "profile": null,
  "label": "Shopping",
  "note": "Used for the store account",
  "receives": false,
  "createdAt": "2026-10-19T09:12:44.000Z",
  "origin": "manual",
  "firstSender": null
}
```

### Delete an alias

`DELETE /v1/aliases/:id`

Deletes an alias and frees its slot.

Nobody, you included, can take the address again until `reservedUntil`: `releaseAfterDays` days, 365 by default. To stop the mail but keep the address, set `receives` to false instead.

Scope: `aliases:write`. MCP tool: `aliases_delete`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The alias id, as the list answers it. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `deleted` | true | Always true. |
| `address` | string | The address of the deleted alias. |
| `reservedUntil` | datetime | Nobody, you included, can take the address before this time. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No alias of yours has this id. List them first. |

**Example**

```bash
curl -s -X DELETE "https://api.rapidmail.work/v1/aliases/4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "deleted": true,
  "address": "alice.ter@rmail.ink",
  "reservedUntil": "2027-10-19T09:20:00.000Z"
}
```

### List blocked senders

`GET /v1/aliases/:id/blocks`

Lists the blocked senders of one alias.

Mail to the alias from a blocked sender goes to the folder Blocked, not the inbox. `caught` counts it. `limit` is the most blocked senders one alias may hold.

Scope: `aliases:read`. MCP tool: `aliases_blocks_list`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The alias id, as the list answers it. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `blocks` | object[] | The blocked senders of the alias, oldest first. |
| `limit` | integer | The most blocked senders one alias may hold. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No alias of yours has this id. List them first. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/aliases/4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69/blocks" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "blocks": [
    {
      "id": "7a3e1c5b-9d2f-4b8a-8c6e-2f4d1b3a5c70",
      "sender": "@deals.example",
      "caught": 3,
      "createdAt": "2026-10-20T08:00:00.000Z"
    }
  ],
  "limit": 200
}
```

### Block a sender

`POST /v1/aliases/:id/blocks`

Blocks one sender, or every sender of a domain, for one alias.

Send `sender` as `local@domain` or `@domain`. New mail from that sender to the alias goes to the folder Blocked. Mail that arrived earlier stays where it is.

Scope: `aliases:write`. MCP tool: `aliases_blocks_create`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The alias id, as the list answers it. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `sender` | string | yes | The sender to block: `local@domain`, or `@domain` for every sender of that domain. |

**Answer (201)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The blocked sender id. |
| `sender` | string | `local@domain` or `@domain`, in lower case. |
| `caught` | integer \| null | Messages from this sender filed into Blocked. Null when the mail server did not answer. |
| `createdAt` | datetime | When the sender was blocked, ISO 8601 in UTC. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No alias of yours has this id. List them first. |
| 409 | `block_exists` | The alias blocks this sender already. Nothing to do. |
| 422 | `invalid_sender` | The sender is not `local@domain` or `@domain`, or it is your own address or a forwarding address. Fix it. |
| 422 | `block_limit` | The alias holds its most blocked senders. Remove one first. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/aliases/4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69/blocks" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sender":"@deals.example"}'
```

```json
{
  "id": "7a3e1c5b-9d2f-4b8a-8c6e-2f4d1b3a5c70",
  "sender": "@deals.example",
  "caught": 0,
  "createdAt": "2026-10-20T08:00:00.000Z"
}
```

### Unblock a sender

`DELETE /v1/aliases/:id/blocks/:blockId`

Removes one blocked sender of an alias.

New mail from the sender reaches the inbox again. Mail already in Blocked stays there.

Scope: `aliases:write`. MCP tool: `aliases_blocks_delete`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The alias id, as the list answers it. |
| `blockId` | string | yes | The blocked sender id, as the list answers it. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `deleted` | true | Always true. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No such alias or blocked sender. List them first. |

**Example**

```bash
curl -s -X DELETE "https://api.rapidmail.work/v1/aliases/4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69/blocks/7a3e1c5b-9d2f-4b8a-8c6e-2f4d1b3a5c70" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "deleted": true
}
```

## Subdomains

### List your subdomains

`GET /v1/subdomains`

Lists your subdomains, the domains that take one, and the daily cap.

A subdomain such as `alice.rmail.ink` takes mail to any name: `github@alice.rmail.ink` makes itself an alias on its first mail. A new name must be in the `To` or `Cc` of the message, on that subdomain; a `+tag` is dropped, and the name follows the alias rule. An alias made this way counts against your alias limit and carries `origin: auto` and `firstSender`. At most `perDay` aliases make themselves per day. At the limit or the daily cap, and for spam, a reserved or released name or an alias with Receives off, the message is dropped with no bounce.

Scope: `aliases:read`. MCP tool: `subdomains_list`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `subdomains` | object[] | Your subdomains, one per domain at most. |
| `domains` | object[] | Every domain where you can claim a subdomain. |
| `defaultLabel` | string \| null | The label a claim takes when the body names none: your username when it is free. Else null. |
| `perDay` | integer | The most aliases that make themselves per account per UTC day. |
| `today` | integer | The aliases that made themselves today. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/subdomains" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "subdomains": [
    {
      "id": "9b1e4c2a-7d3f-4e5a-8c6b-2a1f0e9d8c7b",
      "name": "alice.rmail.ink",
      "label": "alice",
      "domain": "rmail.ink",
      "profile": null,
      "enabled": true,
      "paused": null,
      "createdAt": "2026-10-21T09:00:00.000Z"
    }
  ],
  "domains": [
    {
      "name": "rmail.ink",
      "claim": "alice.rmail.ink"
    }
  ],
  "defaultLabel": null,
  "perDay": 20,
  "today": 3
}
```

### Claim a subdomain

`POST /v1/subdomains`

Claims `<label>.<domain>` on a domain that takes subdomains, and turns it on.

The label is 3 to 32 characters: lowercase letters, digits and single hyphens, starting with a letter. Leave it out to take your username. You hold one subdomain per domain.

Scope: `aliases:write`. MCP tool: `subdomains_claim`.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `domain` | string | yes | A domain whose `autoAliases` is true. |
| `label` | string | no | 3 to 32 lowercase letters, digits and single hyphens, starting with a letter. Left out: your username. |
| `profile` | string | no | One of your profiles: its mailbox takes the mail and the aliases. Left out: your own mailbox. |

**Answer (201)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The subdomain id. |
| `name` | string | The whole subdomain: `alice.rmail.ink`. Mail to any name at it is yours. |
| `label` | string | The part you chose: `alice`. |
| `domain` | string | The open domain it sits on: `rmail.ink`. |
| `profile` | uuid \| null | The profile whose mailbox takes its mail and its aliases. Null: your own mailbox. |
| `enabled` | boolean | False: mail to a new name bounces. The aliases it made keep working. |
| `paused` | "limit" \| "daily_cap" \| null | Why no alias makes itself now: `limit` (you hold your alias limit) or `daily_cap` (the aliases of today). Null: not paused. |
| `createdAt` | datetime | When you claimed it, ISO 8601 in UTC. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | The profile is not yours. List your profiles first. |
| 409 | `subdomain_taken` | Another account holds the label, it is reserved, or you hold a subdomain on this domain already. |
| 422 | `invalid_local` | The label breaks the label rule. Pick another one. |
| 422 | `domain_closed` | The domain takes no subdomains. Pick one whose `autoAliases` is true in `GET /v1/domains`. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/subdomains" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain":"rmail.ink","label":"alice"}'
```

```json
{
  "id": "9b1e4c2a-7d3f-4e5a-8c6b-2a1f0e9d8c7b",
  "name": "alice.rmail.ink",
  "label": "alice",
  "domain": "rmail.ink",
  "profile": null,
  "enabled": true,
  "paused": null,
  "createdAt": "2026-10-21T09:00:00.000Z"
}
```

### Change a subdomain

`PATCH /v1/subdomains/:id`

Turns a subdomain on or off, or moves its mail to a profile.

Off bounces mail to a new name; the aliases it made keep working.

Scope: `aliases:write`. MCP tool: `subdomains_update`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The subdomain id, as the list answers it. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | no | False: mail to a new name bounces. True: aliases make themselves again. |
| `profile` | string \| null | no | One of your profiles, or null for your own mailbox. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The subdomain id. |
| `name` | string | The whole subdomain: `alice.rmail.ink`. Mail to any name at it is yours. |
| `label` | string | The part you chose: `alice`. |
| `domain` | string | The open domain it sits on: `rmail.ink`. |
| `profile` | uuid \| null | The profile whose mailbox takes its mail and its aliases. Null: your own mailbox. |
| `enabled` | boolean | False: mail to a new name bounces. The aliases it made keep working. |
| `paused` | "limit" \| "daily_cap" \| null | Why no alias makes itself now: `limit` (you hold your alias limit) or `daily_cap` (the aliases of today). Null: not paused. |
| `createdAt` | datetime | When you claimed it, ISO 8601 in UTC. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No subdomain of yours has this id. List them first. |

**Example**

```bash
curl -s -X PATCH "https://api.rapidmail.work/v1/subdomains/9b1e4c2a-7d3f-4e5a-8c6b-2a1f0e9d8c7b" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
```

```json
{
  "id": "9b1e4c2a-7d3f-4e5a-8c6b-2a1f0e9d8c7b",
  "name": "alice.rmail.ink",
  "label": "alice",
  "domain": "rmail.ink",
  "profile": null,
  "enabled": false,
  "paused": null,
  "createdAt": "2026-10-21T09:00:00.000Z"
}
```

## Profiles

### List your profiles

`GET /v1/profiles`

Lists your profiles, one page at a time, oldest first.

A profile is one more mailbox of your account: its own address, its own mail and its own aliases. Nobody signs in to it, and it never sends. Send `next` back as `cursor` for the next page.

Scope: `profiles:read`. MCP tool: `profiles_list`.

**Query**

| Field | Type | Required | Description |
|---|---|---|---|
| `limit` | string | no | The page size, a whole number from 1 to 100. Left out: 100. |
| `cursor` | string | no | The `next` of the page before. Left out: the first page. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `profiles` | object[] | One page of profiles, oldest first. |
| `used` | integer | Every profile of the account. |
| `limit` | integer \| null | The most profiles your account may hold. Null: no limit. |
| `next` | string \| null | Send it back as `cursor` for the next page. Null on the last page. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 400 | `invalid_cursor` | The cursor is not one this API gave. Start without it. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/profiles" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "profiles": [
    {
      "id": "9b1d2c3e-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
      "address": "shop@rmail.ink",
      "local": "shop",
      "domain": "rmail.ink",
      "label": "Shop",
      "aliases": 3,
      "createdAt": "2026-10-19T09:12:44.000Z"
    }
  ],
  "used": 1,
  "limit": 10,
  "next": null
}
```

### Create a profile

`POST /v1/profiles`

Creates a profile: a new mailbox with its own address.

The address follows the same rules as an alias. Read its mail with `GET /v1/messages?profile=<id>`, and give it aliases with `POST /v1/aliases` and `profile`.

Scope: `profiles:write`. MCP tool: `profiles_create`.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `local` | string | yes | The part before the @. Letters, digits and single dots, starting with a letter, 3 to 32 characters. Upper case is stored as lower case. |
| `domain` | string | yes | A domain from `GET /v1/domains`. |
| `label` | string \| null | no | A note for you, at most 60 characters. Left out, null or "": no label. |

**Answer (201)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The profile id. |
| `address` | string | The profile's address: `shop@rmail.ink`. |
| `local` | string | The part before the @, in lower case. |
| `domain` | string | The part after the @. |
| `label` | string \| null | Your note on the profile, or null. |
| `aliases` | integer | How many aliases live in this profile's mailbox. |
| `createdAt` | datetime | When the profile was created, ISO 8601 in UTC. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 409 | `address_taken` | Someone holds the address. Try another local part or domain. |
| 422 | `profile_limit` | Your account holds its most profiles. Delete one first. |
| 422 | `invalid_local` | The local part breaks the format rule. Use letters, digits and single dots, 3 to 32 characters. |
| 422 | `domain_closed` | The domain is not in `GET /v1/domains`. Pick one from that list. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/profiles" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"local":"shop","domain":"rmail.ink","label":"Shop"}'
```

```json
{
  "id": "9b1d2c3e-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
  "address": "shop@rmail.ink",
  "local": "shop",
  "domain": "rmail.ink",
  "label": "Shop",
  "aliases": 0,
  "createdAt": "2026-10-19T09:12:44.000Z"
}
```

### Delete a profile

`DELETE /v1/profiles/:id`

Deletes a profile with its mailbox, its mail and its aliases.

The mail is gone at once and cannot come back. Nobody can take the profile’s address or its aliases’ addresses until `reservedUntil`.

Scope: `profiles:write`. MCP tool: `profiles_delete`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The profile id, as the list answers it. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `deleted` | true | Always true. |
| `address` | string | The address of the deleted profile. |
| `aliases` | integer | How many aliases were deleted with it. |
| `reservedUntil` | datetime | Nobody can take the profile's address or its aliases' addresses before this time. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No profile of yours has this id. List them first. |

**Example**

```bash
curl -s -X DELETE "https://api.rapidmail.work/v1/profiles/9b1d2c3e-4f5a-4b6c-8d7e-0f1a2b3c4d5e" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "deleted": true,
  "address": "shop@rmail.ink",
  "aliases": 3,
  "reservedUntil": "2027-10-19T09:20:00.000Z"
}
```

## Messages

### List your messages

`GET /v1/messages`

Lists the mail in one of your mailboxes, newest first.

Poll it to follow new mail: call it once without `since` and store `cursor`; then call it with `since=<cursor>` every 30 seconds or so, and store each new `cursor`, also from an empty answer. When more than `limit` messages arrived, one answer holds only the newest `limit`. Send the same `to` or `profile` with every cursor.

Scope: `messages:read`. MCP tool: `messages_list`.

**Query**

| Field | Type | Required | Description |
|---|---|---|---|
| `since` | string | no | The `cursor` of an earlier answer: only mail that arrived after it. Left out: the newest mail. |
| `to` | string | no | Only mail sent to this address: your main address, a profile, or an alias of either. With neither `to` nor `profile`: your main mailbox. |
| `profile` | string | no | The whole mailbox of this profile. Send `to` or `profile`, not both. |
| `limit` | string | no | At most this many messages, a whole number from 1 to 100. Left out: 50. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `messages` | object[] | Newest first. |
| `cursor` | string | Opaque. Store it as it is and send it back as `since` to get only the mail that arrived later. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 400 | `invalid_cursor` | The cursor is not one this API gave, or it is from another mailbox. Start again without `since`. |
| 404 | `not_found` | The account holds no such address or profile. Check `to` or `profile`. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/messages?to=alice.ter%40rmail.ink&limit=20" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "messages": [
    {
      "id": "m2f9a1",
      "receivedAt": "2026-10-19T09:30:02.000Z",
      "from": "orders@store.example",
      "to": [
        "alice.ter@rmail.ink"
      ],
      "subject": "Your order shipped",
      "preview": "Your parcel is on its way and arrives on",
      "hasAttachments": false,
      "alias": "alice.ter@rmail.ink"
    }
  ],
  "cursor": "3k"
}
```

### Wait for new mail

`GET /v1/messages/wait`

Waits up to 60 seconds for the next messages after a cursor, and answers them oldest first.

It answers at once when mail newer than `since` exists, else when the first message arrives, else at `timeout` with no message and `timedOut: true`. The answer cursor stops right after the last message it answers, so a script that walks the cursors misses no message. When `more` is true, call again at once. A key holds at most 5 waiting requests.

Scope: `messages:read`. MCP tool: `messages_wait`.

**Query**

| Field | Type | Required | Description |
|---|---|---|---|
| `since` | string | no | A cursor from `GET /v1/messages` or an earlier wait. Left out: the mailbox as it is now. |
| `to` | string | no | Only mail sent to this address: your main address, a profile, or an alias of either. With neither `to` nor `profile`: your main mailbox. |
| `profile` | string | no | The whole mailbox of this profile. Send `to` or `profile`, not both. |
| `timeout` | string | no | Seconds to wait for new mail, 1 to 60. Left out: 30. |
| `limit` | string | no | At most this many messages, 1 to 20. Left out: 10. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `messages` | object[] | The new messages, oldest first, each with `code` and `link`. |
| `cursor` | string | Send it back as `since` for the mail after these messages. It stops right after the last one. |
| `more` | boolean | True when more mail waits past this answer: call again at once with `cursor`. |
| `timedOut` | boolean | True when the wait ended at `timeout` with no new message. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 400 | `invalid_cursor` | The cursor is not one this API gave, or it is from another mailbox. Start again without `since`. |
| 404 | `not_found` | The account holds no such address or profile. Check `to` or `profile`. |
| 429 | `too_many_waits` | This key holds 5 waiting requests already. Wait `Retry-After` seconds, then call again. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/messages/wait?to=alice.ter%40rmail.ink&since=3k&timeout=60" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "messages": [
    {
      "id": "m2f9a1",
      "receivedAt": "2026-10-19T09:30:02.000Z",
      "from": "orders@store.example",
      "to": [
        "alice.ter@rmail.ink"
      ],
      "subject": "482913 is your sign-in code",
      "preview": "Use 482913 to sign in.",
      "hasAttachments": false,
      "alias": "alice.ter@rmail.ink",
      "cc": [],
      "textBody": "Use 482913 to sign in. The code works for 10 minutes.",
      "htmlBody": null,
      "attachments": [],
      "code": "482913",
      "link": null
    }
  ],
  "cursor": "3m",
  "more": false,
  "timedOut": false
}
```

### Read the newest message

`GET /v1/messages/latest`

Reads the newest message within `maxAge`, with its one-time code, or waits for the next one.

One call for a script that fills a sign-up form on another site and then needs the code: `maxAge=120&wait=60` answers the code of a message from the last 2 minutes, or of the next one within 60 seconds. Without a message it answers `{ "message": null }`.

Scope: `messages:read`. MCP tool: `messages_latest`.

**Query**

| Field | Type | Required | Description |
|---|---|---|---|
| `to` | string | no | Only mail sent to this address: your main address, a profile, or an alias of either. With neither `to` nor `profile`: your main mailbox. |
| `profile` | string | no | The whole mailbox of this profile. Send `to` or `profile`, not both. |
| `maxAge` | string | no | Only a message that arrived in the last this many seconds, 1 to 86,400. Left out: 600. |
| `wait` | string | no | With no such message, wait up to this many seconds for the next one, 0 to 60. Left out: 0. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `message` | object \| null | The newest message within `maxAge`, or the next one; else null. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | The account holds no such address or profile. Check `to` or `profile`. |
| 429 | `too_many_waits` | This key holds 5 waiting requests already. Wait `Retry-After` seconds, then call again. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/messages/latest?to=alice.ter%40rmail.ink&maxAge=120&wait=60" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "message": {
    "id": "m2f9a1",
    "receivedAt": "2026-10-19T09:30:02.000Z",
    "from": "orders@store.example",
    "to": [
      "alice.ter@rmail.ink"
    ],
    "subject": "482913 is your sign-in code",
    "preview": "Use 482913 to sign in.",
    "hasAttachments": false,
    "alias": "alice.ter@rmail.ink",
    "cc": [],
    "textBody": "Use 482913 to sign in. The code works for 10 minutes.",
    "htmlBody": null,
    "attachments": [],
    "code": "482913",
    "link": null
  }
}
```

### Read a message

`GET /v1/messages/:id`

Reads one message with its text and HTML bodies, and its one-time code and sign-in link.

A message id is unique within its mailbox only, so send the same `to` or `profile` as the list that gave it. `code` and `link` are found when the message is read and never stored. `GET /v1/messages` does not carry them.

Scope: `messages:read`. MCP tool: `messages_get`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The message id from `GET /v1/messages`. |

**Query**

| Field | Type | Required | Description |
|---|---|---|---|
| `to` | string | no | Only mail sent to this address: your main address, a profile, or an alias of either. With neither `to` nor `profile`: your main mailbox. |
| `profile` | string | no | The whole mailbox of this profile. Send `to` or `profile`, not both. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `id` | string | The message id. It is unique within its mailbox only. |
| `receivedAt` | datetime | When the message arrived, ISO 8601 in UTC. |
| `from` | string | The sender, as the message names it. |
| `to` | string[] | The `To` addresses. |
| `subject` | string | The subject line, or "" when there is none. |
| `preview` | string | The first words of the text body. |
| `hasAttachments` | boolean | True when the message carries at least one attachment. |
| `alias` | string \| null | The first `To` address that is one of your aliases. Null for mail to a main address. |
| `cc` | string[] | The `Cc` addresses. |
| `textBody` | string | The plain text body. |
| `htmlBody` | string \| null | The HTML body, or null when the message has none. |
| `attachments` | object[] | The attachments. The API does not serve their content. |
| `code` | string \| null | The one-time code of a sign-in or confirmation message, without its gaps: `482913`. Null when there is none, or when you marked the message "Not a code". |
| `link` | string \| null | The one sign-in or confirmation link of the message. Null when there is none, when the message did not pass DMARC for its From domain, or when you marked it "Not a code". Nothing on our side opens it. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No such message in this mailbox, or no such address or profile. Check the id and `to` or `profile`. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/messages/m2f9a1" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "id": "m2f9a1",
  "receivedAt": "2026-10-19T09:30:02.000Z",
  "from": "orders@store.example",
  "to": [
    "alice.ter@rmail.ink"
  ],
  "subject": "Your order shipped",
  "preview": "Your parcel is on its way and arrives on",
  "hasAttachments": false,
  "alias": "alice.ter@rmail.ink",
  "cc": [],
  "textBody": "Your parcel is on its way and arrives on Friday.",
  "htmlBody": null,
  "attachments": [],
  "code": null,
  "link": null
}
```

### List your folders

`GET /v1/folders`

Lists the folders of your own mailbox.

Use a folder `id` in `POST /v1/auto-delete`. `role` names the folders the mail server knows: `inbox`, `trash`, and so on.

Scope: `messages:read`. MCP tool: `folders_list`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `folders` | object[] | The folders of your own mailbox. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/folders" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "folders": [
    {
      "id": "a",
      "name": "Inbox",
      "role": "inbox"
    },
    {
      "id": "b7",
      "name": "Blocked",
      "role": null
    }
  ]
}
```

## Forwarding

### List forwarding addresses

`GET /v1/forward-targets`

Lists the outside addresses your account may forward to.

An address forwards nothing until it is `confirmed`: its owner follows the link of the confirmation mail. `used` and `limit` say how many more you can add.

Scope: `forwards:read`. MCP tool: `forward_targets_list`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `targets` | object[] | Your forwarding addresses, oldest first. |
| `used` | integer | Forwarding addresses your account holds. |
| `limit` | integer | The most forwarding addresses your account may hold. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/forward-targets" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "targets": [
    {
      "id": "2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58",
      "address": "alice@outside.example",
      "state": "confirmed",
      "confirmedAt": "2026-10-20T10:04:00.000Z",
      "createdAt": "2026-10-20T10:00:00.000Z"
    }
  ],
  "used": 1,
  "limit": 5
}
```

### Add a forwarding address

`POST /v1/forward-targets`

Adds an outside address and sends it one confirmation mail.

The address starts `pending`. Rapidmail sends it one mail with a link to confirm or decline. Only a `confirmed` address can receive forwarded mail. This is the one route that makes Rapidmail send mail.

Scope: `forwards:write`. MCP tool: `forward_targets_create`.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `address` | email | yes | The outside address to forward to, at most 254 characters. It gets one confirmation mail. |

**Answer (201)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The forwarding address id. |
| `address` | string | The outside address mail is forwarded to. |
| `state` | "pending" \| "confirmed" \| "declined" \| "stopped" | `pending` until the address answers its confirmation mail, then `confirmed` or `declined`. `stopped`: the address stopped forwarding later. |
| `confirmedAt` | datetime \| null | When the address confirmed, or null. |
| `createdAt` | datetime | When the address was added, ISO 8601 in UTC. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 403 | `account_pending` | An operator has not approved your account yet. Forwarding unlocks once it is approved. |
| 409 | `target_exists` | You added this address already. List your addresses. |
| 422 | `forward_target_refused` | The address is on a Rapidmail domain, is one of your own addresses, or is reserved. Use an outside address. |
| 422 | `forward_target_limit` | Your account holds its most forwarding addresses. Remove one first. |
| 422 | `target_declined` | The owner of the address declined forwarding within the last days. Ask them, then try again later. |
| 503 | `forward_disabled` | The operators turned forwarding off. Try again later. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/forward-targets" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"address":"alice@outside.example"}'
```

```json
{
  "id": "2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58",
  "address": "alice@outside.example",
  "state": "pending",
  "confirmedAt": null,
  "createdAt": "2026-10-20T10:00:00.000Z"
}
```

### Resend the confirmation

`POST /v1/forward-targets/:id/resend`

Sends the confirmation mail of a pending address again.

The new mail carries new links, and the links of the earlier mail stop working.

Scope: `forwards:write`. MCP tool: `forward_targets_resend`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The forwarding address id, as the list answers it. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The forwarding address id. |
| `address` | string | The outside address mail is forwarded to. |
| `state` | "pending" \| "confirmed" \| "declined" \| "stopped" | `pending` until the address answers its confirmation mail, then `confirmed` or `declined`. `stopped`: the address stopped forwarding later. |
| `confirmedAt` | datetime \| null | When the address confirmed, or null. |
| `createdAt` | datetime | When the address was added, ISO 8601 in UTC. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 403 | `account_pending` | An operator has not approved your account yet. Forwarding unlocks once it is approved. |
| 404 | `not_found` | No forwarding address of yours has this id. List them first. |
| 409 | `target_exists` | The address is not pending: it answered already. |
| 429 | `resend_limit` | The mail went out too often today. Try again tomorrow. |
| 503 | `forward_disabled` | The operators turned forwarding off. Try again later. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/forward-targets/2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58/resend" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "id": "2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58",
  "address": "alice@outside.example",
  "state": "pending",
  "confirmedAt": null,
  "createdAt": "2026-10-20T10:00:00.000Z"
}
```

### Remove a forwarding address

`DELETE /v1/forward-targets/:id`

Removes a forwarding address and every forward to it.

No mail goes to the address after this. To forward again, add it again: it gets a new confirmation mail.

Scope: `forwards:write`. MCP tool: `forward_targets_delete`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The forwarding address id, as the list answers it. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `deleted` | true | Always true. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 403 | `account_pending` | An operator has not approved your account yet. Forwarding unlocks once it is approved. |
| 404 | `not_found` | No forwarding address of yours has this id. List them first. |
| 503 | `forward_disabled` | The operators turned forwarding off. Try again later. |

**Example**

```bash
curl -s -X DELETE "https://api.rapidmail.work/v1/forward-targets/2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "deleted": true
}
```

### List your forwards

`GET /v1/forwards`

Lists the forward of each alias and of All mail.

A forward with `alias` null forwards All mail of your own mailbox. A forward pauses after too many permanent failures in the bounce window, when it sends too much, or when its address stops forwarding: `pausedReason` says which.

Scope: `forwards:read`. MCP tool: `forwards_list`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `forwards` | object[] | Your forwards, oldest first. |
| `bounceCounting` | boolean | False when bounces cannot reach Rapidmail, so no forward pauses for bounces. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/forwards" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "forwards": [
    {
      "id": "9c1e3a5b-7d2f-4e6a-8b0c-4d2f1e3a5b67",
      "alias": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
      "target": "2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58",
      "state": "active",
      "pausedReason": null,
      "bounces": 0,
      "forwarded": 42,
      "lastForwardedAt": "2026-10-21T07:15:00.000Z"
    }
  ],
  "bounceCounting": true
}
```

### Set a forward

`PUT /v1/forwards`

Sets or removes the forward of one alias, or of All mail.

Send `alias` (an alias id, or null for All mail) and `target` (a confirmed address id, or null to remove the forward). A new target starts the forward `active` with no bounces. The same target keeps its counts.

Scope: `forwards:write`. MCP tool: `forwards_set`.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `alias` | uuid \| null | yes | The id of one of your aliases, or null for All mail. |
| `target` | uuid \| null | yes | The id of a confirmed forwarding address, or null to remove the forward of that scope. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `forward` | object \| null | The forward of that scope, or null after a removal. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 403 | `account_pending` | An operator has not approved your account yet. Forwarding unlocks once it is approved. |
| 404 | `not_found` | No such alias or forwarding address of yours. List them first. |
| 422 | `target_not_confirmed` | The forwarding address has not confirmed. Wait for its owner, or resend the mail. |
| 422 | `forward_unavailable` | The alias lives in a profile, and a profile alias cannot forward. Pick another alias. |
| 503 | `forward_disabled` | The operators turned forwarding off. Try again later. |

**Example**

```bash
curl -s -X PUT "https://api.rapidmail.work/v1/forwards" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"alias":"4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69","target":"2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58"}'
```

```json
{
  "forward": {
    "id": "9c1e3a5b-7d2f-4e6a-8b0c-4d2f1e3a5b67",
    "alias": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
    "target": "2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58",
    "state": "active",
    "pausedReason": null,
    "bounces": 0,
    "forwarded": 42,
    "lastForwardedAt": "2026-10-21T07:15:00.000Z"
  }
}
```

### Resume a forward

`POST /v1/forwards/:id/resume`

Clears the bounce count of a paused forward and sets it active.

The forward sends new mail again. A forward whose address stopped forwarding cannot resume.

Scope: `forwards:write`. MCP tool: `forwards_resume`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The forward id, as the list answers it. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The forward id. |
| `alias` | uuid \| null | The alias id, or null: All mail of your own mailbox. |
| `target` | uuid | The id of the forwarding address. |
| `state` | "active" \| "paused" | `active` forwards new mail; `paused` forwards nothing until you resume it. |
| `pausedReason` | "bounces" \| "rate" \| "target_stopped" \| null | Why the forward is paused: `bounces`, `rate` or `target_stopped`. Null while it is active. |
| `bounces` | integer | Permanent failures inside the bounce window. |
| `forwarded` | integer | Messages forwarded so far. |
| `lastForwardedAt` | datetime \| null | The last forwarded message, or null. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 403 | `account_pending` | An operator has not approved your account yet. Forwarding unlocks once it is approved. |
| 404 | `not_found` | No forward of yours has this id. List them first. |
| 422 | `target_not_confirmed` | The forwarding address stopped forwarding. Pick another address with `PUT /v1/forwards`. |
| 503 | `forward_disabled` | The operators turned forwarding off. Try again later. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/forwards/9c1e3a5b-7d2f-4e6a-8b0c-4d2f1e3a5b67/resume" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "id": "9c1e3a5b-7d2f-4e6a-8b0c-4d2f1e3a5b67",
  "alias": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
  "target": "2b8d4f6a-1c3e-4a5b-9d7f-0e1c2b3a4d58",
  "state": "active",
  "pausedReason": null,
  "bounces": 0,
  "forwarded": 42,
  "lastForwardedAt": "2026-10-21T07:15:00.000Z"
}
```

## Auto-delete

### List auto-delete rules

`GET /v1/auto-delete`

Lists your auto-delete rules.

A rule names one alias or one folder of your own mailbox, and `days`. Once an hour, mail under a rule that is older than `days` is deleted for good. Starred mail stays. `used` and `limit` say how many more rules you can create.

Scope: `autodelete:read`. MCP tool: `autodelete_list`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `rules` | object[] | Your auto-delete rules, oldest first. |
| `used` | integer | Rules your account holds. |
| `limit` | integer | The most rules your account may hold. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/auto-delete" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "rules": [
    {
      "id": "5d7f9b1c-3e5a-4c7e-9f1b-6a8c0e2d4f61",
      "alias": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
      "folder": null,
      "days": 30,
      "deleted": 18,
      "lastRunAt": "2026-10-21T06:00:00.000Z"
    }
  ],
  "used": 1,
  "limit": 50
}
```

### Create an auto-delete rule

`POST /v1/auto-delete`

Creates a rule that deletes old mail of one alias or one folder.

Send `alias` or `folder`, not both, and `days` from 1 to 3650. Folder ids come from `GET /v1/folders`. The rule deletes nothing until the next hourly run.

Scope: `autodelete:write`. MCP tool: `autodelete_create`.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `alias` | string | no | The id of one of your aliases. Send `alias` or `folder`, not both. |
| `folder` | string | no | The id of a folder from `GET /v1/folders`. Send `alias` or `folder`. |
| `days` | integer | yes | Delete mail older than this many days, 1 to 3650. |

**Answer (201)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The rule id. |
| `alias` | uuid \| null | The alias id of the rule, or null for a folder rule. |
| `folder` | string \| null | The folder id of the rule, or null for an alias rule. |
| `days` | integer | Mail older than this many days is deleted. |
| `deleted` | integer | Messages this rule deleted so far. |
| `lastRunAt` | datetime \| null | The last hourly run that applied the rule, or null. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No alias or folder of yours has this id. List them first. |
| 409 | `rule_exists` | That alias or folder has a rule already. Change its `days` instead. |
| 422 | `rule_limit` | Your account holds its most rules. Delete one first. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/auto-delete" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"alias":"4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69","days":30}'
```

```json
{
  "id": "5d7f9b1c-3e5a-4c7e-9f1b-6a8c0e2d4f61",
  "alias": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
  "folder": null,
  "days": 30,
  "deleted": 0,
  "lastRunAt": null
}
```

### Change an auto-delete rule

`PATCH /v1/auto-delete/:id`

Changes the days of a rule.

The next hourly run applies the new `days`.

Scope: `autodelete:write`. MCP tool: `autodelete_update`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The rule id, as the list answers it. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `days` | integer | yes | Delete mail older than this many days, 1 to 3650. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `id` | uuid | The rule id. |
| `alias` | uuid \| null | The alias id of the rule, or null for a folder rule. |
| `folder` | string \| null | The folder id of the rule, or null for an alias rule. |
| `days` | integer | Mail older than this many days is deleted. |
| `deleted` | integer | Messages this rule deleted so far. |
| `lastRunAt` | datetime \| null | The last hourly run that applied the rule, or null. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No rule of yours has this id. List them first. |

**Example**

```bash
curl -s -X PATCH "https://api.rapidmail.work/v1/auto-delete/5d7f9b1c-3e5a-4c7e-9f1b-6a8c0e2d4f61" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"days":14}'
```

```json
{
  "id": "5d7f9b1c-3e5a-4c7e-9f1b-6a8c0e2d4f61",
  "alias": "4f0c6a8e-2b1d-4c3e-9a7f-1e2d3c4b5a69",
  "folder": null,
  "days": 14,
  "deleted": 18,
  "lastRunAt": "2026-10-21T06:00:00.000Z"
}
```

### Delete an auto-delete rule

`DELETE /v1/auto-delete/:id`

Deletes a rule. Mail it did not delete yet stays.

Mail the rule deleted earlier does not come back.

Scope: `autodelete:write`. MCP tool: `autodelete_delete`.

**Path**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The rule id, as the list answers it. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `deleted` | true | Always true. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | No rule of yours has this id. List them first. |

**Example**

```bash
curl -s -X DELETE "https://api.rapidmail.work/v1/auto-delete/5d7f9b1c-3e5a-4c7e-9f1b-6a8c0e2d4f61" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "deleted": true
}
```

## Webhooks

### Show the webhook

`GET /v1/webhook`

Shows this key's webhook URL and its state.

Every new message of the account, in the main mailbox and in each profile, goes to one URL per key.

Scope: `webhooks:read`. MCP tool: `webhook_get`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `url` | string | The HTTPS URL that gets the events. |
| `createdAt` | datetime | When the webhook was set, ISO 8601 in UTC. |
| `disabledAt` | datetime \| null | When the webhook was turned off. Null: it is on. `PUT /v1/webhook` turns it on again. |
| `disabledReason` | "failures" \| "gone" \| "account" \| null | Why it is off: `failures` after 5 failed attempts, `gone` after a 410, `account` when the account was suspended or deleted. |
| `lastDeliveryAt` | datetime \| null | The last delivery the receiver took with a 2xx, or null. |
| `lastStatus` | integer \| null | The HTTP status of the last attempt, or null before the first. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | This key has no webhook. Set one with `PUT /v1/webhook`. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/webhook" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "url": "https://hooks.example.com/rapidmail",
  "createdAt": "2026-10-21T09:00:00.000Z",
  "disabledAt": null,
  "disabledReason": null,
  "lastDeliveryAt": "2026-10-21T09:41:03.000Z",
  "lastStatus": 200
}
```

### Set the webhook

`PUT /v1/webhook`

Sets this key's webhook URL, turns it on, and answers the signing secret once.

The key also needs `messages:read`, because the events carry mail. A webhook that was turned off is on again, and gets the mail that arrives from now on. Each `POST` carries `Rapidmail-Event` (`message.received` or `ping`), `Rapidmail-Delivery` (the delivery id; a retry carries the same id) and `Rapidmail-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is the hex HMAC-SHA256 of `<t>.<raw body>` keyed with the whole secret. Compare it in constant time and refuse a `t` older than 5 minutes. The body of `message.received` is `{ "event", "delivery", "message" }`; `message` has the list fields and `profile`, `code` and `link`. A 2xx is success. A delivery gets at most 5 attempts, at 0, 1, 5, 30 and 120 minutes after the message arrived; after the fifth failure or a 410 the webhook is turned off. Redirects are not followed.

Scope: `webhooks:write`. MCP tool: `webhook_set`.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes | An `https` URL on port 443 whose host resolves to public addresses only. |

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `url` | string | The HTTPS URL that gets the events. |
| `createdAt` | datetime | When the webhook was set, ISO 8601 in UTC. |
| `disabledAt` | datetime \| null | When the webhook was turned off. Null: it is on. `PUT /v1/webhook` turns it on again. |
| `disabledReason` | "failures" \| "gone" \| "account" \| null | Why it is off: `failures` after 5 failed attempts, `gone` after a 410, `account` when the account was suspended or deleted. |
| `lastDeliveryAt` | datetime \| null | The last delivery the receiver took with a 2xx, or null. |
| `lastStatus` | integer \| null | The HTTP status of the last attempt, or null before the first. |
| `secret` | string | `whsec_` and 43 characters: the key of the signature. Shown once; rotate to get a new one. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 403 | `missing_scope` | The key lacks `messages:read`. Create a key with both scopes. |
| 422 | `webhook_url_refused` | The URL is not `https` on port 443, or its host resolves to an address that is not public. |
| 503 | `api_disabled` | Webhooks are turned off on this server. Poll or wait for mail instead. |

**Example**

```bash
curl -s -X PUT "https://api.rapidmail.work/v1/webhook" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/rapidmail"}'
```

```json
{
  "url": "https://hooks.example.com/rapidmail",
  "createdAt": "2026-10-21T09:00:00.000Z",
  "disabledAt": null,
  "disabledReason": null,
  "lastDeliveryAt": "2026-10-21T09:41:03.000Z",
  "lastStatus": 200,
  "secret": "whsec_q3Zx81kLp0QvT4mNa9RcW3eYb6Pq5sTu2wHj7Ff8Gg"
}
```

### Delete the webhook

`DELETE /v1/webhook`

Deletes this key's webhook and the deliveries that wait for it.

No event goes out after it. Set a new URL with `PUT /v1/webhook`.

Scope: `webhooks:write`. MCP tool: `webhook_delete`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `deleted` | true | Always true. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | This key has no webhook. Set one with `PUT /v1/webhook`. |

**Example**

```bash
curl -s -X DELETE "https://api.rapidmail.work/v1/webhook" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "deleted": true
}
```

### Rotate the secret

`POST /v1/webhook/rotate`

Makes a new signing secret. The old one stops at once.

The next event is signed with the new secret. Store it before you answer the next delivery.

Scope: `webhooks:write`. MCP tool: `webhook_rotate`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `url` | string | The HTTPS URL that gets the events. |
| `createdAt` | datetime | When the webhook was set, ISO 8601 in UTC. |
| `disabledAt` | datetime \| null | When the webhook was turned off. Null: it is on. `PUT /v1/webhook` turns it on again. |
| `disabledReason` | "failures" \| "gone" \| "account" \| null | Why it is off: `failures` after 5 failed attempts, `gone` after a 410, `account` when the account was suspended or deleted. |
| `lastDeliveryAt` | datetime \| null | The last delivery the receiver took with a 2xx, or null. |
| `lastStatus` | integer \| null | The HTTP status of the last attempt, or null before the first. |
| `secret` | string | `whsec_` and 43 characters: the key of the signature. Shown once; rotate to get a new one. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | This key has no webhook. Set one with `PUT /v1/webhook`. |
| 503 | `api_disabled` | Webhooks are turned off on this server. Poll or wait for mail instead. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/webhook/rotate" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "url": "https://hooks.example.com/rapidmail",
  "createdAt": "2026-10-21T09:00:00.000Z",
  "disabledAt": null,
  "disabledReason": null,
  "lastDeliveryAt": "2026-10-21T09:41:03.000Z",
  "lastStatus": 200,
  "secret": "whsec_q3Zx81kLp0QvT4mNa9RcW3eYb6Pq5sTu2wHj7Ff8Gg"
}
```

### Test the webhook

`POST /v1/webhook/test`

Sends one `ping` event and answers the receiver's status.

The ping is signed as every event is, with `Rapidmail-Event: ping` and no `message`.

Scope: `webhooks:write`. MCP tool: `webhook_test`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `status` | integer \| null | The receiver's HTTP status, or null when it gave none. |
| `ok` | boolean | True for a 2xx. |

**Errors**

| Status | Code | What to do |
|---|---|---|
| 404 | `not_found` | This key has no webhook. Set one with `PUT /v1/webhook`. |
| 503 | `api_disabled` | Webhooks are turned off on this server. Poll or wait for mail instead. |

**Example**

```bash
curl -s -X POST "https://api.rapidmail.work/v1/webhook/test" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "status": 200,
  "ok": true
}
```

## Keys

### List your API keys

`GET /v1/keys`

Lists your API keys, newest first, without their secrets.

`current` is true on the key that made the call. Create, rotate and revoke keys in Settings, API; no API call can do it.

Scope: `keys:read`. MCP tool: `keys_list`.

**Answer (200)**

| Field | Type | Description |
|---|---|---|
| `keys` | object[] | Your keys, newest first, revoked ones included. |
| `active` | integer | Keys that are not revoked. |
| `limit` | integer | The most keys that are not revoked your account may hold. |

**Example**

```bash
curl -s "https://api.rapidmail.work/v1/keys" \
  -H "Authorization: Bearer $RAPIDMAIL_KEY"
```

```json
{
  "keys": [
    {
      "id": "2c7d9e1f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
      "name": "Inbox poller",
      "prefix": "rm_live_7Hq2",
      "scopes": [
        "aliases:read",
        "messages:read",
        "keys:read"
      ],
      "createdAt": "2026-10-19T08:00:00.000Z",
      "lastUsedAt": "2026-10-21T07:41:00.000Z",
      "revokedAt": null,
      "current": true
    }
  ],
  "active": 1,
  "limit": 25
}
```
