Authentication
The two key formats, what each is scoped to, and how a request is authorized.
Every request — REST or MCP — carries the same header:
Authorization: Bearer <key>
There are two key formats. Both are minted from the dashboard under Settings → API keys and both work with every endpoint on this site; what differs is what each is scoped to.
Project keys
A project key starts with gm_live_ followed by 43
base64url characters, and is opaque — looked up by a sha256 hash, never
decoded (apps/web/lib/api-keys.ts:24,48-56). It is bound to exactly one
project at creation time. It:
- Can send, list mail, and use every MCP tool as that project.
- Cannot create a new project (Projects requires an org-wide key) and cannot act as any other project, even one in the same organisation.
- Silently ignores a
project_idfield in a request body — a project key's own project always wins. Sending one is harmless, not an error.
This is the key most integrations want: mint one per project, put it in one service's environment, and it can't reach anything else.
Org and scoped keys
Under-documented elsewhere
This format is real and works today, but the in-app /help page and the
MCP key-prefix constant both mention only the project-key prefix above —
this page is the fullest public description of it.
An org or scoped key is an HS256 JWT with iss: "kontacts.dev" and
typ: "api_key" in its payload, display-prefixed jwt_…
(apps/web/lib/api-keys.ts:68-69,165-181). It carries a scope claim
instead of a single project id:
| Scope | Meaning |
|---|---|
"*" | Every project in the organisation. |
{ workspaces: [orgId, …] } | Every project in the listed organisation(s). |
{ projects: [projectId, …] } | Exactly the listed projects. |
Only a scope: "*" key may create a project (Projects),
and only when it also has an org_id — a * key is always minted from an
organisation's settings, so this is automatic once you mint one there.
For everything else (send, list, MCP), an org/scoped key resolves to
whichever project(s) it's allowed to touch. Most endpoints then require you
to say which project with a project_id — see each endpoint's page for
where that goes (query string, body, or MCP tool argument).
What's the same either way
- Storage — only
sha256(token)is ever written to the database. The plaintext is shown exactly once, in the response to the mint request — copy it then, because the dashboard cannot show it again. - Revocation — a project admin can revoke a key at any time
(
revoked_atis stamped). A revoked key fails every endpoint with401 API key has been revoked. - No expiry, no rotation. The
api_keystable hascreated_at,last_used_at, andrevoked_at— there is noexpires_atcolumn (supabase/migrations/0013_api_keys.sql:21-34), and there is no rotation endpoint for an API key. If you need to rotate one, mint a new key and revoke the old one. Webhook signing secrets are a separate credential and do have a rotate action — see Webhooks.
Ownership matters
A key's created_by user can be deleted independently of the key (the
column is set null, not cascaded). A key with no owner left is refused by
the send path — 403 API key has no owner; create a new one — because
nobody remains to charge the send's quota to. This applies to REST send and
to the MCP send_email / reply_email tools.
Minting a key
Both formats are minted from the same page: Settings → API keys
(/dashboard/settings/api-keys). A project key is minted from a
project's own settings; an org-wide or scoped key is minted from the
organisation's settings and requires picking a scope.
MCP uses the exact same keys and the exact same settings page
(/dashboard/settings/api-keys) — there is no separate MCP credential.