Simple Inbox

API guide

Versioned same-origin endpoints for authentication, inbox reads, settings, downloads, and sending.

The browser-facing API is versioned under /api/v1 on the Worker origin. The single Worker dispatches those requests to the API module in-process; clients cannot call the private mail module or its internal routes.

Use the generated OpenAPI 3.1 document at /api/v1/openapi.json as the machine-readable contract. Check /api/v1/capabilities before depending on optional behavior.

Authentication

Browser clients use the HTTP-only session cookie created by magic-link verification. Non-browser integrations send a separately provisioned bearer token:

Authorization: Bearer YOUR_OPAQUE_API_TOKEN
Accept: application/json

Tokens can carry read, send, and settings scopes. A bearer token takes precedence when an Authorization header is present; do not send an invalid bearer header alongside a valid browser session.

Session-authenticated mutations must be same-origin. API-token callers are not subject to the cookie origin check, but still need the appropriate scope and mailbox membership.

Endpoint summary

MethodPathAuthPurpose
GET/healthPublicAPI health
GET/capabilitiesPublicSupported optional behavior
GET/openapi.jsonPublicOpenAPI 3.1 contract
POST/auth/magic-linksPublicRequest a one-time sign-in link
POST/auth/magic-links/verifyPublicConsume a link and create a session
GET/auth/sessionPublic / optional sessionInspect the current browser session
POST/auth/logoutSessionRevoke the session and clear its cookie
GET/mailboxesreadList authorized mailboxes and folder counts
PATCH/mailboxes/{mailboxId}settingsChange sender alias and/or forwarding target
GET/threadsreadList, filter, search, and paginate threads
GET/threads/{threadId}readFetch a thread and its chronological messages
POST/threads/{threadId}/readsettingsMark inbound messages in a thread read
POST/threads/{threadId}/archivesettingsRemove messages from Inbox
DELETE/threads/{threadId}/archivesettingsMove messages to Inbox
POST/messagessendSend a new multipart message
POST/threads/{threadId}/messagessendSend a multipart reply
GET/messages/{messageId}/rawreadDownload the immutable RFC 822 source
GET/messages/{messageId}/attachments/{attachmentId}readDownload an authorized attachment projection

Prefix every path in the table with /api/v1 when calling the public web origin.

curl --request POST 'https://inbox.example.test/api/v1/auth/magic-links' \
  --header 'content-type: application/json' \
  --data '{"email":"owner@example.test"}'

The response is always intentionally generic:

{ "status": "accepted" }

The email link points to the browser verification flow. Direct API clients can exchange its token at POST /api/v1/auth/magic-links/verify with {"token":"..."}. A token can be consumed only once.

List and search threads

curl 'https://inbox.example.test/api/v1/threads?mailboxId=01996f7a-7bcd-7abc-8def-1123456789ab&folder=inbox&unread=1&limit=25' \
  --header 'authorization: Bearer YOUR_OPAQUE_API_TOKEN'

Supported query fields:

FieldValues
mailboxIdRequired lower-case UUIDv7 mailbox ID
folderinbox, starred, sent, all, spam, trash (legacy archive also accepted); defaults to inbox
unread1 to return unread threads only, or 0
qTrimmed full-text search query, 1–200 characters
limit1–50; defaults to 25
cursorOpaque nextCursor from the previous response

Cursors are stable continuation tokens, not offsets. Do not decode, edit, or persist them as record identifiers.

Update mailbox settings

Supply at least one field. null clears a value.

curl --request PATCH \
  'https://inbox.example.test/api/v1/mailboxes/01996f7a-7bcd-7abc-8def-1123456789ab' \
  --header 'authorization: Bearer YOUR_OPAQUE_API_TOKEN' \
  --header 'content-type: application/json' \
  --data '{"senderAlias":"Example Support","forwardTo":"owner@example.test"}'

senderAlias changes the display name, not the envelope mailbox address. forwardTo controls the optional post-capture forwarding destination.

Send a new message

Send endpoints accept multipart/form-data; let the HTTP client generate its boundary. Repeat to, cc, bcc, and attachments fields as needed.

curl --request POST 'https://inbox.example.test/api/v1/messages' \
  --header 'authorization: Bearer YOUR_OPAQUE_API_TOKEN' \
  --header 'idempotency-key: 01996f7a-7bcd-7abc-8def-b123456789ab' \
  --form 'mailboxId=01996f7a-7bcd-7abc-8def-1123456789ab' \
  --form 'to=Customer <customer@example.test>' \
  --form 'cc=Accounts <accounts@example.test>' \
  --form 'subject=Example delivery' \
  --form 'body=This is a synthetic message.' \
  --form 'format=plain' \
  --form 'attachments=@./synthetic-note.txt;type=text/plain'

A reply uses /api/v1/threads/{threadId}/messages. Omit mailboxId; the API derives it from the authorized thread. You may add targetMessageId to bind reply headers to a specific inbound message.

The idempotency-key must be 16–128 URL-safe ASCII characters. Repeat the same key only for the same logical request. A different payload with an existing key returns 409 idempotency_conflict.

Successful submission returns 201 or 202 and a state document:

{
  "id": "01996f7a-7bcd-7abc-8def-9123456789ab",
  "idempotencyKey": "01996f7a-7bcd-7abc-8def-b123456789ab",
  "state": "sent",
  "threadId": "01996f7a-7bcd-7abc-8def-2123456789ab",
  "messageId": "01996f7a-7bcd-7abc-8def-3123456789ab",
  "acceptedAt": "2026-08-01T00:00:00.000Z",
  "completedAt": "2026-08-01T00:00:01.000Z",
  "safeErrorCode": null
}

Possible states are queued, sending, sent, failed, and unknown. Treat unknown as an ambiguous provider outcome: retain the original idempotency key for reconciliation and never create a duplicate by switching keys.

Limits

LimitValue
Combined To/CC/BCC recipients50
Attachments per API request20
Size of one attachment10 MiB
Total uploaded attachment bytes20 MiB
Outbound provider wire budgetBelow 5 MiB after safety margin and MIME/base64 overhead
Inbound raw message25 MiB
Threads per page50 maximum

Encoded wire size is larger than the uploaded files. A multipart request can pass upload validation and still exceed the provider budget after headers, Markdown rendering, MIME structure, and base64 encoding are added.

Errors and request IDs

JSON failures use one stable envelope:

{
  "error": {
    "code": "validation_failed",
    "message": "The request was not valid.",
    "requestId": "01JZXY8J6VSK8PKSRM3R7S3B2G",
    "details": {
      "issues": ["subject: String must contain at least 1 character"]
    }
  }
}

The same opaque ID appears in the x-request-id response header. Include it when reporting an operational problem, but do not attach raw messages, tokens, or attachment contents.

Common statuses are 400 validation, 401 missing or expired authentication, 403 scope or CSRF, 404 absent or unauthorized resource, 409 idempotency conflict, 413 request size, 429 rate limit, and 503 unavailable dependency.

Raw-message responses use message/rfc822; attachment responses are binary. Do not call .json() on successful download responses.

HTML preferences and previews

Mailbox settings include boolean forwardHtml and renderHtml flags. forwardHtml defaults to true; renderHtml defaults to false. Update either with PATCH /api/v1/mailboxes/{mailboxId}; omitted preferences are preserved. Only the mailbox owner with settings permission can make changes. Forwarding changes apply to future deliveries.

GET /api/v1/messages/{messageId}/html returns an authenticated text/html preview when the mailbox allows HTML display and the original MIME is retained. The response is private and uncached and must be embedded in an opaque-origin sandbox. Missing, unauthorized, disabled, or expired resources do not expose raw mail. The inbox falls back to plain text if a preview is unavailable or exceeds 4 MB.

Alias and message state actions

GET /mailboxes includes whitelisted for each owner-scoped alias. Daily navigation shows only whitelisted records; GET /threads?mailboxId=other&folder=inbox groups quiet aliases. The same other scope works with search, pagination, and every folder.

POST /mailboxes accepts { "address": "team@example.test", "forward": true } to create or promote an inbox. The domain must be the configured domain or one already receiving mail. PATCH mailbox settings accepts whitelisted; hiding a mailbox suppresses forwarding independently of its stored destination preference.

PATCH /threads/{threadId}/state accepts optional read, starred, and location fields. At least one action is required. Locations are inbox, archive, spam, trash, restore, and not_spam. Optional messageIds (1–80 IDs) restrict the action to messages in that thread. Use explicit IDs to mark displayed messages read without consuming new arrivals. Without IDs, an action affects the whole conversation. These mutations require settings scope and same-origin protection for cookies.

GET /spam-rules lists the actor's blacklist. The installation owner can POST /spam-rules with { "kind": "recipient", "value": "unused@example.test" }, or kind sender / domain. DELETE /spam-rules/{ruleId} removes an owned rule idempotently. Domain values include subdomains. Rule creation and deletion affect future mail only. These endpoints require settings scope.

For an explicit single-message HTML preview while mailbox HTML display is off, request GET /messages/{messageId}/html?preview=1. This does not update mailbox settings. Authentication, mailbox access, raw retention, sanitization, and sandbox requirements still apply.

Mailbox creation accepts forwardTo as an email address or null to disable forwarding. It takes precedence over the legacy forward boolean. Mailbox summaries and settings include blocked, derived from matching recipient blacklist rules. Blocking does not modify existing messages.

Linked webmail attachments

Webmail uploads file parts through the authenticated Worker API to private R2 and submits linkedAttachmentIds in compose or reply form data. Binary attachments fields remain available for existing API clients under their original MIME safety bounds.

  1. POST /api/v1/uploads with JSON filename, mediaType, and size creates an authenticated upload session and returns id, partSize, and complete (true for empty files).
  2. PUT /api/v1/uploads/{id}/parts/{partNumber} streams a binary application/octet-stream body into R2 and returns its receipt in the ETag response header. Retain that ETag for completion. Parts are numbered from 1. Each PUT requires authentication and ownership; cookie sessions also require the same-origin mutation headers. Retrying the same part replaces it before completion.
  3. POST /api/v1/uploads/{id}/complete with ordered parts: [{partNumber, etag}] verifies the completed object. Send may reference only completed files owned by the caller.
  4. Include each ID as a linkedAttachmentIds form value when sending. A file belongs to one outbound send; unchanged retries must reuse the same idempotency key and IDs.

The email contains HTML and plain-text links to /api/v1/downloads/{token}. These capability links require no session, stream the file with attachment disposition, support byte ranges, and never expire by age. Completed files and raw mail remain retained, including in Spam and Trash. No product file-size or count quota applies to linked uploads; Cloudflare service limits and message-body limits remain. The public OpenAPI document contains the upload contracts.

On this page