Authentication
Every request (except GET /health) is authenticated with an HMAC-SHA256 signature
over a canonical string. Nothing secret travels on the wire — only the signature.
Headers
| Header | Value |
|---|---|
Authorization |
TMM-HMAC-SHA256 Credential=<keyId>, Signature=<hex> |
X-TMM-Timestamp |
Unix time in milliseconds when the request was signed |
X-TMM-Nonce |
A unique value per request (UUID), for replay protection |
Content-Type |
application/json (for requests with a body) |
The timestamp must be within ±5 minutes of server time, or the request is rejected
(unauthorized). Each nonce is single-use within that window.
The string to sign
Build the canonical string by joining these five lines with \n:
METHOD # uppercase, e.g. POST
PATH # exact request target, INCLUDING the query string — e.g. /console/v1/projects/x/messages?limit=3
TIMESTAMP # same value as X-TMM-Timestamp
NONCE # same value as X-TMM-Nonce
sha256hex(body) # hex SHA-256 of the raw JSON body ("" if no body)
Then Signature = hex(HMAC_SHA256(secret, stringToSign)).
Reference (Node.js)
import { createHmac, createHash, randomUUID } from "node:crypto";
function sign(secret: string, method: string, path: string, body = "") {
const timestamp = Date.now();
const nonce = randomUUID();
const bodyHash = createHash("sha256").update(body).digest("hex");
const stringToSign = [method.toUpperCase(), path, String(timestamp), nonce, bodyHash].join("\n");
const signature = createHmac("sha256", secret).update(stringToSign).digest("hex");
return { timestamp, nonce, signature };
}
You normally never write this yourself — the SDK does it for you.
Credential types
There are four kinds of credential. Which one you use depends on the endpoint.
Project key — tmm_<hex>
The everyday credential. Belongs to one project, carries scopes, and is minted
via the API (and rotatable). Used for sending (/v1/services/sendEmail), message reads
(/v1/messages/*), and the project's own console endpoints. The keyId (tmm_…) is
public; the secret is shown once at mint time.
Scopes:
| Scope | Grants |
|---|---|
messages:send |
POST /v1/services/sendEmail |
messages:read |
GET /v1/messages/* |
suppressions:read |
read the project's suppression list |
suppressions:write |
add/remove suppressions |
project:admin |
manage the project itself (keys, sender domain, webhooks) |
Account token — tmma_<hex>
A user-scoped credential that drives organization + project lifecycle
(/console/v1/organizations, /console/v1/projects) and operates on the projects of the
orgs you belong to. Issued at /auth/v1/login and /auth/v1/signup (returned alongside
the user), and self-manageable via /console/v1/account-tokens (mint/list/revoke your own);
rotatable. This is the credential a logged-in app (e.g. the Console) acts with — never the
god-key. Access is organization membership: you can reach a project iff you're a member
of its organization (non-members get 404). Call POST /auth/v1/logout to revoke the token
you're signing with (the Console does this on logout).
Organization token — tmmo_<hex>
An organization-scoped admin credential: it authenticates AS the organization and
authorizes managing the org + its projects/domains/billing. Minted by default when an org
is created (POST /console/v1/organizations returns it, secret once) and rotatable via
/console/v1/organizations/{id}/tokens. It does not send mail — that stays a project key
(tmm_, messages:send). Use it for org-scoped automation that shouldn't depend on a
particular user's account token.
God-key (platform / bootstrap) — server-side only
The platform operator credential, accepted on /admin/v1/*. It is not a customer
credential and must never ship in client code. It's sent as:
Authorization: TMM-HMAC-SHA256 Credential=admin, Signature=bootstrap
X-TMM-Admin-Key: <the platform admin secret>
Reserved for break-glass / bootstrap (creating the first staff user, minting account
tokens). The staff Admin console authenticates with a staff account token (tmma_) via
the SDK — /admin/v1/* accepts either — so the god-key ships in no deployed app.
Getting a key
- First key for a new project:
POST /console/v1/projects(with an account token) returns the project and its firstproject:adminkey (secret shown once). - More keys:
POST /console/v1/projects/{id}/api-keys. - Lost a secret? You can't retrieve it — mint a new key and revoke the old one.