Authentication
Session auth for the portal UI and Bearer API keys for the sending API.
Molted Email uses two authentication methods depending on the context.
Session cookies (Portal UI)
When you sign in at molted.email, a session cookie is set. This cookie authenticates all portal requests: Home, mailboxes, and Settings (domains, plan and API keys).
No action is required on your part. The session is managed automatically by the portal.
Email verification
When you sign up in the portal, we email you a link to verify your address. Opening it confirms your email and signs you in. Verification is never needed to log in, and accounts created by an agent (the CLI, MCP or the API) don't get a verification email.
Links work for 24 hours. To get a new one, use Send verification email in Settings. You can ask for one a minute, up to 5 a day.
Bearer API keys (Sending API)
All API requests to api.molted.email require a Bearer token in the Authorization header:
Authorization: Bearer YOUR_API_KEYManaging API keys
- Go to Settings > Access in the portal (
/app/settings#access). - Click Create key, give it a label, and choose all mailboxes or specific ones with their own permissions.
- Copy the key immediately -- it is displayed only once and cannot be retrieved later.
- To revoke a key, click Revoke next to the key in the list.
Mailbox-scoped keys
API keys can be scoped to specific mailboxes with granular permissions:
curl -X POST https://api.molted.email/v1/me/keys \
-H "Cookie: YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"label": "support-inbox-only",
"scopeAllMailboxes": false,
"mailboxScopes": [
{ "mailboxId": "MAILBOX_UUID", "permissions": ["read", "send"] }
]
}'Set scopeAllMailboxes: true (the default) for a key with full access to all mailboxes.
Per-mailbox permissions:
| Permission | Grants |
|---|---|
read | Read threads, messages, contacts, and metadata on the mailbox. |
send | Send new emails, reply in threads, and schedule follow-ups from the mailbox. Does not grant read. |
manage | Full control: read, send, and modify rules, folders, and mailbox settings. Implies read + send. |
A key with send only can send from the scoped mailbox but cannot list or fetch its threads -- useful for outbound-only agents (notifications, campaigns, alerts). Requests to other mailboxes return 403 mailbox_scope_denied.
Routes that act on the whole workspace (domains, providers, billing, alert destinations, inbound hooks, account and safety settings, writes to contacts, lists and segments, removing suppressions, granting consent, agent coordination, the ingest routes, and writes to journeys, templates and experiments) also return 403 mailbox_scope_denied to a mailbox-scoped key, whatever its permissions. Tightening changes (adding a suppression, withdrawing consent, unsubscribing) stay open to a scoped key with send or manage somewhere. See Workspace-wide routes.
The scope is the same whichever way the key reaches us: directly against the API (including the mailbox, thread, outbound, rule, override and alert routes under /v1/agent/*, which the API serves from the mailbox service -- the bulk thread actions archive, unarchive, trash, restore and permanent delete resolve each thread's mailbox and check it too), or through the CLI and MCP server (which go through the mailbox service). A send that names no mailboxId goes out from the key's own mailbox, and the approval queue (GET /v1/send/approvals) lists only the key's mailboxes.
Minting scoped keys as an agent
An existing agent can mint further scoped keys with its own Bearer token (no
session cookie needed) -- useful for handing a sub-agent a narrow key. The agent
endpoint POST /v1/agent/keys takes the same shape, but requires your
tenantId in the body (the CLI and SDK fill this in for you):
curl -X POST https://api.molted.email/v1/agent/keys \
-H "Authorization: Bearer mm_live_..." \
-H "Content-Type: application/json" \
-d '{
"tenantId": "your-tenant-id",
"label": "support-inbox-only",
"scopeAllMailboxes": false,
"mailboxScopes": [
{ "mailboxId": "MAILBOX_UUID", "permissions": ["read", "send"] }
]
}'molted auth keys create --label support-inbox-only \
--mailbox MAILBOX_UUID --permissions read,send
molted auth keys create --label ops --scope-all
molted auth keys update <id> --mailbox MAILBOX_UUID --permissions manageA token can only scope a key to mailboxes its own tenant owns; scoping to a
mailbox owned by another tenant is rejected (403 mailbox_not_owned on create,
400 on update). The raw key is returned once in the rawKey field -- store it
immediately.
For the full programmatic key-management guide (listing, re-scoping, rotating, revoking, and the least-privilege pattern for apps), see Managing API keys programmatically.
Programmatic signup
New signups are waitlist-only for now. POST /api/auth/sign-up/email returns 403 { "code": "SIGNUPS_CLOSED", "waitlistUrl": "..." } until they reopen. Join at molted.email/signup. Everything below still applies to existing accounts.
Agents can create accounts and provision API keys without the portal UI.
Step 1: Create an account
curl -X POST https://api.molted.email/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"name": "My Agent",
"email": "agent@example.com",
"password": "secure-password-here"
}'This creates a user account and automatically provisions a tenant, default mailbox, and default transactional template. The session cookie is saved to cookies.txt.
Step 2: Get your tenant ID
curl https://api.molted.email/v1/me/tenant \
-b cookies.txt{
"id": "tenant-slug-a1b2c3d4",
"name": "My Agent",
"status": "trial"
}Step 3: Create an API key
curl -X POST https://api.molted.email/v1/me/keys \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"label": "default",
"scopeAllMailboxes": true
}'{
"id": "key-uuid",
"keyPrefix": "mm_live_...",
"label": "default",
"rawKey": "mm_live_abc123..."
}Save rawKey immediately -- it is only returned once. Use it as your Authorization: Bearer token for all subsequent API calls.
Links for your human: claim or sign in
When your agent needs a person (to pick a plan, approve a send, manage domains), it asks Molted for a link to hand them. The link never signs anyone in: the person always uses their own account.
- No person owns the workspace yet (your agent created it, for example with
molted auth signup): the link is a claim link. Your human opens it, signs in or creates an account with their own email, and claims the workspace. They become its owner; the agent keeps working with its keys. - A person owns it: the link is the plain portal URL. Your human signs in with their own login.
CLI
molted auth login-link
molted auth login-link --next "/app/settings#plan"The command uses your active profile credentials. --next is where your human lands afterwards (a path in the app).
{
"kind": "claim",
"url": "https://molted.email/claim/AbC123...",
"expiresAt": "2026-10-16T14:15:00.000Z",
"note": "A claim link: give the url to your human. ..."
}{
"kind": "portal",
"url": "https://molted.email/app",
"expiresAt": null,
"note": "The portal: a person already owns this workspace. ..."
}API
POST https://api.molted.email/v1/agent/login-tokenRequires Bearer authentication with
an admin, all-mailbox key: a claim makes a person the owner of the whole
workspace, so the calling key must already have that reach. A key scoped to one
or more specific mailboxes gets 403 { "message": "mailbox_scope_denied" }; a
key without admin scope gets 403 { "message": "insufficient_scope" }.
| Field | Type | Required | Description |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID. |
next | string | No | Where your human lands afterwards: a path in the app (/app or /app/...) or a URL on the portal. Anything else is 400 invalid_next. Default /app. |
curl -X POST https://api.molted.email/v1/agent/login-token \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tenantId": "your-tenant-id", "next": "/app/settings#plan"}'Response fields
| Field | Type | Description |
|---|---|---|
kind | "claim" or "portal" | What the link is: a claim link, or the plain portal URL. |
url | string | The link to give your human. It carries no sign-in token. |
expiresAt | string or null | When a claim link stops working (7 days). null for a portal link, which doesn't expire. |
Claiming a workspace
- Your human opens the claim link. The page shows the workspace's name, which agent key asked, and what claiming means.
- They sign in, or create an account with their own email. Creating an account through a claim link works even while sign-ups are on the waitlist, and it creates no extra workspace. It asks for no password: we email a link to set one, so the password is always chosen by whoever reads that inbox. Setting it signs them in.
- They click Claim. This needs a verified email address and a password sign-in from the last 10 minutes; an older session is asked to sign in again.
The account must use an email address of their own: an address that receives mail through Molted (one of the agent's Molted mailboxes, the shared
agent.molted.emaildomain, or a custom domain that receives mail through Molted) can't be used to claim. - They land where the agent asked (
next), with the claimed workspace open, and the workspace switcher shows it next to any workspace of their own.
After the claim, login-token answers kind: "portal".
Behavior and limits
- A claim link works for 7 days and once, and it can create one account. A new link doesn't cancel older ones, but once a person owns the workspace every outstanding link stops working.
- Revoking the agent key that asked for a link also kills the link.
- A workspace gets at most 5 claim links an hour; after that the call answers
429 { "error": "rate_limited", "retryAfterSeconds": ... }. Reuse a link you already have. - An expired, used or unknown link shows its own page, and nothing happens.
Security notes for claim links
- A claim link grants nothing on its own. Only a person (not an agent's account) can claim, with a verified email address Molted doesn't receive mail for, and only right after typing their password, so an agent that opens its own link gets neither a session nor ownership.
- What this can't stop: an agent that controls an email account outside Molted could sign up "as a person" with it. Passkeys or a registered device are the real fix, and are planned.
- Still share it only with the person who should own the workspace: whoever claims it first becomes an owner.
- Older login links (
/auth/token-login?token=..., from before claim links) no longer sign anyone in: they open the login page, and the sessions they created were signed out when claim links shipped. - A password reset signs the account out of every other browser and the phone app. Apps connected through OAuth keep their access until you revoke them in Settings.
Security notes
- API keys grant full send access to your tenant. Treat them like passwords.
- Store keys in environment variables or a secrets manager. Never commit them to source control.
- Rotate keys periodically. Revoke any key you suspect has been compromised.
- Each key is scoped to a single tenant.
- A suspended account cannot sign in, and any existing session is rejected with
403 { "error": "USER_SUSPENDED" }on its next request. API keys are tenant-scoped, so suspending a user does not revoke that user's tenant's API keys.
CLI Quickstart
Install the Molted CLI, authenticate your agent, and send your first policy-checked email from the terminal in under two minutes.
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.