mailQA API

Everything a test suite needs lives under /v1 and is authenticated with an API key. This is the stable, supported surface: the endpoints that back the dashboard itself are internal and are not documented here.

Inboxes

A mailbox with a stable address. Creating one is idempotent, which is what makes it safe to call at the top of every test run. The ones you create are MX inboxes; the SMTP sink adds EXTERNAL ones of its own, one per address the app under test sent to.

GET /v1/inboxes — List inboxes

Every inbox in the organization, newest first — the MX ones you created and the EXTERNAL ones the SMTP sink created, one per address the app under test sent to. SMTP credentials are **not** included here: they are the organization's, at GET /v1/smtp/credentials, and on the response to POST /v1/inboxes.

POST /v1/inboxes — Create an inbox

Idempotent. A second call with the same address hands back the same inbox — same id, same address — which is what makes it safe at the top of every CI run. Both halves attach smtp, the organization's sink credentials, so one call configures the mailer under test too. The status code carries which half happened, so a plain HTTP client can branch without parsing the body:

DELETE /v1/inboxes/{id} — Delete an inbox

Deletes the inbox and everything in it, and reports how many messages went with it.

SMTP

The organization's sink credentials — one set, shared by every inbox. Mail sent through them is filed by the address it was sent to, so the credentials say whose mail it is and the recipient says where it goes.

GET /v1/smtp/credentials — Read the SMTP credentials

The organization's sink credentials: one set, shared by every inbox. Point the app under test at them and everything it sends is captured and filed by the address it was sent to:

POST /v1/smtp/credentials/rotate — Rotate the SMTP password

Issues a new password and returns the same shape. The old password stops working immediately for every mailer configured with it, across the whole team; the username is kept, so a config only needs its password line changed. From the dashboard this takes an admin or owner; an API key acts for the organization.

Messages

Captured mail. A message row is written by the parse worker *after* parsing, so a message is never visible half-parsed — until the parse job runs it simply does not exist and fetching it returns 404. That is what makes polling for a message a sound way to wait for one.

GET /v1/inboxes/{id}/messages — List messages in an inbox

Message summaries, newest first. Filters combine with AND.

DELETE /v1/inboxes/{id}/messages — Empty an inbox

Deletes every message in the inbox and returns the count, so a suite can assert on its own teardown. The inbox itself and its address survive. From the dashboard, a MEMBER can only empty MX inboxes they created themselves — see createdById on the inbox — and any EXTERNAL one; an API key can empty any inbox in its organization.

GET /v1/messages/{id} — Get a message

The full message. Field notes, in the order people trip over them:

DELETE /v1/messages/{id} — Delete a message

Deletes the stored raw message and its attachment blobs too.

GET /v1/messages/{id}/raw — Download the original .eml

Byte-identical to what the sender transmitted, as message/rfc822 with Content-Disposition: attachment. The one thing to reach for when a test needs to assert on something the parser did not surface.

Attachments

Attachment bytes never proxy through the API process: download redirects to a short-lived presigned URL, and content exists for the cid: images the sanitised HTML points at.

GET /v1/attachments/{id}/download — Download an attachment

302 to a short-lived presigned URL on object storage, so attachment bytes never proxy through the API process. The link expires in five minutes by default — it ends up in test logs and browser history, and needs to survive exactly one download.

GET /v1/attachments/{id}/content — Stream attachment content inline

The bytes inline, with the real content type and X-Content-Type-Options: nosniff, cached privately for an hour. This is what cid: images in the sanitised HTML are rewritten to, so the sandboxed preview renders them without a cross-origin redirect.

Billing

Plans and their entitlements. Public — the pricing page reads it before anyone has signed up. Amounts are resolved at request time rather than baked into a build, so what you read here is what checkout will charge — after the free trial an organization's first subscription starts with (trialDays).

GET /v1/billing/plans — List plans and prices

Public — no credential needed, because the pricing page reads it before anyone has signed up. Amounts are in minor units (cents) and are resolved at request time, so they cannot be a stale number baked into a build. prices is null when the server this reference describes has no billing configured. trialDays is the free trial an organization's first subscription starts with — once per organization, so a later subscription is charged from its first day.

Service

Unauthenticated liveness and readiness, for a load balancer or an uptime check.

GET /health — Liveness

Answers as long as the process is up. Does not touch the database.

GET /ready — Readiness

Checks the dependencies the process needs to serve traffic, and reports any migration this build expects that the database does not have. It reports only that direction: an older image against a newer schema is a supported rollback, not a fault.