Managing API keys programmatically
Mint, scope, list, rotate, and revoke API keys over the REST API -- for apps that provision keys for their own agents, workers, or customers.
If you're building on Molted, you'll often need to create and manage API keys without the portal -- for example, to hand each of your agents, workers, or end-customers a key scoped to just the mailboxes they should touch. Everything the portal does is available over the REST API.
This page focuses on key management. For the auth model and the portal/ session flow, see Authentication. For provisioning whole accounts programmatically, see Programmatic signup.
Prerequisites
You need one existing API key (mm_live_…) to authenticate the calls below.
Create it at signup or in Dashboard → API Keys. That key's tenant is the
tenant every key you mint will belong to -- you can never mint a key for another
tenant.
The CLI and TypeScript SDK resolve your tenant ID and routing automatically. The
raw REST examples here require you to pass tenantId explicitly (see below) -- that's the only difference.
Base URL, auth, and your tenant ID
All calls go to https://api.molted.email with a Bearer token:
Authorization: Bearer mm_live_your_keyKey endpoints are tenant-scoped, so they require your tenantId -- as a query
param for GET/DELETE, or in the JSON body for POST/PATCH. If you don't
know it, ask whoami (the one endpoint that derives the tenant from the key):
curl -X POST https://api.molted.email/v1/agent/whoami \
-H "Authorization: Bearer mm_live_your_key" \
-H "Content-Type: application/json" -d '{}'
# → { "tenant_id": "tenant-…", ... }Mint a scoped key
The minting endpoint is POST /v1/agent/keys. Scope a key three ways:
curl -X POST https://api.molted.email/v1/agent/keys \
-H "Authorization: Bearer mm_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"tenantId": "tenant-…",
"label": "support-bot",
"scopeAllMailboxes": false,
"mailboxScopes": [
{ "mailboxId": "MAILBOX_UUID", "permissions": ["read", "send"] }
]
}'curl -X POST https://api.molted.email/v1/agent/keys \
-H "Authorization: Bearer mm_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"tenantId": "tenant-…",
"label": "ops",
"scopeAllMailboxes": false,
"mailboxScopes": [
{ "mailboxId": "MBX_A", "permissions": ["read", "send"] },
{ "mailboxId": "MBX_B", "permissions": ["read"] }
]
}'curl -X POST https://api.molted.email/v1/agent/keys \
-H "Authorization: Bearer mm_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"tenantId": "tenant-…",
"label": "admin-worker",
"scopeAllMailboxes": true
}'Always send scopeAllMailboxes: false alongside mailboxScopes. The API infers
it when omitted, but being explicit is the safe contract -- and it documents
intent for anyone reading the call later.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (from whoami). |
label | string | No | Human-readable name, max 64 chars. Helps you identify keys later. |
scopeAllMailboxes | boolean | No | true mints a full-access (admin) key. Omit or false for a scoped key. |
mailboxScopes | array | No | Per-mailbox grants: { mailboxId, permissions }. Up to 50 entries. |
mailboxId | string | No | Shorthand for a single mailbox with default ["read","send"] (instead of mailboxScopes). |
Response -- the raw key is shown once
{
"id": "b2707074-…",
"keyPrefix": "mm_live_b3a4aa6c31f6",
"label": "support-bot",
"status": "active",
"scopeAllMailboxes": false,
"createdAt": "2026-06-09T…",
"rawKey": "mm_live_…"
}Store rawKey immediately -- it is never retrievable again. Only the prefix
is kept server-side.
Permission model
| Permission | Grants | Note |
|---|---|---|
read | View threads, messages, contacts, and metadata on the mailbox. | |
send | Send, reply, and schedule follow-ups from the mailbox. | Does not grant read. |
manage | Read, send, and modify rules, folders, and mailbox settings. | Implies read + send. |
A send-only key is ideal for outbound-only workers (notifications, campaigns)
that should never read inbox contents.
Pattern: a key per worker (least privilege)
The common shape for apps built on Molted: provision one narrow key per agent or customer-facing worker, scoped to only what it needs. In TypeScript:
const API = "https://api.molted.email";
const ADMIN_KEY = process.env.MOLTED_API_KEY!; // your bootstrap key
async function whoami(): Promise<string> {
const res = await fetch(`${API}/v1/agent/whoami`, {
method: "POST",
headers: { Authorization: `Bearer ${ADMIN_KEY}`, "Content-Type": "application/json" },
body: "{}",
});
return (await res.json()).tenant_id;
}
async function mintScopedKey(tenantId: string, mailboxId: string) {
const res = await fetch(`${API}/v1/agent/keys`, {
method: "POST",
headers: { Authorization: `Bearer ${ADMIN_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
tenantId,
label: `worker:${mailboxId}`,
scopeAllMailboxes: false,
mailboxScopes: [{ mailboxId, permissions: ["read", "send"] }],
}),
});
if (!res.ok) throw new Error(`mint failed: ${res.status} ${await res.text()}`);
const key = await res.json();
return key.rawKey; // hand this to the worker; store it securely
}Each worker then authenticates with its own scoped key. If it ever tries to act
on a mailbox outside its scope, the request is rejected with
403 mailbox_scope_denied -- your blast radius stays contained.
List, re-scope, and revoke
curl "https://api.molted.email/v1/agent/keys?tenantId=tenant-…" \
-H "Authorization: Bearer mm_live_your_key"
# → [{ id, keyPrefix, label, status, scopeAllMailboxes, mailboxScopes, lastUsedAt, createdAt }]curl -X PATCH https://api.molted.email/v1/agent/keys/KEY_ID \
-H "Authorization: Bearer mm_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"tenantId": "tenant-…",
"scopeAllMailboxes": false,
"mailboxScopes": [{ "mailboxId": "MAILBOX_UUID", "permissions": ["manage"] }]
}'curl -X DELETE "https://api.molted.email/v1/agent/keys/KEY_ID?tenantId=tenant-…" \
-H "Authorization: Bearer mm_live_your_key"
# → { "revoked": true }listApiKeys returns each key's mailboxScopes (with the mailbox address) so
you can audit exactly what every key can reach. Mailbox IDs come from
GET /v1/agent/mailboxes.
Rotation: raw keys can't be re-read, so to rotate you mint a fresh key, swap it into the consumer, then revoke the old one once traffic has moved over.
Errors
| Status | Reason | Meaning |
|---|---|---|
401 | missing_api_key / invalid_api_key | Missing Bearer token, or the key doesn't match the tenantId you passed. |
403 | mailbox_not_owned | You tried to scope a key to a mailbox your tenant doesn't own (create). |
400 | Mailbox IDs do not belong to this tenant | Same ownership failure on the update path. |
403 | mailbox_scope_denied | A scoped key was used against a mailbox outside its scope. |
See Errors for the full catalogue.
Security
- Treat keys like passwords. Store them in a secrets manager or env vars; never commit them.
- Prefer scoped keys over
scopeAllMailboxes: true. Grant the minimum each consumer needs. - A
tenantIdyou pass is always validated against the key -- you cannot mint or manage keys for another tenant. - Revoke any key you suspect is compromised; revocation takes effect immediately.
Managing everything else programmatically
Key management is one piece. Most of the platform is available over the same Bearer-authenticated API:
- Send & reply -- Sending email, Outbound, Batch operations
- Mailboxes & routing -- Mailboxes, Rules, Threads
- Domains & deliverability -- Domains, Sender addresses, Analytics
- Contacts & compliance -- Contacts, Suppressions, Lists, Segmentation
- Automation -- Journeys, Experimentation
- Real-time -- Events (SSE), Webhooks, Alerts
- Higher-level clients -- TypeScript SDK, MCP server, and framework integrations