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_session cookie, 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.

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=; HttpOnly; SameSite=Lax; Path=/ (plus Secure on https).

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=; HttpOnly; SameSite=Lax; Path=/auth (the sign-in in progress, 10 minutes), and mm_session once signed in.
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.

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=; HttpOnly; SameSite=Lax; Path=/ (plus Secure on https).
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 /mcp) or / (the REST API and MCP).
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=; HttpOnly; SameSite=Lax; Path=/auth (this browser’s passkey sign-ins, 5 minutes; kept across requests, so several tabs can sign in at once).
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.

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