Kontacts docs

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_id field 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:

ScopeMeaning
"*"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_at is stamped). A revoked key fails every endpoint with 401 API key has been revoked.
  • No expiry, no rotation. The api_keys table has created_at, last_used_at, and revoked_at — there is no expires_at column (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.

On this page