02 · MailMarco API
Authentication
Two credential types, one header. Which one a route wants depends on whether it is runtime integration or account management.
Two authenticated surfaces
Every authenticated route reads the same header — Authorization: Bearer … — but the token differs:
| Credential | Used by | Typical routes |
|---|---|---|
API keymf_live_… | Your application at runtime. Long-lived, revocable, scoped to one organization. | /v1/emails, /v1/webhooks, /v1/suppressions, /v1/bounces, /v1/complaints, /v1/templates (writes), /v1/contacts, /v1/topics, /v1/segments, /v1/broadcasts, /v1/received-emails, /v1/billing/subscription |
| Session access token JWT | A signed-in dashboard user. Short-lived, refreshable. | /v1/domains, /v1/api-keys, /v1/smtp-credentials, /v1/organization, /v1/inbound, /v1/reputation, /v1/emails (list), /v1/logs, /v1/analytics, /v1/templates (reads) |
API keys
Send the key as a bearer token. Nothing else is accepted — there is no query parameter, no basic auth, and no cookie fallback.
curl https://api.mailmarco.com/v1/suppressions \
-H "authorization: Bearer mf_live_9f1c3d5b7e9a1c3d.8kQ2h9Rt7vXc1mB4nZ0pL6sW"The key resolves to exactly one organization, and that organization is the only thing the request can see or write. An organization id in a body or query string is never read for authorization; it is ignored.
Key format and one-time plaintext
The token is three parts:
mf_live_9f1c3d5b7e9a1c3d.8kQ2h9Rt7vXc1mB4nZ0pL6sW
└──┬───┘└──────┬───────┘ └──────────┬──────────┘
│ │ │
│ │ └─ secret (24 random bytes, base64url)
│ └─ prefix (8 random bytes as 16 hex chars) — shown in listings
└─ fixed literal "mf_live_"- Only a SHA-256 hash of the whole token and the prefix are stored. The plaintext is returned once, in the
tokenfield of the create response, and never again. GET /v1/api-keysreturns metadata only — id, name, prefix, scopes,lastUsedAt,revokedAt,createdAt. The hash is never selected.- Authentication matches on prefix and hash, and skips any key with a
revokedAt. Revocation is immediate. - Each successful authentication stamps
lastUsedAt, which is how you find keys that are safe to retire.
Scopes
A key carries a list of scope strings, defaulting to ["emails:send"] when you omit the field. Scopes are stored on the key, returned in listings, and attached to the authenticated request.
Dashboard session tokens
POST /v1/auth/login returns an access token and a refresh token. The access token is a JWT whose payload is { sub: userId, org: orgId, typ: "access" } — there is no role claim and no embedded user record.
# 1. log in
curl -X POST https://api.mailmarco.com/v1/auth/login \
-H "content-type: application/json" \
-d '{"email":"[email protected]","password":"a-long-password"}'
# 2. refresh when the access token expires
curl -X POST https://api.mailmarco.com/v1/auth/refresh \
-H "content-type: application/json" \
-d '{"refreshToken":"…"}'If the account has TOTP enrolled, pass the 6-digit totp field in the login body. Users who belong to more than one organization can select which one the token is scoped to with an orgId field.
Two-factor
Enrolment and verification are both session-authenticated: POST /v1/auth/2fa/enroll returns the enrolment material, and POST /v1/auth/2fa/verify with { "code": "123456" } activates it, answering { "enabled": true }.
Platform admin
Routes under /v1/admin/* are the cross-tenant operator surface. They take the same session JWT but additionally require a platform-admin claim. These are not part of the customer-facing API and are documented here only so the status codes are unambiguous: a valid non-admin token gets 403, an absent or invalid token gets 401.
401 vs 403
| Status | Meaning | What to do |
|---|---|---|
401 | No Authorization header, the header does not start with `Bearer `, the API key does not resolve (unknown, malformed or revoked), or the JWT failed verification. | Fix or re-mint the credential. Retrying with the same token will not help. |
403 | The credential is valid but not permitted: a non-admin token on an admin route, a paused organization on a send (`error: org_suspended`), or an inbound attachment whose malware scan is not `clean`. | This is a state problem, not a credential problem — see the specific route's page. |
Endpoints
/v1/auth/signupCreate a user and their first organization. Rate limited to 5 per hour per IP.
Body
emailstring (email)required | Login address. |
passwordstringrequired | At least 8 characters. |
organizationNamestringoptional | 1–120 characters. Derived from the email local-part when omitted. |
Errors
400— Validation failed.409— That email or organization slug already exists.429— Sign-up rate limit exceeded.
/v1/auth/loginExchange credentials for an access/refresh token pair. Rate limited to 10 per minute per IP.
Body
emailstring (email)required | Login address. |
passwordstringrequired | At least 8 characters. |
totpstringoptional | Exactly 6 digits, when 2FA is enrolled. |
orgIdstring (uuid)optional | Scope the token to a specific organization. |
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"
}/v1/auth/refreshExchange a refresh token for a fresh pair.
Body
refreshTokenstringrequired | The refresh token. |
/v1/api-keysList the organization's keys, newest first. Metadata only — never the hash or plaintext.
[
{
"id": "0f0a1a1e-6f6f-4f3a-9c2e-6b6a5a1d7c11",
"name": "production",
"prefix": "9f1c3d5b7e9a1c3d",
"scopes": ["emails:send"],
"lastUsedAt": "2026-07-29T09:31:44.000Z",
"revokedAt": null,
"createdAt": "2026-07-29T09:14:02.113Z"
}
]/v1/api-keysMint a key. The plaintext token is in the response and is never retrievable again.
Body
namestringrequired | 1–100 characters. Shown in listings. |
scopesstring[]optionaldefault ["emails:send"] | Recorded on the key; not enforced by any route today. |
curl -X POST https://api.mailmarco.com/v1/api-keys \
-H "authorization: Bearer $MAILMARCO_ACCESS_TOKEN" \
-H "content-type: application/json" \
-d '{"name":"production","scopes":["emails:send"]}'/v1/api-keys/{id}Revoke a key. Effective immediately for every subsequent request.
Path parameters
idstring (uuid)required | Key id. |
{ "revoked": true }SMTP credentials follow the same one-time-secret pattern under /v1/smtp-credentials (session-authenticated): the generated password is shown once at creation, and the relay host/port come back with every listing. The name field the dashboard sends is accepted and ignored — usernames are generated server-side.
Once you have a key, continue to Emails.