MOLTED EMAIL

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_KEY

Managing API keys

  1. Go to Settings > Access in the portal (/app/settings#access).
  2. Click Create key, give it a label, and choose all mailboxes or specific ones with their own permissions.
  3. Copy the key immediately -- it is displayed only once and cannot be retrieved later.
  4. 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
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:

PermissionGrants
readRead threads, messages, contacts, and metadata on the mailbox.
sendSend new emails, reply in threads, and schedule follow-ups from the mailbox. Does not grant read.
manageFull 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
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"] }
    ]
  }'
CLI
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 manage

A 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
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
curl https://api.molted.email/v1/me/tenant \
  -b cookies.txt
Response
{
  "id": "tenant-slug-a1b2c3d4",
  "name": "My Agent",
  "status": "trial"
}

Step 3: Create an API key

curl
curl -X POST https://api.molted.email/v1/me/keys \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "label": "default",
    "scopeAllMailboxes": true
  }'
Response
{
  "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.

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

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).

Response (no person owns the workspace yet)
{
  "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. ..."
}
Response (a person owns the workspace)
{
  "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-token

Requires 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" }.

FieldTypeRequiredDescription
tenantIdstringYesYour tenant ID.
nextstringNoWhere 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
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

FieldTypeDescription
kind"claim" or "portal"What the link is: a claim link, or the plain portal URL.
urlstringThe link to give your human. It carries no sign-in token.
expiresAtstring or nullWhen a claim link stops working (7 days). null for a portal link, which doesn't expire.

Claiming a workspace

  1. Your human opens the claim link. The page shows the workspace's name, which agent key asked, and what claiming means.
  2. 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.
  3. 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.email domain, or a custom domain that receives mail through Molted) can't be used to claim.
  4. 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.
  • 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.