Dashboard Auth · MachineMail Docs

Skip to content Skip to content

[01/40]Legacy Sign-In Redirect (Deprecated)

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)

http

GET /auth/authkit

Warning

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

FieldDescription

next``string

Same-origin path to land on after sign-in. Default /app.

Response

302 To /login.

Headers:

FieldDescription

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

Request

curl "https://api.machinemail.dev/auth/authkit"

Sign-in Callback

http

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

FieldDescription

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:

FieldDescription

Location``string

Where to go next.

Set-Cookie``string

mm_session=; HttpOnly; SameSite=Lax; Path=/ (plus Secure on https).

Errors

Status Meaning
400 Invalid request: error.type is validation and error.param names the field.
500 Unexpected server error.

Example

Request

curl "https://api.machinemail.dev/auth/callback"

Preview Claim

http

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

FieldDescription

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)

FieldDescription

agent_id``stringRequired

ID of the registration (reg_...).

registration_id``stringRequired

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

Request

curl "https://api.machinemail.dev/auth/claim" \
  -H "Cookie: mm_session=$SESSION"

Response 200

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

http

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.

FieldDescription

user_code``stringRequired

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)

FieldDescription

agent_id``stringRequired

ID of the registration (reg_...).

registration_id``stringRequired

Same value as agent_id.

registration_type``"anonymous" | "service_auth" | "identity_assertion"Required

status``"claimed"Required

name``string

Agent-described label. Untrusted.

client``string

Provider name for identity_assertion, otherwise agent (self-described) or anonymous agent.

organization_id``stringRequired

The organization of the signed-in user.

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.

inbox``object

The former sandbox inbox, now permanent and in the user’s default pod.

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

Request

curl -X POST "https://api.machinemail.dev/auth/claim" \
  -H "Cookie: mm_session=$SESSION" \
  -H "Content-Type: application/json" \
  -d '{
  "user_code": "string"
}'

Response 200

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

http

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)

FieldDescription

workos``booleanRequired

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``booleanRequired

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``booleanRequired

Magic-link sign-in (POST /auth/magic-link) is available.

password``booleanRequired

The deprecated password endpoints are available.

authkit``booleanRequiredDeprecated

Deprecated: always false. Kept for older clients; see email_code.

passkey``booleanRequired

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

Request

curl "https://api.machinemail.dev/auth/config"

Response 200

{
  "workos": false,
  "email_code": false,
  "providers": ["google"],
  "magic_link": false,
  "password": false,
  "authkit": false,
  "passkey": false
}

Turn Email Sign-in Codes On or Off

http

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.

FieldDescription

enabled``booleanRequired

false turns email sign-in codes off; true back on.

Response

200 The setting (application/json)

FieldDescription

email_codes``booleanRequired

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

Request

curl -X PUT "https://api.machinemail.dev/auth/email-codes" \
  -H "Cookie: mm_session=$SESSION" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": true
}'

Response 200

{
  "email_codes": false
}

Send Sign-in Code

http

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.

FieldDescription

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:

FieldDescription

Set-Cookie``string

mm_signin=; HttpOnly; SameSite=Lax; Path=/auth (the sign-in in progress, 10 minutes), and mm_session once signed in.

FieldDescription

ok``trueRequired

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

Request

curl -X POST "https://api.machinemail.dev/auth/email/start" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "agent@machinemail.dev"
}'

Response 202

{
  "ok": true,
  "redirect_to": "string"
}

Verify Sign-in Code

http

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.

FieldDescription

code``stringRequired

The 6-digit code from the email. Spaces and dashes are ignored.

Response

200 Signed in, or the next step (application/json)

Headers:

FieldDescription

Set-Cookie``string

mm_signin=; HttpOnly; SameSite=Lax; Path=/auth (the sign-in in progress, 10 minutes), and mm_session once signed in.

FieldDescription

ok``trueRequired

signed_in``booleanRequired

true: the mm_session cookie is set. false: another step comes first (organization picker, email verification, SSO).

redirect_to``stringRequired

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

Request

curl -X POST "https://api.machinemail.dev/auth/email/verify" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "string"
}'

Response 200

{
  "ok": true,
  "signed_in": false,
  "redirect_to": "string"
}

Get First API Key

http

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)

FieldDescription

api_key OneTimeApiKey Required

An API key with its plaintext secret, returned once.

api_key is a OneTimeApiKey:

FieldDescription

api_key_id``stringRequired

ID of the API key.

api_key``stringRequired

Plaintext secret (mm_live_...). Shown once.

prefix``stringRequired

First characters of the key, shown in listings.

name``stringRequired

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

Request

curl "https://api.machinemail.dev/auth/first-key" \
  -H "Cookie: mm_session=$SESSION"

Response 200

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

http

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

FieldDescription

token``stringRequired

The token from the invitation link (/invite/{token}).

Response

200 The invitation (application/json)

FieldDescription

organization_name``stringRequired

The organization the invitation is to.

email``stringRequired

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

Request

curl "https://api.machinemail.dev/auth/invitations/string"

Response 200

{
  "organization_name": "string",
  "email": "agent@machinemail.dev",
  "state": "pending",
  "expires_at": "2026-10-01T12:00:00.000Z"
}

Log In (Deprecated)

http

POST /auth/login

Warning

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.

FieldDescription

email``stringRequired

password``stringRequired

Response

200 Signed in (application/json)

Headers:

FieldDescription

Set-Cookie``string

mm_session=; HttpOnly; SameSite=Lax; Path=/ (plus Secure on https).

FieldDescription

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:

FieldDescription

user_id``stringRequired

ID of the user.

email``stringRequired

Email address the user signs in with.

name``string | null

Display name, when set.

organization_id``stringRequired

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:

FieldDescription

organization_id``stringRequired

ID of the organization.

name``stringRequired

Name of the organization.

slug``stringRequired

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``integerRequired

Number of inboxes in the credential scope.

domain_count``integerRequired

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``booleanRequired

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 | nullRequired

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

Request

curl -X POST "https://api.machinemail.dev/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "agent@machinemail.dev",
  "password": "string"
}'

Response 200

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

http

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)

FieldDescription

ok``trueRequired

Errors

Status Meaning
500 Unexpected server error.

Example

Request

curl -X POST "https://api.machinemail.dev/auth/logout"

Response 200

{
  "ok": true
}

Redeem Magic Link

http

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

FieldDescription

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= (with &invited=1 for an invitation) when the address’s mailbox already has an account.

Headers:

FieldDescription

Location``string

Where to go next.

Set-Cookie``string

mm_session=; HttpOnly; SameSite=Lax; Path=/ (plus Secure on https).

Errors

Status Meaning
400 Invalid request: error.type is validation and error.param names the field.
500 Unexpected server error.

Example

Request

curl "https://api.machinemail.dev/auth/magic"

Request Magic Link

http

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.

FieldDescription

email``stringRequired

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``stringDeprecated

Alias of org_name.

Response

202 Accepted. The link was sent if the address can receive mail. (application/json)

FieldDescription

ok``trueRequired

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

Request

curl -X POST "https://api.machinemail.dev/auth/magic-link" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "agent@machinemail.dev"
}'

Response 202

{
  "ok": true
}

Current User

http

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)

FieldDescription

user User Required

A person who signs in to the dashboard.

organization Organization Required

user is a User:

FieldDescription

user_id``stringRequired

ID of the user.

email``stringRequired

Email address the user signs in with.

name``string | null

Display name, when set.

organization_id``stringRequired

ID of the organization this record is about: the session’s active organization, or the organization listed.

role``"owner" | "admin" | "member"Required

created_at``string (date-time)Required

When the user was created.

organization is a Organization:

FieldDescription

organization_id``stringRequired

ID of the organization.

name``stringRequired

Name of the organization.

slug``stringRequired

plan``"free" | "pro" | "team" | "scale"Required

Plan of the organization.

inbox_count``integerRequired

Number of inboxes in the credential scope.

domain_count``integerRequired

Number of domains in the organization.

domain_limit``integer

usage``object

Usage. Always present on GET /v0/organizations and PATCH /v0/organizations.

shared_domain``string

undo_send``booleanRequired

default_inbox_id``string | nullRequired

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
401 Missing, invalid, expired or revoked credential.
500 Unexpected server error.

Error bodies use the error envelope.

Example

Request

curl "https://api.machinemail.dev/auth/me" \
  -H "Cookie: mm_session=$SESSION"

Response 200

Preview App Authorization

http

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

FieldDescription

request_id``stringRequired

The request parameter of the /connect page URL.

Response

200 The pending request (application/json)

FieldDescription

request_id``stringRequired

client OAuthClientInfo Required

redirect_uri``stringRequired

Where the response goes.

redirect_host``stringRequired

Host (or scheme, for an app link) of redirect_uri, for the human to check.

redirect_loopback``booleanRequired

The response goes to a program on this computer (http://127.0.0.1, localhost).

resource``stringRequired

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:

FieldDescription

client_id``stringRequired

client_name``string | nullRequired

Self-reported by the client.

client_uri``string | nullRequired

Self-reported; https only.

logo_uri``string | nullRequired

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 | nullRequired

metadata_document: the host that serves the document (for example claude.ai).

Each item of scopes is a ScopeDescription:

FieldDescription

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``stringRequired

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

Request

curl "https://api.machinemail.dev/auth/oauth/authorize?request_id=string" \
  -H "Cookie: mm_session=$SESSION"

Response 200

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

http

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.

FieldDescription

request_id``stringRequired

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)

FieldDescription

status``"approved" | "denied"Required

redirect_to``stringRequired

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

Request

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

Response 200

{
  "status": "approved",
  "redirect_to": "string"
}

Deny App Authorization

http

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.

FieldDescription

request_id``stringRequired

Response

200 Denied (application/json)

FieldDescription

status``"approved" | "denied"Required

redirect_to``stringRequired

Send the browser here: redirect_uri with code (or error=access_denied), state and iss.

Errors

Error bodies use the error envelope.

Example

Request

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

Response 200

{
  "status": "approved",
  "redirect_to": "string"
}

List My Organizations

http

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)

FieldDescription

organizations MembershipOrganization[] Required

Every organization the person belongs to, by name.

Each item of organizations is a MembershipOrganization:

FieldDescription

organization_id``stringRequired

ID of the organization.

name``stringRequired

Name of the organization.

slug``stringRequired

URL-safe name of the organization. Fixed when the organization is created; it never changes.

role``"owner" | "admin" | "member"Required

current``booleanRequired

The session is signed in to this organization.

Errors

Status Meaning
401 Missing, invalid, expired or revoked credential.
500 Unexpected server error.

Error bodies use the error envelope.

Example

Request

curl "https://api.machinemail.dev/auth/organizations" \
  -H "Cookie: mm_session=$SESSION"

Response 200

{
  "organizations": [\
    {\
      "organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",\
      "name": "Production",\
      "slug": "string",\
      "role": "owner",\
      "current": false\
    }\
  ]
}

Create Organization

http

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.

FieldDescription

name``stringRequired

Name of the new organization.

Response

201 Created; the session is on the new organization (application/json)

FieldDescription

user User Required

A person who signs in to the dashboard.

organization Organization Required

user is a User:

FieldDescription

user_id``stringRequired

ID of the user.

email``stringRequired

Email address the user signs in with.

name``string | null

Display name, when set.

organization_id``stringRequired

ID of the organization this record is about: the session’s active organization, or the organization listed.

role``"owner" | "admin" | "member"Required

created_at``string (date-time)Required

When the user was created.

organization is a Organization:

FieldDescription

organization_id``stringRequired

ID of the organization.

name``stringRequired

Name of the organization.

slug``stringRequired

plan``"free" | "pro" | "team" | "scale"Required

Plan of the organization.

inbox_count``integerRequired

Number of inboxes in the credential scope.

domain_count``integerRequired

Number of domains in the organization.

domain_limit``integer

usage``object

Usage. Always present on GET /v0/organizations and PATCH /v0/organizations.

shared_domain``string

undo_send``booleanRequired

default_inbox_id``string | nullRequired

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

Request

curl -X POST "https://api.machinemail.dev/auth/organizations" \
  -H "Cookie: mm_session=$SESSION" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Production"
}'

Response 201

Switch Organization

http

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.

FieldDescription

organization_id``stringRequired

ID of an organization the person belongs to.

Response

200 The session, on the organization (application/json)

FieldDescription

user User Required

A person who signs in to the dashboard.

organization Organization Required

user is a User:

FieldDescription

user_id``stringRequired

ID of the user.

email``stringRequired

Email address the user signs in with.

name``string | null

Display name, when set.

organization_id``stringRequired

ID of the organization this record is about: the session’s active organization, or the organization listed.

role``"owner" | "admin" | "member"Required

created_at``string (date-time)Required

When the user was created.

organization is a Organization:

FieldDescription

organization_id``stringRequired

ID of the organization.

name``stringRequired

Name of the organization.

slug``stringRequired

plan``"free" | "pro" | "team" | "scale"Required

Plan of the organization.

inbox_count``integerRequired

Number of inboxes in the credential scope.

domain_count``integerRequired

Number of domains in the organization.

domain_limit``integer

usage``object

Usage. Always present on GET /v0/organizations and PATCH /v0/organizations.

shared_domain``string

undo_send``booleanRequired

default_inbox_id``string | nullRequired

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

Request

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

Response 200

Start Passkey Sign-in

http

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.

FieldDescription

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:

FieldDescription

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

FieldDescription

options``Record<string, unknown>Required

PublicKeyCredentialRequestOptionsJSON: pass to navigator.credentials.get().

Errors

Example

Request

curl -X POST "https://api.machinemail.dev/auth/passkey/options" \
  -H "Content-Type: application/json" \
  -d '{}'

Response 200

{
  "options": {
    "key": "value"
  }
}

Sign In with a Passkey

http

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.

FieldDescription

credential``Record<string, unknown>Required

The browser’s AuthenticationResponseJSON.

Response

200 Signed in, or the next step (application/json)

FieldDescription

ok``trueRequired

signed_in``booleanRequired

true: the mm_session cookie is set. false: another step comes first (organization picker, email verification, SSO).

redirect_to``stringRequired

Errors

Example

Request

curl -X POST "https://api.machinemail.dev/auth/passkey/verify" \
  -H "Content-Type: application/json" \
  -d '{
  "credential": {
    "key": "value"
  }
}'

Response 200

{
  "ok": true,
  "signed_in": false,
  "redirect_to": "string"
}

List My Passkeys

http

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)

FieldDescription

passkeys Passkey[] Required

The person’s passkeys, oldest first.

email_codes``booleanRequired

Email sign-in codes work for this account. false once turned off (with at least one passkey).

step_up``objectRequired

Confirmation before sensitive changes.

prompt``booleanRequired

Offer to create a passkey (no passkey yet, and the offer was not snoozed or declined).

Each item of passkeys is a Passkey:

FieldDescription

passkey_id``stringRequired

ID of the passkey (pk_…).

name``stringRequired

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

When it last signed in or confirmed a change.

backed_up``booleanRequired

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

Request

curl "https://api.machinemail.dev/auth/passkeys" \
  -H "Cookie: mm_session=$SESSION"

Response 200

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

http

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.

FieldDescription

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)

FieldDescription

passkey_id``stringRequired

ID of the passkey (pk_…).

name``stringRequired

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

When it last signed in or confirmed a change.

backed_up``booleanRequired

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

Request

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

Response 201

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

http

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.

FieldDescription

action``"not_now" | "never"Required

not_now: ask again in 14 days. never: don’t ask again.

Response

200 Saved (application/json)

FieldDescription

ok``trueRequired

Errors

Error bodies use the error envelope.

Example

Request

curl -X POST "https://api.machinemail.dev/auth/passkeys/prompt" \
  -H "Cookie: mm_session=$SESSION" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "not_now"
}'

Response 200

{
  "ok": true
}

Start Adding a Passkey

http

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)

FieldDescription

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

Request

curl -X POST "https://api.machinemail.dev/auth/passkeys/registration" \
  -H "Cookie: mm_session=$SESSION"

Response 200

{
  "options": {
    "key": "value"
  }
}

Rename a Passkey

http

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

FieldDescription

passkey_id``stringRequired

ID of the passkey (pk_…).

Request body

Content type: application/json.

FieldDescription

name``stringRequired

New name, 1 to 64 characters.

Response

200 Renamed (application/json)

FieldDescription

passkey_id``stringRequired

ID of the passkey (pk_…).

name``stringRequired

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

When it last signed in or confirmed a change.

backed_up``booleanRequired

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

Request

curl -X PATCH "https://api.machinemail.dev/auth/passkeys/string" \
  -H "Cookie: mm_session=$SESSION" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Production"
}'

Response 200

Remove a Passkey

http

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

FieldDescription

passkey_id``stringRequired

ID of the passkey (pk_…).

Response

200 Removed (application/json)

FieldDescription

ok``trueRequired

sessions_ended``integerRequired

Sessions the passkey had signed in that were signed out.

signed_out``booleanRequired

This session was one of them: sign in again.

Errors

Error bodies use the error envelope.

Example

Request

curl -X DELETE "https://api.machinemail.dev/auth/passkeys/string" \
  -H "Cookie: mm_session=$SESSION"

Response 200

{
  "ok": true,
  "sessions_ended": 1,
  "signed_out": false
}

Get Sign-in in Progress

http

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)

FieldDescription

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``booleanRequired

Signing in accepts an invitation.

Errors

Status Meaning
404 Not found. Also returned for resources outside the credential scope.
500 Unexpected server error.

Example

Request

curl "https://api.machinemail.dev/auth/pending"

Response 200

{
  "step": "code",
  "email": "agent@machinemail.dev",
  "organizations": [\
    {\
      "id": "string",\
      "name": "Production"\
    }\
  ],
  "invitation": false
}

Choose Organization

http

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.

FieldDescription

organization_id``stringRequired

One of the organizations from GET /auth/pending.

Response

200 Signed in, or the next step (application/json)

Headers:

FieldDescription

Set-Cookie``string

mm_signin=; HttpOnly; SameSite=Lax; Path=/auth (the sign-in in progress, 10 minutes), and mm_session once signed in.

FieldDescription

ok``trueRequired

signed_in``booleanRequired

true: the mm_session cookie is set. false: another step comes first (organization picker, email verification, SSO).

redirect_to``stringRequired

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

Request

curl -X POST "https://api.machinemail.dev/auth/pending/organization" \
  -H "Content-Type: application/json" \
  -d '{
  "organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e"
}'

Response 200

{
  "ok": true,
  "signed_in": false,
  "redirect_to": "string"
}

Verify Email

http

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.

FieldDescription

code``stringRequired

The 6-digit code from the email. Spaces and dashes are ignored.

Response

200 Signed in, or the next step (application/json)

Headers:

FieldDescription

Set-Cookie``string

mm_signin=; HttpOnly; SameSite=Lax; Path=/auth (the sign-in in progress, 10 minutes), and mm_session once signed in.

FieldDescription

ok``trueRequired

signed_in``booleanRequired

true: the mm_session cookie is set. false: another step comes first (organization picker, email verification, SSO).

redirect_to``stringRequired

Errors

Example

Request

curl -X POST "https://api.machinemail.dev/auth/pending/verify" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "string"
}'

Response 200

{
  "ok": true,
  "signed_in": false,
  "redirect_to": "string"
}

Sign Out Other Sessions

http

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)

FieldDescription

ok``trueRequired

ended``integerRequired

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

Request

curl -X POST "https://api.machinemail.dev/auth/sessions/revoke-others" \
  -H "Cookie: mm_session=$SESSION"

Response 200

{
  "ok": true,
  "ended": 1
}

Sign Up (Deprecated)

http

POST /auth/signup

Warning

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.

FieldDescription

email``stringRequired

password``stringRequired

At least 8 characters.

name``string

Display name of the user.

org_name``string

Organization name.

organization_name``string

Alias of org_name.

organizationName``stringDeprecated

Alias of org_name.

Response

201 Signed up and signed in (application/json)

Headers:

FieldDescription

Set-Cookie``string

mm_session=; HttpOnly; SameSite=Lax; Path=/ (plus Secure on https).

FieldDescription

user User Required

A person who signs in to the dashboard.

organization Organization Required

api_key OneTimeApiKey Required

An API key with its plaintext secret, returned once.

user is a User:

FieldDescription

user_id``stringRequired

ID of the user.

email``stringRequired

Email address the user signs in with.

name``string | null

Display name, when set.

organization_id``stringRequired

ID of the organization this record is about: the session’s active organization, or the organization listed.

role``"owner" | "admin" | "member"Required

created_at``string (date-time)Required

When the user was created.

organization is a Organization:

FieldDescription

organization_id``stringRequired

ID of the organization.

name``stringRequired

Name of the organization.

slug``stringRequired

plan``"free" | "pro" | "team" | "scale"Required

Plan of the organization.

inbox_count``integerRequired

Number of inboxes in the credential scope.

domain_count``integerRequired

Number of domains in the organization.

domain_limit``integer

usage``object

Usage. Always present on GET /v0/organizations and PATCH /v0/organizations.

shared_domain``string

undo_send``booleanRequired

default_inbox_id``string | nullRequired

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.

api_key is a OneTimeApiKey:

FieldDescription

api_key_id``stringRequired

ID of the API key.

api_key``stringRequired

Plaintext secret (mm_live_...). Shown once.

prefix``stringRequired

First characters of the key, shown in listings.

name``stringRequired

Name of the key.

created_at``string (date-time)Required

When the key was created.

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

Request

curl -X POST "https://api.machinemail.dev/auth/signup" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "agent@machinemail.dev",
  "password": "string",
  "name": "Production"
}'

Response 201

Sign In with a Social Provider

http

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

FieldDescription

provider``"google" | "github" | "apple" | "microsoft"Required

A social sign-in provider.

Query parameters

FieldDescription

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:

FieldDescription

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

Request

curl "https://api.machinemail.dev/auth/social/google"

Sign In to an Organization with SSO

http

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

FieldDescription

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:

FieldDescription

Location``string

Where to go next.

Errors

Error bodies use the error envelope.

Example

Request

curl "https://api.machinemail.dev/auth/sso" \
  -H "Cookie: mm_session=$SESSION"

Email a Confirmation Code

http

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)

FieldDescription

ok``trueRequired

email``stringRequired

Where the code went, masked.

expires_at``string (date-time)Required

When the code expires (10 minutes).

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).
404 Not found. Also returned for resources outside the credential scope.
409 Conflicts with an existing resource.
429 Rate limited.
500 Unexpected server error.

Error bodies use the error envelope.

Example

Request

curl -X POST "https://api.machinemail.dev/auth/step-up/email" \
  -H "Cookie: mm_session=$SESSION"

Response 202

{
  "ok": true,
  "email": "agent@machinemail.dev",
  "expires_at": "2026-10-01T12:00:00.000Z"
}

Confirm with an Emailed Code

http

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.

FieldDescription

code``stringRequired

The 6-digit code from the email. Spaces and dashes are ignored.

Response

200 Confirmed (application/json)

FieldDescription

ok``trueRequired

recent_until``string (date-time)Required

Until when sensitive changes need no further confirmation.

Errors

Error bodies use the error envelope.

Example

Request

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

Response 200

{
  "ok": true,
  "recent_until": "2026-10-01T12:00:00.000Z"
}

Start Confirming with a Passkey

http

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)

FieldDescription

options``Record<string, unknown>Required

PublicKeyCredentialRequestOptionsJSON: pass to navigator.credentials.get().

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

Request

curl -X POST "https://api.machinemail.dev/auth/step-up/passkey/options" \
  -H "Cookie: mm_session=$SESSION"

Response 200

{
  "options": {
    "key": "value"
  }
}

Confirm with a Passkey

http

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.

FieldDescription

credential``Record<string, unknown>Required

The browser’s AuthenticationResponseJSON.

Response

200 Confirmed (application/json)

FieldDescription

ok``trueRequired

recent_until``string (date-time)Required

Until when sensitive changes need no further confirmation.

Errors

Error bodies use the error envelope.

Example

Request

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

Response 200

{
  "ok": true,
  "recent_until": "2026-10-01T12:00:00.000Z"
}

On this page

Legacy Sign-In Redirect (Deprecated) Sign-in Callback Preview Claim Complete Claim Sign-in Options Turn Email Sign-in Codes On or Off Send Sign-in Code Verify Sign-in Code Get First API Key Preview Invitation Log In (Deprecated) Log Out Redeem Magic Link Request Magic Link Current User Preview App Authorization Approve App Authorization Deny App Authorization List My Organizations Create Organization Switch Organization Start Passkey Sign-in Sign In with a Passkey List My Passkeys Add a Passkey Answer the Passkey Offer Start Adding a Passkey Rename a Passkey Remove a Passkey Get Sign-in in Progress Choose Organization Verify Email Sign Out Other Sessions Sign Up (Deprecated) Sign In with a Social Provider Sign In to an Organization with SSO Email a Confirmation Code Confirm with an Emailed Code Start Confirming with a Passkey Confirm with a Passkey

100% readTop