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.
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.
| Scope | Grants |
|---|---|
contacts:read | Read contacts (GET /contacts, GET /contacts/:id) |
contacts:write | Upsert contacts, unsubscribe, and create/update/delete segments |
send | POST /send — one transactional or marketing email |
sends:read | Read the send log (GET /sends) |
templates:write | Enable templates and write/publish template content |
sequences:write | Create and edit sequences, enroll contacts, track events, cancel enrollments |
campaigns:write | Create and cancel campaigns |
suppressions:write | Add and remove suppressions |
webhooks:write | Create, 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.
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
| Status | code | Meaning |
|---|---|---|
| 401 | unauthorized | No Authorization header, or the key is unknown/revoked |
| 403 | forbidden | Valid key, missing the scope this endpoint requires |
| 403 | project_suspended | The project is not active |
| 403 | email_unverified | The 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.