Skip to main content

Authentication

Every /api/v1 request authenticates with a project API key as a bearer token:

curl https://mailroom.example.com/api/v1/health \
-H "Authorization: Bearer pk_live_…"

The SDK does this for you:

const mailroom = createClient({
apiKey: process.env.MAILROOM_API_KEY!,
baseUrl: process.env.MAILROOM_URL!,
});

A key resolves to exactly one project. There is no account id, org id, or tenant header to pass — the key is the tenancy.

The key is a secret

pk_live_… grants full access to its project's contacts and sending. Keep it in server environment variables. Mailroom issues no publishable/browser key, sets no CORS headers, and expects to be called from your backend only.

Scopes

Keys carry scopes. Write endpoints require a specific scope; read endpoints require only a valid, non-revoked key. * grants everything.

ScopeGrants
contacts:readRead contacts (GET /contacts, GET /contacts/:id)
contacts:writeUpsert contacts, unsubscribe, and create/update/delete segments
sendPOST /send — one transactional or marketing email
sends:readRead the send log (GET /sends)
templates:writeEnable templates and write/publish template content
sequences:writeCreate and edit sequences, enroll contacts, track events, cancel enrollments
campaigns:writeCreate and cancel campaigns
suppressions:writeAdd and remove suppressions
webhooks:writeCreate, update, delete, rotate, and ping webhook endpoints
*Everything above

Grant the narrowest set that works. A storefront that only sends receipts needs send; a CRM sync needs contacts:write and nothing else.

Reads are key-gated, not scope-gated

Listing templates, campaigns, segments, suppressions, and webhook endpoints requires a valid key but no particular scope. Contacts and the send log are the exceptions — they need contacts:read / sends:read. Sequence reads are grouped under sequences:write.

Failure modes

StatuscodeMeaning
401unauthorizedNo Authorization header, or the key is unknown/revoked
403forbiddenValid key, missing the scope this endpoint requires
403project_suspendedThe project is not active
403email_unverifiedThe organization owner's email is unverified, so sending is blocked

In the SDK all four raise MailroomAuthError — read err.code to tell them apart:

try {
await mailroom.contacts.upsert({ email: "ada@example.com" });
} catch (err) {
if (err instanceof MailroomAuthError && err.code === "forbidden") {
// The key exists but lacks contacts:write.
}
}

Rotating a key

Create the new key, deploy it, then revoke the old one — revocation takes effect immediately on the next request. Keys never expire on their own; the console shows last_used_at so you can spot one nothing is calling anymore.

Unauthenticated endpoints

Two paths take no API key:

  • GET /api/v1/health — liveness, safe to poll from uptime monitors.
  • GET|POST /api/v1/unsubscribe?c=<contactId>&t=<token> — the one-click unsubscribe link in every marketing email. It authenticates with an HMAC token signed by the project's own secret, so a token minted for one project never verifies under another. See REST: unsubscribe.