Skip to content
MailMarcodocs

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:

CredentialUsed byTypical routes
API key
mf_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
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:

format
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 token field of the create response, and never again.
  • GET /v1/api-keys returns 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.

curl
# 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

StatusMeaningWhat to do
401No 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.
403The 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

POST/v1/auth/signup
auth: Public201 on success

Create a user and their first organization. Rate limited to 5 per hour per IP.

Body

email
string (email)required
Login address.
password
stringrequired
At least 8 characters.
organizationName
stringoptional
1–120 characters. Derived from the email local-part when omitted.

Errors

  • 400Validation failed.
  • 409That email or organization slug already exists.
  • 429Sign-up rate limit exceeded.
POST/v1/auth/login
auth: Public200 on success

Exchange credentials for an access/refresh token pair. Rate limited to 10 per minute per IP.

Body

email
string (email)required
Login address.
password
stringrequired
At least 8 characters.
totp
stringoptional
Exactly 6 digits, when 2FA is enrolled.
orgId
string (uuid)optional
Scope the token to a specific organization.
response · 200
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"
}
POST/v1/auth/refresh
auth: Public200 on success

Exchange a refresh token for a fresh pair.

Body

refreshToken
stringrequired
The refresh token.
GET/v1/api-keys
auth: Session200 on success

List the organization's keys, newest first. Metadata only — never the hash or plaintext.

response · 200
[
  {
    "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"
  }
]
POST/v1/api-keys
auth: Session201 on success

Mint a key. The plaintext token is in the response and is never retrievable again.

Body

name
stringrequired
1–100 characters. Shown in listings.
scopes
string[]optionaldefault ["emails:send"]
Recorded on the key; not enforced by any route today.
request
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"]}'
DELETE/v1/api-keys/{id}
auth: Session200 on success

Revoke a key. Effective immediately for every subsequent request.

Path parameters

id
string (uuid)required
Key id.
response · 200
{ "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.