dashboard auth.md
title: "Dashboard Auth" description: "Cookie-session endpoints behind the MachineMail dashboard: sign-in codes, magic links, passkeys, invitations, organizations and sessions. Not for integrations." canonical: "https://machinemail.dev/docs/reference/api/dashboard-auth"
Dashboard Auth
Cookie-session endpoints behind the MachineMail dashboard: sign-in codes, magic links, passkeys, invitations, organizations and sessions. Not for integrations.
Source: https://machinemail.dev/docs/reference/api/dashboard-auth
Note: These endpoints serve the MachineMail dashboard. They authenticate with the
mm_sessioncookie, not with an API key. They are listed for completeness; integrations should use API keys.
| Method | Path | Summary |
|---|---|---|
GET |
/auth/authkit |
Legacy Sign-In Redirect (Deprecated) |
GET |
/auth/callback |
Sign-in Callback |
GET |
/auth/claim |
Preview Claim |
POST |
/auth/claim |
Complete Claim |
GET |
/auth/config |
Sign-in Options |
PUT |
/auth/email-codes |
Turn Email Sign-in Codes On or Off |
POST |
/auth/email/start |
Send Sign-in Code |
POST |
/auth/email/verify |
Verify Sign-in Code |
GET |
/auth/first-key |
Get First API Key |
GET |
/auth/invitations/{token} |
Preview Invitation |
POST |
/auth/login |
Log In (Deprecated) |
POST |
/auth/logout |
Log Out |
GET |
/auth/magic |
Redeem Magic Link |
POST |
/auth/magic-link |
Request Magic Link |
GET |
/auth/me |
Current User |
GET |
/auth/oauth/authorize |
Preview App Authorization |
POST |
/auth/oauth/authorize/approve |
Approve App Authorization |
POST |
/auth/oauth/authorize/deny |
Deny App Authorization |
GET |
/auth/organizations |
List My Organizations |
POST |
/auth/organizations |
Create Organization |
POST |
/auth/organizations/switch |
Switch Organization |
POST |
/auth/passkey/options |
Start Passkey Sign-in |
POST |
/auth/passkey/verify |
Sign In with a Passkey |
GET |
/auth/passkeys |
List My Passkeys |
POST |
/auth/passkeys |
Add a Passkey |
POST |
/auth/passkeys/prompt |
Answer the Passkey Offer |
POST |
/auth/passkeys/registration |
Start Adding a Passkey |
PATCH |
/auth/passkeys/{passkey_id} |
Rename a Passkey |
DELETE |
/auth/passkeys/{passkey_id} |
Remove a Passkey |
GET |
/auth/pending |
Get Sign-in in Progress |
POST |
/auth/pending/organization |
Choose Organization |
POST |
/auth/pending/verify |
Verify Email |
POST |
/auth/sessions/revoke-others |
Sign Out Other Sessions |
POST |
/auth/signup |
Sign Up (Deprecated) |
GET |
/auth/social/{provider} |
Sign In with a Social Provider |
GET |
/auth/sso |
Sign In to an Organization with SSO |
POST |
/auth/step-up/email |
Email a Confirmation Code |
POST |
/auth/step-up/email/verify |
Confirm with an Emailed Code |
POST |
/auth/step-up/passkey/options |
Start Confirming with a Passkey |
POST |
/auth/step-up/passkey/verify |
Confirm with a Passkey |
Legacy Sign-In Redirect (Deprecated)
GET /auth/authkit
Deprecated: an old sign-in entry point, kept so existing links work. Redirects to the sign-in page, /login (keeping a same-origin next).
Authentication: None.
Query parameters
| Field | Type | Description | |
|---|---|---|---|
next |
string |
Same-origin path to land on after sign-in. Default /app. |
Response
302 To /login.
Headers:
| Field | Type | Description | |
|---|---|---|---|
Location |
string |
Where to go next. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/auth/authkit"
Sign-in Callback
GET /auth/callback
Where the browser returns after a social or SSO sign-in. Checks state against the browser’s cookie, exchanges the code, links the sign-in to the MachineMail account with the same verified email (or creates one), syncs their organizations, sets the mm_session cookie and redirects to next. A person with no organization gets one, with a first API key (welcome=1, GET /auth/first-key).
When the sign-in needs one more step, the browser goes to our own screens with a mm_signin cookie: /login/organization (pick an organization) or /login/verify (verify the email with a code), or on to the organization’s SSO connection.
Failures redirect to /login?error=signin_failed (the sign-in was refused or failed), access_denied (cancelled at the provider), unverified (an unverified email matching an existing account), linked (the account is already linked to a different sign-in identity), mfa (the sign-in asked for multi-factor authentication, which MachineMail does not support), methods (the organization does not allow this sign-in method), address_not_allowed (a new account for an address that receives mail through MachineMail: sign up with an address outside MachineMail), disposable_address (a new account for a disposable email address: sign up with an address you’ll keep receiving mail at), mailbox_has_account (a new account for a mailbox that already has one, such as a Gmail address with dots moved or a + alias: account carries the existing account’s masked address to sign in with, and invited=1 that an organization brought the person in) or codes_paused (too many codes for the address recently: a verification code was not sent; try later). A callback without state (one this browser did not start) never signs anyone in: it goes to /login, or to /invite/{token} when it carries an invitation token.
Authentication: None.
Query parameters
| Field | Type | Description | |
|---|---|---|---|
code |
string |
Authorization code from the sign-in. | |
state |
string |
The state sent with the authorization request. | |
error |
string |
Error code, when sign-in failed. | |
error_description |
string |
Error description, when sign-in failed. | |
invitation_token |
string |
Invitation token, on a sign-in started from an invitation email. |
Response
302 Signed in (Set-Cookie carries mm_session), our next screen, or back to /login with an error.
Headers:
| Field | Type | Description | |
|---|---|---|---|
Location |
string |
Where to go next. | |
Set-Cookie |
string |
mm_session= |
Errors
Example
curl "https://api.machinemail.dev/auth/callback"
Preview Claim
GET /auth/claim
Called by the dashboard /claim page for a signed-in human before confirming. Pass user_code or claim_attempt_token. Returns the registration (its agent-described name is untrusted), the claim email and the sandbox inbox. 403 when the registration is bound to a different email than the signed-in user; 429 after too many lookups (per user, 10 minute window).
Authentication: the dashboard mm_session cookie.
Query parameters
| Field | Type | Description | |
|---|---|---|---|
user_code |
string |
The 6-digit code the agent shows; spaces and dashes are ignored. | |
claim_attempt_token |
string |
The cat_... token from verification_uri. |
Response
200 The pending registration (application/json)
| Field | Type | Description | |
|---|---|---|---|
agent_id |
string |
required | ID of the registration (reg_...). |
registration_id |
string |
required | Same value as agent_id. |
registration_type |
"anonymous" | "service_auth" | "identity_assertion" |
required | anonymous: no human yet; a sandbox inbox until a human claims it. service_auth: on behalf of a named user, who confirms first. identity_assertion: an ID-JAG from a trusted agent provider. |
status |
"pending_claim" | "claimed" | "revoked" |
required | |
name |
string |
Agent-described label. Untrusted. | |
client |
string |
Provider name for identity_assertion, otherwise agent (self-described) or anonymous agent. | |
organization_id |
string |
Organization the registration is bound to (after a claim, or right away for identity_assertion). | |
inbox_id |
string |
The sandbox inbox of an anonymous registration. | |
email |
string |
Verified email bound at claim or carried by the identity assertion. | |
issuer |
string |
Issuer of the identity assertion (identity_assertion). | |
agent_platform |
string |
agent_platform claim of the identity assertion. | |
created_at |
string (date-time) |
required | When the agent registered. |
claimed_at |
string (date-time) |
When a human claimed the registration. | |
revoked_at |
string (date-time) |
When the registration was revoked. | |
expires_at |
string (date-time) |
When an unclaimed registration expires. | |
claim_email |
string |
Only this user may claim the registration. | |
user_code_expires_at |
string (date-time) |
When the code expires. | |
sandbox_inbox |
object |
The sandbox inbox of an anonymous registration. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
403 |
The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/auth/claim" \
-H "Cookie: mm_session=$SESSION"
{
"agent_id": "string",
"registration_id": "string",
"registration_type": "anonymous",
"status": "pending_claim",
"name": "Production",
"client": "string",
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"inbox_id": "agent@machinemail.dev",
"email": "agent@machinemail.dev",
"issuer": "string",
"agent_platform": "string",
"created_at": "2026-10-01T12:00:00.000Z",
"claimed_at": "2026-10-01T12:00:00.000Z",
"revoked_at": "2026-10-01T12:00:00.000Z",
"expires_at": "2026-10-01T12:00:00.000Z",
"claim_email": "agent@machinemail.dev",
"user_code_expires_at": "2026-10-01T12:00:00.000Z",
"sandbox_inbox": {
"pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
"inbox_id": "agent@machinemail.dev",
"email": "agent@machinemail.dev",
"display_name": "Support Agent",
"client_id": "my-client-id",
"status": "active",
"metadata": {
"key": "value"
},
"updated_at": "2026-10-01T12:00:00.000Z",
"created_at": "2026-10-01T12:00:00.000Z",
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"ephemeral": false,
"ttl_seconds": 1,
"expires_at": "2026-10-01T12:00:00.000Z",
"created_by": "api"
}
}
Complete Claim
POST /auth/claim
Called by the dashboard /claim page for a signed-in human. Binds the agent registration to their organization, moves the sandbox inbox into their default pod (made permanent) and invalidates pre-claim tokens. 403 when the signed-in user does not match the claim email; 429 after too many attempts (per user, 10 minute window).
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
user_code |
string |
required | The 6-digit code the agent shows; spaces and dashes are ignored. |
claim_attempt_token |
string |
The cat_... token from verification_uri; when given, it must belong to the same attempt as user_code. |
Response
200 Claimed (application/json)
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
402 |
Free plan: the agent inbox's received mail would take the organization past its monthly messages cap (limit_key: "messages_per_month"); nothing is claimed. Received mail counts toward the month it is claimed in. |
403 |
The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/claim" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"user_code": "string"
}'
{
"agent_id": "string",
"registration_id": "string",
"registration_type": "anonymous",
"status": "claimed",
"name": "Production",
"client": "string",
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"inbox_id": "agent@machinemail.dev",
"email": "agent@machinemail.dev",
"issuer": "string",
"agent_platform": "string",
"created_at": "2026-10-01T12:00:00.000Z",
"claimed_at": "2026-10-01T12:00:00.000Z",
"revoked_at": "2026-10-01T12:00:00.000Z",
"expires_at": "2026-10-01T12:00:00.000Z",
"inbox": {
"pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
"inbox_id": "agent@machinemail.dev",
"email": "agent@machinemail.dev",
"display_name": "Support Agent",
"client_id": "my-client-id",
"status": "active",
"metadata": {
"key": "value"
},
"updated_at": "2026-10-01T12:00:00.000Z",
"created_at": "2026-10-01T12:00:00.000Z",
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"ephemeral": false,
"ttl_seconds": 1,
"expires_at": "2026-10-01T12:00:00.000Z",
"created_by": "api"
}
}
Sign-in Options
GET /auth/config
How people sign in to MachineMail: email codes (POST /auth/email/start), social providers (GET /auth/social/{provider}), SSO and passkeys (POST /auth/passkey/options), all on MachineMail’s own pages.
Authentication: None.
Response
200 Sign-in options (application/json)
| Field | Type | Description | |
|---|---|---|---|
workos |
boolean |
required | Legacy alias of email_code, kept for older clients: sign in with an email code (POST /auth/email/start), a social provider (GET /auth/social/{provider}) or SSO. |
email_code |
boolean |
required | Email-code sign-in (POST /auth/email/start, then POST /auth/email/verify) is available. |
providers |
("google" | "github" | "apple" | "microsoft")[] |
required | Social providers on offer (GET /auth/social/{provider}). Empty when none are offered. |
magic_link |
boolean |
required | Magic-link sign-in (POST /auth/magic-link) is available. |
password |
boolean |
required | The deprecated password endpoints are available. |
authkit |
boolean |
required, deprecated | Deprecated: always false. Kept for older clients; see email_code. |
passkey |
boolean |
required | Passkey sign-in (POST /auth/passkey/options, then POST /auth/passkey/verify) is offered: show “Sign in with a passkey” and passkey autofill on the email field. |
Errors
| Status | Meaning |
|---|---|
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/auth/config"
{
"workos": false,
"email_code": false,
"providers": ["google"],
"magic_link": false,
"password": false,
"authkit": false,
"passkey": false
}
Turn Email Sign-in Codes On or Off
PUT /auth/email-codes
Turns email sign-in codes off for the signed-in person (they sign in with a passkey, or a connected Google, GitHub or single sign-on account), or back on. While off, asking for a code for the address sends no code, and the address gets an email saying to use a passkey instead. Each change is emailed to the address.
Turning codes off needs at least one passkey (409 otherwise) and a recent sign-in (see below). Turning them on needs neither. Lost every passkey with codes off? Contact support@machinemail.dev.
Needs a recent sign-in: within 10 minutes of signing in, or of confirming it’s you on this session (POST /auth/step-up/email/verify or POST /auth/step-up/passkey/verify). Otherwise 403 with error.code step_up_required and error.methods, the ways this account can confirm.
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
enabled |
boolean |
required | false turns email sign-in codes off; true back on. |
Response
200 The setting (application/json)
| Field | Type | Description | |
|---|---|---|---|
email_codes |
boolean |
required | Email sign-in codes work for this account now. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
403 |
Confirm it’s you first (error.code step_up_required, with error.methods), or the request came from another site. |
404 |
Not found. Also returned for resources outside the credential scope. |
409 |
Conflicts with an existing resource. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl -X PUT "https://api.machinemail.dev/auth/email-codes" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"enabled": true
}'
{
"email_codes": false
}
Send Sign-in Code
POST /auth/email/start
Starts an email-code sign-in: emails a 6-digit code, valid 10 minutes, and sets the mm_signin cookie that POST /auth/email/verify needs. Sending it again sends a new code. The answer is the same whether or not the address has an account; a malformed address is 400, and an unknown or used invitation_token is 404.
SSO discovery first: when the address’s domain is verified by an organization with an active SSO connection, no code is sent and redirect_to is that connection.
With an invitation_token and no email, the code goes to the invited address; signing in accepts the invitation.
429 also while email codes for the address are paused for this client IP (too many wrong codes from it; see POST /auth/email/verify), when this client IP (an IPv6 client by its /64) has had 5 new codes for the address in 15 minutes, and when the address has had 10 new codes in the last hour from any IP, counting verification codes. These limits only hold back new codes: a code already sent can still be used.
404 when email-code sign-in is not available (GET /auth/config).
Rate limited per client IP and per email address (5 codes per 15 minutes per address and client IP; 10 per hour per address): 429 with Retry-After.
A request whose Origin is another site is refused (403).
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
email |
string |
Address to send the code to. Optional with invitation_token (the code goes to the invited address). | |
next |
string |
Same-origin path to land on after sign-in, for example /cli?code=ABCD-EFGH. Anything else is ignored. Default /app. | |
invitation_token |
string |
An invitation token (from /invite/{token}): signing in accepts the invitation. |
Response
202 Code sent (if the address can receive mail), or the SSO redirect (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Set-Cookie |
string |
mm_signin= |
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required | |
redirect_to |
string |
Set when the address’s domain signs in with SSO: send the browser here (the organization’s identity provider). Otherwise a code was emailed: show the code screen. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
403 |
The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/auth/email/start" \
-H "Content-Type: application/json" \
-d '{
"email": "agent@machinemail.dev"
}'
{
"ok": true,
"redirect_to": "string"
}
Verify Sign-in Code
POST /auth/email/verify
Checks the emailed code for the sign-in in the mm_signin cookie. Signed in: sets mm_session and redirect_to is next. The sign-in may need another step first (signed_in: false, redirect_to is our organization picker, the email verification screen or SSO).
A wrong code is 400 (param code); after 3 wrong codes, or when the code has expired or was already used, there is no sign-in any more: 404, send a new code. Only the newest code sent to the address works: an earlier one is 400, and the newest works whichever browser asked for it. An invitation revoked, expired or accepted since the code was sent is 404 too. Other failures answer redirect_to: /login?error=<code>.
429 with Retry-After when the address has had 4 right codes in the last minute, or sign-in for the address is paused for a few minutes; the sign-in is kept, so the same code works once the wait is over. Wrong codes never count toward this, so guessing cannot hold the person back.
403 with error.code signup_address_not_allowed when the code is right but the address has no account yet and receives mail through MachineMail (an inbox on a MachineMail domain, or on a custom domain verified on MachineMail): new accounts need an address outside MachineMail, and the sign-in ends. An invitation from the organization that verified the custom domain is the exception. Codes are sent alike to every address, so this answer comes only to someone who proved they read the address’s mail.
403 with error.code disposable_email_not_allowed when the code is right but the address has no account yet and is a disposable email address: MachineMail doesn’t accept those for new accounts, and the sign-in ends. Like the answer above, it comes only after the code is checked; accounts that already exist sign in as before.
403 with error.code mailbox_has_account when the code is right but the address has no account yet and its mailbox already has one: one account per mailbox, and Gmail addresses with dots moved (or on googlemail.com) and + aliases on any domain are the same mailbox. The message names the existing account’s address, masked (k•••@gmail.com), to sign in with. Like the answer above, it comes only after the code is checked. Accounts made on such an address before this rule sign in as before.
20 wrong codes for one address from one client IP (an IPv6 client by its /64) within 24 hours, across all codes, pause email codes for that address from that IP: this endpoint and POST /auth/email/start answer 429 to it until the oldest is a day old, and the address is emailed. Other IPs are not affected, so guessing cannot lock the person out of their right code.
Rate limited per client IP and per email address (5 attempts, then 1 per minute, per address and client IP): 429 with Retry-After.
A request whose Origin is another site is refused (403).
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
code |
string |
required | The 6-digit code from the email. Spaces and dashes are ignored. |
Response
200 Signed in, or the next step (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required | |
signed_in |
boolean |
required | true: the mm_session cookie is set. false: another step comes first (organization picker, email verification, SSO). |
redirect_to |
string |
required | Where to send the browser next, built by the server: next (with welcome=1 after a first sign-in), /login/organization, /login/verify, an identity provider, or /login?error=. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
403 |
The request came from another site, or the code is right but would create a new account for an address that receives mail through MachineMail (error.code signup_address_not_allowed), for a disposable email address (disposable_email_not_allowed) or for a mailbox that already has an account (mailbox_has_account). |
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/auth/email/verify" \
-H "Content-Type: application/json" \
-d '{
"code": "string"
}'
{
"ok": true,
"signed_in": false,
"redirect_to": "string"
}
Get First API Key
GET /auth/first-key
The organization API key created on first sign-in, with its plaintext secret. Returned once: the record is deleted on read and expires 10 minutes after sign-in; 404 afterwards.
Authentication: the dashboard mm_session cookie.
Response
200 The key, shown once (application/json)
| Field | Type | Description | |
|---|---|---|---|
api_key |
OneTimeApiKey |
required | An API key with its plaintext secret, returned once. |
api_key is a OneTimeApiKey:
| Field | Type | Description | |
|---|---|---|---|
api_key_id |
string |
required | ID of the API key. |
api_key |
string |
required | Plaintext secret (mm_live_...). Shown once. |
prefix |
string |
required | First characters of the key, shown in listings. |
name |
string |
required | Name of the key. |
created_at |
string (date-time) |
required | When the key was created. |
Errors
| Status | Meaning |
|---|---|
401 |
Missing, invalid, expired or revoked credential. |
404 |
Not found. Also returned for resources outside the credential scope. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/auth/first-key" \
-H "Cookie: mm_session=$SESSION"
{
"api_key": {
"api_key_id": "key_01k6x3e8mr0000h4r0ty6c9k1n",
"api_key": "mm_live_4f8Kq2ZbX9mN3pR7tV1wY6cH0jL5sD8a",
"prefix": "mm_live_4f8K",
"name": "Production",
"created_at": "2026-10-01T12:00:00.000Z"
}
}
Preview Invitation
GET /auth/invitations/{token}
What an invitation link (/invite/{token}) is for: the organization, the invited address (masked) and the state. Accept it by signing in with invitation_token (POST /auth/email/start or GET /auth/social/{provider}). 404 for an unknown token. An expired, revoked or already accepted invitation is a 200 with that state. 429 with Retry-After when the lookup is rate limited, or cannot be answered right now (an outage): retry after that many seconds.
Authentication: None.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
token |
string |
required | The token from the invitation link (/invite/{token}). |
Response
200 The invitation (application/json)
| Field | Type | Description | |
|---|---|---|---|
organization_name |
string |
required | The organization the invitation is to. |
email |
string |
required | The invited address, masked (a••@example.com). |
state |
"pending" | "accepted" | "expired" | "revoked" |
required | Only a pending invitation can be accepted. |
expires_at |
string (date-time) |
required | When the invitation expires. |
Errors
| Status | Meaning |
|---|---|
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/auth/invitations/string"
{
"organization_name": "string",
"email": "agent@machinemail.dev",
"state": "pending",
"expires_at": "2026-10-01T12:00:00.000Z"
}
Log In (Deprecated)
POST /auth/login
Deprecated: the dashboard signs in with an email code (POST /auth/email/start). Sets the mm_session cookie.
Not available: 404.
Rate limited per client IP and per email address (5 attempts, then 1 per minute; a successful sign-in resets the address): 429 with Retry-After.
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
email |
string |
required | |
password |
string |
required |
Response
200 Signed in (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Set-Cookie |
string |
mm_session= |
| Field | Type | Description | |
|---|---|---|---|
user |
User |
required | A person who signs in to the dashboard. |
organization |
Organization |
required | The organization of the credential. plan and GET /v0/organizations/usage report plan allowances; inboxes are unlimited (no inbox_limit) and there are no billing fields. |
user is a User:
| Field | Type | Description | |
|---|---|---|---|
user_id |
string |
required | ID of the user. |
email |
string |
required | Email address the user signs in with. |
name |
string | null |
Display name, when set. | |
organization_id |
string |
required | ID of the organization this record is about: the session’s active organization, or the organization listed. |
role |
"owner" | "admin" | "member" |
required | Role in the organization. Owners and admins invite, change roles and remove members; only owners grant or remove the owner role, and an organization always keeps one owner. |
created_at |
string (date-time) |
required | When the user was created. |
organization is a Organization:
| Field | Type | Description | |
|---|---|---|---|
organization_id |
string |
required | ID of the organization. |
name |
string |
required | Name of the organization. |
slug |
string |
required | URL-safe name of the organization, chosen from its name when it is created. It never changes, not even when the organization is renamed. {slug}@ on the shared domain is reserved for this organization: no other organization can create that inbox. |
plan |
"free" | "pro" | "team" | "scale" |
required | Plan of the organization. |
inbox_count |
integer |
required | Number of inboxes in the credential scope. |
domain_count |
integer |
required | Number of domains in the organization. |
domain_limit |
integer |
Custom domains included in the plan. The Free plan cannot add more; paid plans can, and each extra domain is billed. Always present on GET /v0/organizations and PATCH /v0/organizations. | |
usage |
object |
Usage. Always present on GET /v0/organizations and PATCH /v0/organizations. | |
shared_domain |
string |
Domain every organization can create inboxes on, used when an inbox is created without domain. Always present on GET /v0/organizations and PATCH /v0/organizations. | |
undo_send |
boolean |
required | Undo send in the dashboard: when true (the default), a message sent from the MachineMail dashboard waits 5 seconds before it goes out, so it can be undone. Only the dashboard reads it: the API, the MCP server and the CLI always send at once. |
default_inbox_id |
string | null |
required | The default inbox of the organization (its inbox_id). A new organization gets {slug}@ on the shared domain, named after the organization, as its default inbox; when that address cannot be used (a reserved name), it starts without one. The dashboard lists the default inbox first. Change it with PATCH /v0/organizations. null when there is none: it was cleared, its inbox was deleted, or (for a pod or inbox credential) it is outside the credential scope. |
updated_at |
string (date-time) |
required | Time at which the organization was last updated. |
created_at |
string (date-time) |
required | Time at which the organization was created. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "agent@machinemail.dev",
"password": "string"
}'
{
"user": {
"user_id": "usr_01k6x3j0rw0000n8w4yc0g3q5s",
"email": "agent@machinemail.dev",
"name": "Production",
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"role": "owner",
"created_at": "2026-10-01T12:00:00.000Z"
},
"organization": {
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"name": "Production",
"slug": "string",
"plan": "free",
"inbox_count": 1,
"domain_count": 1,
"domain_limit": 1,
"usage": {
"message_count": 1,
"storage_bytes": 2048,
"messages_received_30d": 1,
"messages_sent_30d": 1
},
"shared_domain": "string",
"undo_send": true,
"default_inbox_id": "acme@machinemail.dev",
"updated_at": "2026-10-01T12:00:00.000Z",
"created_at": "2026-10-01T12:00:00.000Z"
}
}
Log Out
POST /auth/logout
Ends the session in the mm_session cookie, if any, and clears the cookie. Succeeds without a session too.
Authentication: None.
Response
200 Signed out (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required |
Errors
| Status | Meaning |
|---|---|
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/auth/logout"
{
"ok": true
}
Redeem Magic Link
GET /auth/magic
The link in the sign-in email. Consumes the token (single use), sets the mm_session cookie and redirects to next (default /app). On a first sign-in it creates the user, an organization, a Default Pod and a first organization API key, and adds welcome=1 to the redirect; fetch that key once with GET /auth/first-key.
Authentication: None.
Query parameters
| Field | Type | Description | |
|---|---|---|---|
token |
string |
The token from the emailed link. A missing, unknown, used or expired token redirects to /login?error=expired. |
Response
302 Signed in: Location is next and Set-Cookie carries mm_session. Otherwise Location is /login?error=expired, /login?error=address_not_allowed when the link would create an account for an address that receives mail through MachineMail, /login?error=disposable_address when it would create one for a disposable email address, or /login?error=mailbox_has_account&account=<masked address> (with &invited=1 for an invitation) when the address’s mailbox already has an account.
Headers:
Errors
Example
curl "https://api.machinemail.dev/auth/magic"
Request Magic Link
POST /auth/magic-link
Not available on MachineMail, which uses email-code sign-in: returns 404. Sign in with an email code instead (POST /auth/email/start).
Rate limited per client IP and per email address (5 links per 15 minutes per address): 429 with Retry-After.
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
email |
string |
required | Address to send the sign-in link to. |
org_name |
string |
Organization name, used only when this is the address’s first sign-in. | |
next |
string |
Same-origin path to land on after sign-in, for example /claim?user_code=123456. Anything else is ignored. Default /app. | |
organization_name |
string |
Alias of org_name. | |
organizationName |
string |
deprecated | Alias of org_name. |
Response
202 Accepted. The link was sent if the address can receive mail. (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/auth/magic-link" \
-H "Content-Type: application/json" \
-d '{
"email": "agent@machinemail.dev"
}'
{
"ok": true
}
Current User
GET /auth/me
The signed-in user and the organization the session is on; user.role is their role there. For what an API key can see, use GET /v0/auth/me.
Authentication: the dashboard mm_session cookie.
Response
200 The session (application/json)
user is a User:
organization is a Organization:
Errors
| Status | Meaning |
|---|---|
401 |
Missing, invalid, expired or revoked credential. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/auth/me" \
-H "Cookie: mm_session=$SESSION"
Preview App Authorization
GET /auth/oauth/authorize
Called by the dashboard /connect page for a signed-in person. Returns the client (its name and logo are self-reported unless it identified itself with a metadata document URL), where the response goes, the resource and the requested scopes. 404 for an unknown, decided or expired request. Dashboard session only: API keys are not accepted.
Authentication: the dashboard mm_session cookie.
Query parameters
| Field | Type | Description | |
|---|---|---|---|
request_id |
string |
required | The request parameter of the /connect page URL. |
Response
200 The pending request (application/json)
| Field | Type | Description | |
|---|---|---|---|
request_id |
string |
required | |
client |
OAuthClientInfo |
required | |
redirect_uri |
string |
required | Where the response goes. |
redirect_host |
string |
required | Host (or scheme, for an app link) of redirect_uri, for the human to check. |
redirect_loopback |
boolean |
required | The response goes to a program on this computer (http://127.0.0.1, localhost). |
resource |
string |
required | What the token will be good for: the MCP server (https://mcp.machinemail.dev/mcp, or the legacy |
scopes |
ScopeDescription[] |
required | What the client asked for. |
created_at |
string (date-time) |
required | |
expires_at |
string (date-time) |
required | When the request expires (10 minutes after it was made). |
client is a OAuthClientInfo:
| Field | Type | Description | |
|---|---|---|---|
client_id |
string |
required | |
client_name |
string | null |
required | Self-reported by the client. |
client_uri |
string | null |
required | Self-reported; https only. |
logo_uri |
string | null |
required | Self-reported; https only. |
registration |
"dynamic" | "metadata_document" |
required | dynamic: registered at /oauth2/register (name and logo unverified). metadata_document: client_id is the URL of its metadata document, so its host is verified. |
host |
string | null |
required | metadata_document: the host that serves the document (for example claude.ai). |
Each item of scopes is a ScopeDescription:
| Field | Type | Description | |
|---|---|---|---|
scope |
"inboxes.read" | "inboxes.write" | "messages.read" | "messages.send" | "messages.write" | "webhooks.read" | "webhooks.write" | "domains.read" | "domains.write" | "lists.read" | "lists.write" | "metrics.read" | "pods.read" | "pods.write" | "api_keys.read" | "api_keys.write" |
required | OAuth scope. Each grants API key permissions: inboxes.* (inbox_*), messages.read (message, thread and label reads), messages.send (send, reply, drafts), messages.write (update and delete messages and threads), webhooks., domains., lists.* (allow/block lists and suppressions), pods., api_keys., metrics.read (metrics and the event log). |
description |
string |
required |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/auth/oauth/authorize?request_id=string" \
-H "Cookie: mm_session=$SESSION"
{
"request_id": "string",
"client": {
"client_id": "my-client-id",
"client_name": "string",
"client_uri": "https://example.com",
"logo_uri": "https://example.com",
"registration": "dynamic",
"host": "string"
},
"redirect_uri": "https://example.com",
"redirect_host": "string",
"redirect_loopback": false,
"resource": "string",
"scopes": [
{
"scope": "inboxes.read",
"description": "string"
}
],
"created_at": "2026-10-01T12:00:00.000Z",
"expires_at": "2026-10-01T12:00:00.000Z"
}
Approve App Authorization
POST /auth/oauth/authorize/approve
Approves a pending request for the organization the session is signed in to, optionally limited to one pod or one inbox of it, and returns redirect_to (the client’s redirect_uri with code, state and iss) for the browser to follow. The app then appears under Connected apps (/v0/oauth-grants). Dashboard session only.
A person in several organizations connects the app to another one by switching the session to it first (POST /auth/organizations/switch, which applies that organization’s sign-in policy), then approving. organization_id names the organization the page showed: 409 conflict when the session is no longer on it. A pod_id or inbox_id outside the session’s organization is 404.
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
request_id |
string |
required | |
pod_id |
string |
Limit the grant to one pod of the organization. Default: the whole organization. | |
inbox_id |
string |
Limit the grant to one inbox (and its pod). | |
organization_id |
string |
The organization the grant is for, as the consent page showed it: it must be the one the session is signed in to (409 conflict otherwise, for example after another tab switched organizations). To connect the app to another organization the person belongs to, switch the session to it first (POST /auth/organizations/switch). Default: the session’s organization. |
Response
200 Approved (application/json)
| Field | Type | Description | |
|---|---|---|---|
status |
"approved" | "denied" |
required | |
redirect_to |
string |
required | Send the browser here: redirect_uri with code (or error=access_denied), state and iss. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
404 |
Not found. Also returned for resources outside the credential scope. |
409 |
Conflicts with an existing resource. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/oauth/authorize/approve" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"request_id": "string"
}'
{
"status": "approved",
"redirect_to": "string"
}
Deny App Authorization
POST /auth/oauth/authorize/deny
Denies a pending request and returns redirect_to (the client’s redirect_uri with error=access_denied, state and iss). Dashboard session only.
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
request_id |
string |
required |
Response
200 Denied (application/json)
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/oauth/authorize/deny" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"request_id": "string"
}'
{
"status": "approved",
"redirect_to": "string"
}
List My Organizations
GET /auth/organizations
Every organization the signed-in person belongs to, with their role, for the organization switcher.
Authentication: the dashboard mm_session cookie.
Response
200 Organizations (application/json)
| Field | Type | Description | |
|---|---|---|---|
organizations |
MembershipOrganization[] |
required | Every organization the person belongs to, by name. |
Each item of organizations is a MembershipOrganization:
| Field | Type | Description | |
|---|---|---|---|
organization_id |
string |
required | ID of the organization. |
name |
string |
required | Name of the organization. |
slug |
string |
required | URL-safe name of the organization. Fixed when the organization is created; it never changes. |
role |
"owner" | "admin" | "member" |
required | Role in the organization. Owners and admins invite, change roles and remove members; only owners grant or remove the owner role, and an organization always keeps one owner. |
current |
boolean |
required | The session is signed in to this organization. |
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/auth/organizations" \
-H "Cookie: mm_session=$SESSION"
{
"organizations": [
{
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"name": "Production",
"slug": "string",
"role": "owner",
"current": false
}
]
}
Create Organization
POST /auth/organizations
Creates an organization (with its Default Pod) owned by the signed-in person, and switches the session to it.
403 with error.code signup_address_not_allowed when the person’s email address receives mail through MachineMail (someone an organization invited on a custom domain it verified): they can belong to organizations that invite them, but not create one.
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
name |
string |
required | Name of the new organization. |
Response
201 Created; the session is on the new organization (application/json)
user is a User:
organization is a Organization:
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
402 |
An account can own one Free organization, and this one already does (limit_key: "free_organizations"). Upgrade it, or contact support@machinemail.dev. |
403 |
The person’s address receives mail through MachineMail (error.code signup_address_not_allowed), or an organization they belong to is suspended (account_suspended). |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/organizations" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"name": "Production"
}'
{
"user": {
"user_id": "usr_01k6x3j0rw0000n8w4yc0g3q5s",
"email": "agent@machinemail.dev",
"name": "Production",
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"role": "owner",
"created_at": "2026-10-01T12:00:00.000Z"
},
"organization": {
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"name": "Production",
"slug": "string",
"plan": "free",
"inbox_count": 1,
"domain_count": 1,
"domain_limit": 1,
"usage": {
"message_count": 1,
"storage_bytes": 2048,
"messages_received_30d": 1,
"messages_sent_30d": 1
},
"shared_domain": "string",
"undo_send": true,
"default_inbox_id": "acme@machinemail.dev",
"updated_at": "2026-10-01T12:00:00.000Z",
"created_at": "2026-10-01T12:00:00.000Z"
}
}
Switch Organization
POST /auth/organizations/switch
Moves the session to another organization the person belongs to. 404 when they do not belong to it.
An organization other than the one the session signed in to must allow how the session signed in, under its own sign-in policy. A sign-in that has expired with the sign-in service (sessions here can outlast it) still switches to any organization that does not use single sign-on.
403 with error.code sso_required when the organization uses single sign-on and the session did not sign in with it: send the browser to GET /auth/sso?organization_id=…, which signs in through the organization’s single sign-on and lands on it. 403 with error.code sign_in_required when the organization allows only other sign-in methods, or the sign-in could not be confirmed for it: sign in to it again.
429 when the sign-in service could not be reached, or another request changed the session at the same time: retry after Retry-After. The session stays where it was.
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
organization_id |
string |
required | ID of an organization the person belongs to. |
Response
200 The session, on the organization (application/json)
user is a User:
organization is a Organization:
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
403 |
The organization needs another sign-in: single sign-on (error.code sso_required), or another method (sign_in_required). |
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/organizations/switch" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e"
}'
Start Passkey Sign-in
POST /auth/passkey/options
Starts a passkey sign-in: returns the options for the browser’s WebAuthn API (no address needed: the browser offers the person’s passkeys for MachineMail, also as autofill on the email field) and sets the mm_passkey cookie that POST /auth/passkey/verify needs. The challenge works once, for 5 minutes; ask again for a new one.
404 when passkey sign-in is not available (GET /auth/config).
Rate limited per client IP: 429 with Retry-After.
A request whose Origin is another site is refused (403).
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
next |
string |
Same-origin path to land on after sign-in. Anything else is ignored. Default /app. | |
invitation_token |
string |
An invitation token (from /invite/{token}): signing in accepts the invitation. |
Response
200 Options for the browser (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Set-Cookie |
string |
mm_passkey= |
| Field | Type | Description | |
|---|---|---|---|
options |
Record<string, unknown> |
required | PublicKeyCredentialRequestOptionsJSON: pass to navigator.credentials.get(). |
Errors
Example
curl -X POST "https://api.machinemail.dev/auth/passkey/options" \
-H "Content-Type: application/json" \
-d '{}'
{
"options": {
"key": "value"
}
}
Sign In with a Passkey
POST /auth/passkey/verify
Checks the browser’s passkey assertion against the challenge it signed, which must have been issued to this browser (the mm_passkey cookie); the challenge is used up either way. Signed in: sets mm_session and redirect_to is next. The sign-in may need another step first (signed_in: false, redirect_to is the organization picker or single sign-on), as for POST /auth/email/verify; other failures answer redirect_to: /login?error=<code>.
A passkey MachineMail no longer knows (removed from the account) is 400 with error.code passkey_unknown and error.credential_id: tell the browser it is gone (PublicKeyCredential.signalUnknownCredential) and ask the person to remove it from their password manager. Any other passkey that does not check out is 400 with error.code passkey_invalid. No challenge, or an expired one, is 404: start again.
429 with Retry-After when the address has had 4 passkey sign-ins in the last minute, or sign-in for it is paused for a few minutes.
Rate limited per client IP.
A request whose Origin is another site is refused (403).
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
credential |
Record<string, unknown> |
required | The browser’s AuthenticationResponseJSON. |
Response
200 Signed in, or the next step (application/json)
Errors
Example
curl -X POST "https://api.machinemail.dev/auth/passkey/verify" \
-H "Content-Type: application/json" \
-d '{
"credential": {
"key": "value"
}
}'
{
"ok": true,
"signed_in": false,
"redirect_to": "string"
}
List My Passkeys
GET /auth/passkeys
The signed-in person’s passkeys, whether email sign-in codes are on, how to confirm it’s them before sensitive changes, and whether to offer creating a passkey.
404 when passkeys are not available to this account.
Authentication: the dashboard mm_session cookie.
Response
200 Passkeys and settings (application/json)
| Field | Type | Description | |
|---|---|---|---|
passkeys |
Passkey[] |
required | The person’s passkeys, oldest first. |
email_codes |
boolean |
required | Email sign-in codes work for this account. false once turned off (with at least one passkey). |
step_up |
object |
required | Confirmation before sensitive changes. |
prompt |
boolean |
required | Offer to create a passkey (no passkey yet, and the offer was not snoozed or declined). |
Each item of passkeys is a Passkey:
| Field | Type | Description | |
|---|---|---|---|
passkey_id |
string |
required | ID of the passkey (pk_…). |
name |
string |
required | Name shown in Settings (named after its password manager or device when added; up to 64 characters). |
created_at |
string (date-time) |
required | When the passkey was added. |
last_used_at |
string (date-time) | null |
required | When it last signed in or confirmed a change. |
backed_up |
boolean |
required | Synced by its password manager to the person’s other devices. |
device_type |
"singleDevice" | "multiDevice" |
multiDevice: a synced passkey. singleDevice: bound to one device or security key. |
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/auth/passkeys" \
-H "Cookie: mm_session=$SESSION"
{
"passkeys": [
{
"passkey_id": "string",
"name": "Production",
"created_at": "2026-10-01T12:00:00.000Z",
"last_used_at": "2026-10-01T12:00:00.000Z",
"backed_up": false,
"device_type": "singleDevice"
}
],
"email_codes": false,
"step_up": {
"recent_until": "2026-10-01T12:00:00.000Z",
"methods": ["passkey"]
},
"prompt": false
}
Add a Passkey
POST /auth/passkeys
Adds the passkey the browser created from POST /auth/passkeys/registration (within 5 minutes, on the same session; 404 otherwise). User verification (a device PIN, fingerprint or face) is required. A passkey already on an account is 409; one that does not check out is 400. The account’s address gets an email saying a passkey was added.
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
credential |
Record<string, unknown> |
required | The browser’s RegistrationResponseJSON. |
name |
string |
Name for the passkey (up to 64 characters). Default: its password manager or device, or “Passkey”. |
Response
201 Added (application/json)
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/passkeys" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"credential": {
"key": "value"
},
"name": "Production"
}'
{
"passkey_id": "string",
"name": "Production",
"created_at": "2026-10-01T12:00:00.000Z",
"last_used_at": "2026-10-01T12:00:00.000Z",
"backed_up": false,
"device_type": "singleDevice"
}
Answer the Passkey Offer
POST /auth/passkeys/prompt
“Not now” (ask again in 14 days) or “Don’t ask again” on the offer to create a passkey.
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
action |
"not_now" | "never" |
required | not_now: ask again in 14 days. never: don’t ask again. |
Response
200 Saved (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required |
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/passkeys/prompt" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"action": "not_now"
}'
{
"ok": true
}
Start Adding a Passkey
POST /auth/passkeys/registration
Options for the browser to create a passkey for this account (navigator.credentials.create()). Finish within 5 minutes with POST /auth/passkeys, on this session. An account can have 20 passkeys (409 past that).
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Response
200 Options for the browser (application/json)
| Field | Type | Description | |
|---|---|---|---|
options |
Record<string, unknown> |
required | PublicKeyCredentialCreationOptionsJSON: pass to navigator.credentials.create(). |
Errors
| Status | Meaning |
|---|---|
401 |
Missing, invalid, expired or revoked credential. |
403 |
Confirm it’s you first (error.code step_up_required, with error.methods), or the request came from another site. |
404 |
Not found. Also returned for resources outside the credential scope. |
409 |
Conflicts with an existing resource. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/passkeys/registration" \
-H "Cookie: mm_session=$SESSION"
{
"options": {
"key": "value"
}
}
Rename a Passkey
PATCH /auth/passkeys/{passkey_id}
Renames one of the signed-in person’s passkeys (1 to 64 characters).
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
passkey_id |
string |
required | ID of the passkey (pk_…). |
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
name |
string |
required | New name, 1 to 64 characters. |
Response
200 Renamed (application/json)
Errors
Error bodies use the error envelope.
Example
curl -X PATCH "https://api.machinemail.dev/auth/passkeys/string" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"name": "Production"
}'
{
"passkey_id": "string",
"name": "Production",
"created_at": "2026-10-01T12:00:00.000Z",
"last_used_at": "2026-10-01T12:00:00.000Z",
"backed_up": false,
"device_type": "singleDevice"
}
Remove a Passkey
DELETE /auth/passkeys/{passkey_id}
Removes one of the signed-in person’s passkeys and signs out every session it signed in (this one too, when it did: signed_out). The account’s address gets an email saying so.
409 for the last passkey while email sign-in codes are off: turn them on first (PUT /auth/email-codes), or add another passkey.
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
passkey_id |
string |
required | ID of the passkey (pk_…). |
Response
200 Removed (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required | |
sessions_ended |
integer |
required | Sessions the passkey had signed in that were signed out. |
signed_out |
boolean |
required | This session was one of them: sign in again. |
Errors
Error bodies use the error envelope.
Example
curl -X DELETE "https://api.machinemail.dev/auth/passkeys/string" \
-H "Cookie: mm_session=$SESSION"
{
"ok": true,
"sessions_ended": 1,
"signed_out": false
}
Get Sign-in in Progress
GET /auth/pending
The sign-in in progress in this browser (mm_signin cookie), for the code, organization picker and email verification screens. 404 when there is none or it expired.
Authentication: None.
Response
200 The sign-in in progress (application/json)
| Field | Type | Description | |
|---|---|---|---|
step |
"code" | "organization" | "verify_email" |
required | code: waiting for the emailed sign-in code. organization: pick an organization. verify_email: enter the code that verifies the email. |
email |
string |
The address signing in, when known. | |
organizations |
object[] |
For organization: the organizations to pick from. | |
invitation |
boolean |
required | Signing in accepts an invitation. |
Errors
| Status | Meaning |
|---|---|
404 |
Not found. Also returned for resources outside the credential scope. |
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/auth/pending"
{
"step": "code",
"email": "agent@machinemail.dev",
"organizations": [
{
"id": "string",
"name": "Production"
}
],
"invitation": false
}
Choose Organization
POST /auth/pending/organization
Finishes a sign-in that needs an organization picked (organization_selection_required) with one of the organizations offered (GET /auth/pending); any other ID is 400.
403 with error.code signup_address_not_allowed when this step would create a new account for an address that receives mail through MachineMail, disposable_email_not_allowed when it would create one for a disposable email address, or mailbox_has_account when the address’s mailbox already has an account, as for POST /auth/email/verify. Existing accounts are not affected.
A request whose Origin is another site is refused (403).
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
organization_id |
string |
required | One of the organizations from GET /auth/pending. |
Response
200 Signed in, or the next step (application/json)
Headers:
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
403 |
The request came from another site, or this step would create a new account for an address that receives mail through MachineMail (error.code signup_address_not_allowed), for a disposable email address (disposable_email_not_allowed) or for a mailbox that already has an account (mailbox_has_account). |
404 |
Not found. Also returned for resources outside the credential scope. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/auth/pending/organization" \
-H "Content-Type: application/json" \
-d '{
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e"
}'
{
"ok": true,
"signed_in": false,
"redirect_to": "string"
}
Verify Email
POST /auth/pending/verify
Finishes a sign-in that needs the email verified (email_verification_required, for example a GitHub account whose email GitHub does not vouch for) with the code emailed to that address. Wrong codes as for POST /auth/email/verify. Each such sign-in is a new code for the address, counted with email codes (POST /auth/email/start): past those limits the callback goes to /login?error=codes_paused and no code is sent.
Rate limited per client IP and per email address (5 attempts, then 1 per minute, per address and client IP): 429 with Retry-After.
A request whose Origin is another site is refused (403).
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
code |
string |
required | The 6-digit code from the email. Spaces and dashes are ignored. |
Response
200 Signed in, or the next step (application/json)
Headers:
Errors
Example
curl -X POST "https://api.machinemail.dev/auth/pending/verify" \
-H "Content-Type: application/json" \
-d '{
"code": "string"
}'
{
"ok": true,
"signed_in": false,
"redirect_to": "string"
}
Sign Out Other Sessions
POST /auth/sessions/revoke-others
Signs the person out of every session except this one, on every device.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Response
200 Signed out (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required | |
ended |
integer |
required | Other sessions signed out. |
Errors
| Status | Meaning |
|---|---|
401 |
Missing, invalid, expired or revoked credential. |
403 |
The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/sessions/revoke-others" \
-H "Cookie: mm_session=$SESSION"
{
"ok": true,
"ended": 1
}
Sign Up (Deprecated)
POST /auth/signup
Deprecated: the dashboard signs in with an email code (POST /auth/email/start). Creates a user, an organization, a Default Pod and a first organization API key (returned once as api_key), and sets the mm_session cookie.
It does not verify the email address, so it is not available: 404.
A taken address and a password shorter than 8 characters both answer 409 with the same message.
Rate limited per client IP and per email address (5 attempts, then 1 per minute): 429 with Retry-After.
Authentication: None.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
email |
string |
required | |
password |
string |
required | At least 8 characters. |
name |
string |
Display name of the user. | |
org_name |
string |
Organization name. | |
organization_name |
string |
Alias of org_name. | |
organizationName |
string |
deprecated | Alias of org_name. |
Response
201 Signed up and signed in (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
user |
User |
required | A person who signs in to the dashboard. |
organization |
Organization |
required | The organization of the credential. plan and GET /v0/organizations/usage report plan allowances; inboxes are unlimited (no inbox_limit) and there are no billing fields. |
api_key |
OneTimeApiKey |
required | An API key with its plaintext secret, returned once. |
user is a User:
organization is a Organization:
api_key is a OneTimeApiKey:
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
403 |
This would create a new account for an address that receives mail through MachineMail (error.code signup_address_not_allowed): new accounts need an email address outside MachineMail. Or for a disposable email address (disposable_email_not_allowed): MachineMail doesn’t accept those. |
404 |
Not found. Also returned for resources outside the credential scope. |
409 |
Conflicts with an existing resource. |
429 |
Rate limited. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/auth/signup" \
-H "Content-Type: application/json" \
-d '{
"email": "agent@machinemail.dev",
"password": "string",
"name": "Production"
}'
Sign In with a Social Provider
GET /auth/social/{provider}
Sends the browser straight to Google, GitHub, Apple or Microsoft (no page in between), with a single-use state bound to this browser by a short-lived cookie (mm_oauth_state, Path=/auth/callback). The provider returns to GET /auth/callback.
404 when that provider is not offered (GET /auth/config lists them).
Authentication: None.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
provider |
"google" | "github" | "apple" | "microsoft" |
required | A social sign-in provider. |
Query parameters
| Field | Type | Description | |
|---|---|---|---|
next |
string |
Same-origin path to land on after sign-in. Default /app. | |
invitation_token |
string |
An invitation token (from /invite/{token}): signing in accepts that invitation. |
Response
302 To the provider.
Headers:
| Field | Type | Description | |
|---|---|---|---|
Location |
string |
Where to go next. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
404 |
Not found. Also returned for resources outside the credential scope. |
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/auth/social/google"
Sign In to an Organization with SSO
GET /auth/sso
Where the dashboard sends the browser when switching to an organization answers sso_required: straight to that organization’s single sign-on (no page in between), under a single-use state bound to this browser (mm_oauth_state, as for GET /auth/social/{provider}). Back at GET /auth/callback, the session is on that organization.
Needs a signed-in session and membership in the organization: without a session, redirects to /login. 404 when the person does not belong to the organization. An organization with no single sign-on (any more) redirects to next, on the organization the session is on: switch again from there.
Authentication: the dashboard mm_session cookie.
Query parameters
| Field | Type | Description | |
|---|---|---|---|
organization_id |
string |
ID of an organization the signed-in person belongs to. | |
next |
string |
Same-origin path to land on after sign-in. Default /app. |
Response
302 To the organization’s single sign-on, /login without a session, or next when the organization has no single sign-on.
Headers:
| Field | Type | Description | |
|---|---|---|---|
Location |
string |
Where to go next. |
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/auth/sso" \
-H "Cookie: mm_session=$SESSION"
Email a Confirmation Code
POST /auth/step-up/email
Emails a 6-digit code (10 minutes, 3 tries) that confirms it’s the signed-in person before a sensitive change; check it with POST /auth/step-up/email/verify. A new code replaces the last. 409 while email codes are off for the account: confirm with a passkey. 429 after 3 codes in an hour.
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Response
202 Code sent (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required | |
email |
string |
required | Where the code went, masked. |
expires_at |
string (date-time) |
required | When the code expires (10 minutes). |
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/step-up/email" \
-H "Cookie: mm_session=$SESSION"
{
"ok": true,
"email": "agent@machinemail.dev",
"expires_at": "2026-10-01T12:00:00.000Z"
}
Confirm with an Emailed Code
POST /auth/step-up/email/verify
Checks the code from POST /auth/step-up/email for this session. Right: sensitive changes need no further confirmation for 10 minutes (recent_until). A wrong code is 400 (param code), and the third ends the code: send a new one.
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
code |
string |
required | The 6-digit code from the email. Spaces and dashes are ignored. |
Response
200 Confirmed (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required | |
recent_until |
string (date-time) |
required | Until when sensitive changes need no further confirmation. |
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/step-up/email/verify" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"code": "string"
}'
{
"ok": true,
"recent_until": "2026-10-01T12:00:00.000Z"
}
Start Confirming with a Passkey
POST /auth/step-up/passkey/options
Options for the browser to confirm it’s the signed-in person with one of their passkeys (5 minutes). 409 when they have none.
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Response
200 Options for the browser (application/json)
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/step-up/passkey/options" \
-H "Cookie: mm_session=$SESSION"
{
"options": {
"key": "value"
}
}
Confirm with a Passkey
POST /auth/step-up/passkey/verify
Checks the browser’s passkey assertion against POST /auth/step-up/passkey/options (on this session). It must be one of the signed-in person’s passkeys. Confirmed: sensitive changes need no further confirmation for 10 minutes (recent_until). Errors as for POST /auth/passkey/verify.
404 when passkeys are not available to this account.
A request whose Origin is another site is refused (403).
Authentication: the dashboard mm_session cookie.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
credential |
Record<string, unknown> |
required | The browser’s AuthenticationResponseJSON. |
Response
200 Confirmed (application/json)
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/auth/step-up/passkey/verify" \
-H "Cookie: mm_session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"credential": {
"key": "value"
}
}'
{
"ok": true,
"recent_until": "2026-10-01T12:00:00.000Z"
}