---
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)](#legacy-sign-in-redirect-deprecated)       |
| `GET`    | `/auth/callback`                | [Sign-in Callback](#sign-in-callback)                                             |
| `GET`    | `/auth/claim`                   | [Preview Claim](#preview-claim)                                                   |
| `POST`   | `/auth/claim`                   | [Complete Claim](#complete-claim)                                                 |
| `GET`    | `/auth/config`                  | [Sign-in Options](#sign-in-options)                                               |
| `PUT`    | `/auth/email-codes`             | [Turn Email Sign-in Codes On or Off](#turn-email-sign-in-codes-on-or-off)         |
| `GET`    | `/auth/email-preferences`       | [Get My Email Preferences](#get-my-email-preferences)                             |
| `PUT`    | `/auth/email-preferences`       | [Turn Product Emails On or Off](#turn-product-emails-on-or-off)                   |
| `POST`   | `/auth/email/start`             | [Send Sign-in Code](#send-sign-in-code)                                           |
| `POST`   | `/auth/email/verify`            | [Verify Sign-in Code](#verify-sign-in-code)                                       |
| `GET`    | `/auth/first-key`               | [Get First API Key](#get-first-api-key)                                           |
| `GET`    | `/auth/invitations/{token}`     | [Preview Invitation](#preview-invitation)                                         |
| `POST`   | `/auth/login`                   | [Log In (Deprecated)](#log-in-deprecated)                                         |
| `POST`   | `/auth/logout`                  | [Log Out](#log-out)                                                               |
| `GET`    | `/auth/magic`                   | [Redeem Magic Link](#redeem-magic-link)                                           |
| `POST`   | `/auth/magic-link`              | [Request Magic Link](#request-magic-link)                                         |
| `GET`    | `/auth/me`                      | [Current User](#current-user)                                                     |
| `GET`    | `/auth/oauth/authorize`         | [Preview App Authorization](#preview-app-authorization)                           |
| `POST`   | `/auth/oauth/authorize/approve` | [Approve App Authorization](#approve-app-authorization)                           |
| `POST`   | `/auth/oauth/authorize/deny`    | [Deny App Authorization](#deny-app-authorization)                                 |
| `GET`    | `/auth/organizations`           | [List My Organizations](#list-my-organizations)                                   |
| `POST`   | `/auth/organizations`           | [Create Organization](#create-organization)                                       |
| `POST`   | `/auth/organizations/switch`    | [Switch Organization](#switch-organization)                                       |
| `POST`   | `/auth/passkey/options`         | [Start Passkey Sign-in](#start-passkey-sign-in)                                   |
| `POST`   | `/auth/passkey/verify`          | [Sign In with a Passkey](#sign-in-with-a-passkey)                                 |
| `GET`    | `/auth/passkeys`                | [List My Passkeys](#list-my-passkeys)                                             |
| `POST`   | `/auth/passkeys`                | [Add a Passkey](#add-a-passkey)                                                   |
| `POST`   | `/auth/passkeys/prompt`         | [Answer the Passkey Offer](#answer-the-passkey-offer)                             |
| `POST`   | `/auth/passkeys/registration`   | [Start Adding a Passkey](#start-adding-a-passkey)                                 |
| `PATCH`  | `/auth/passkeys/{passkey_id}`   | [Rename a Passkey](#rename-a-passkey)                                             |
| `DELETE` | `/auth/passkeys/{passkey_id}`   | [Remove a Passkey](#remove-a-passkey)                                             |
| `GET`    | `/auth/pending`                 | [Get Sign-in in Progress](#get-sign-in-in-progress)                               |
| `POST`   | `/auth/pending/organization`    | [Choose Organization](#choose-organization)                                       |
| `POST`   | `/auth/pending/verify`          | [Verify Email](#verify-email)                                                     |
| `POST`   | `/auth/sessions/revoke-others`  | [Sign Out Other Sessions](#sign-out-other-sessions)                               |
| `POST`   | `/auth/signup`                  | [Sign Up (Deprecated)](#sign-up-deprecated)                                       |
| `GET`    | `/auth/social/{provider}`       | [Sign In with a Social Provider](#sign-in-with-a-social-provider)                 |
| `GET`    | `/auth/sso`                     | [Sign In to an Organization with SSO](#sign-in-to-an-organization-with-sso)       |
| `POST`   | `/auth/step-up/email`           | [Email a Confirmation Code](#email-a-confirmation-code)                           |
| `POST`   | `/auth/step-up/email/verify`    | [Confirm with an Emailed Code](#confirm-with-an-emailed-code)                     |
| `POST`   | `/auth/step-up/passkey/options` | [Start Confirming with a Passkey](#start-confirming-with-a-passkey)               |
| `POST`   | `/auth/step-up/passkey/verify`  | [Confirm with a Passkey](#confirm-with-a-passkey)                                 |
| `GET`    | `/auth/unsubscribe`             | [Preview an Unsubscribe Link](#preview-an-unsubscribe-link)                       |
| `POST`   | `/auth/unsubscribe`             | [Unsubscribe or Resubscribe with a Link](#unsubscribe-or-resubscribe-with-a-link) |

## Legacy Sign-In Redirect (Deprecated)

```http
GET /auth/authkit
```

<Callout type="warn">Deprecated.</Callout>

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

```bash title="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

| 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=<token>; 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

```bash title="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

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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`.

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

| 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` | `"claimed"` | required |  |
| `name` | `string` |  | Agent-described label. Untrusted. |
| `client` | `string` |  | Provider name for identity_assertion, otherwise agent (self-described) or anonymous agent. |
| `organization_id` | `string` | required | 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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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`)

| 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

```bash title="Request"
curl "https://api.machinemail.dev/auth/config"
```

```json title="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](mailto: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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="Response 200"
{
  "email_codes": false
}
```

## Get My Email Preferences

```http
GET /auth/email-preferences
```

Whether the signed-in person gets product emails.

Product emails are the getting-started emails MachineMail sends to new accounts. Sign-in codes, invitations and account and security emails are not product emails and always arrive.

**Authentication:** the dashboard `mm_session` cookie.

#### Response

**200** The setting (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `product_emails` | `boolean` | required | The person gets getting-started emails. Sign-in codes, invitations and account and security emails always arrive. |
| `changed_at` | `string (date-time)` |  | When the setting last changed. Absent when it never did. |

#### Errors

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

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/auth/email-preferences" \
  -H "Cookie: mm_session=$SESSION"
```

```json title="Response 200"
{
  "product_emails": false,
  "changed_at": "2026-10-01T12:00:00.000Z"
}
```

## Turn Product Emails On or Off

```http
PUT /auth/email-preferences
```

Turns product emails off for the signed-in person, or back on.

Product emails are the getting-started emails MachineMail sends to new accounts. Sign-in codes, invitations and account and security emails are not product emails and always arrive.

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 |
| --- | --- | --- | --- |
| `product_emails` | `boolean` | required | false stops getting-started emails from MachineMail; true turns them back on. |

#### Response

**200** The setting (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `product_emails` | `boolean` | required | The person gets getting-started emails. Sign-in codes, invitations and account and security emails always arrive. |
| `changed_at` | `string (date-time)` |  | When the setting last changed. Absent when it never did. |

#### 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). |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X PUT "https://api.machinemail.dev/auth/email-preferences" \
  -H "Cookie: mm_session=$SESSION" \
  -H "Content-Type: application/json" \
  -d '{
  "product_emails": false
}'
```

```json title="Response 200"
{
  "product_emails": false,
  "changed_at": "2026-10-01T12:00:00.000Z"
}
```

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

| 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=<handle>; 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

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

```json title="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`.

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

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

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

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

```json title="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`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `api_key` | `OneTimeApiKey` | required | An API key with its plaintext secret, returned once. |

`api_key` is a [OneTimeApiKey](https://machinemail.dev/docs/reference/api/objects#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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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

| 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

```bash title="Request"
curl "https://api.machinemail.dev/auth/invitations/string"
```

```json title="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
```

<Callout type="warn">Deprecated.</Callout>

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=<token>; 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](https://machinemail.dev/docs/reference/api/objects#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](https://machinemail.dev/docs/reference/api/objects#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

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

```json title="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`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `ok` | `true` | required |  |

#### Errors

| Status | Meaning                  |
| ------ | ------------------------ |
| `500`  | Unexpected server error. |

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/auth/logout"
```

```json title="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

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

| Field | Type | | Description |
| --- | --- | --- | --- |
| `Location` | `string` |  | Where to go next. |
| `Set-Cookie` | `string` |  | mm_session=<token>; 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

```bash title="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`.

| 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

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

```json title="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`)

| 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](https://machinemail.dev/docs/reference/api/objects#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](https://machinemail.dev/docs/reference/api/objects#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                                          |
| ------ | ------------------------------------------------ |
| `401`  | Missing, invalid, expired or revoked credential. |
| `500`  | Unexpected server error.                         |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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"
  }
}
```

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

| 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 <issuer>/mcp) or <issuer>/ (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](https://machinemail.dev/docs/reference/api/objects#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](https://machinemail.dev/docs/reference/api/objects#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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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`.

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="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"
}'
```

```json title="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`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `request_id` | `string` | required |  |

#### Response

**200** Denied (`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.                           |
| `500`  | Unexpected server error.                                                   |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="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"
}'
```

```json title="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`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `organizations` | `MembershipOrganization[]` | required | Every organization the person belongs to, by name. |

Each item of `organizations` is a [MembershipOrganization](https://machinemail.dev/docs/reference/api/objects#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

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

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `name` | `string` | required | Name of the new organization. |

#### Response

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

| 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](https://machinemail.dev/docs/reference/api/objects#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](https://machinemail.dev/docs/reference/api/objects#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.                                                                                                                                           |
| `402`  | An account can own one Free organization, and this one already does (limit\_key: "free\_organizations"). Upgrade it, or contact [support@machinemail.dev](mailto: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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="Response 201"
{
  "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

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

| Field | Type | | Description |
| --- | --- | --- | --- |
| `organization_id` | `string` | required | ID of an organization the person belongs to. |

#### Response

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

| 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](https://machinemail.dev/docs/reference/api/objects#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](https://machinemail.dev/docs/reference/api/objects#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.                                                                           |
| `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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="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"
}'
```

```json title="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"
  }
}
```

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

| 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=<handle>; 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

| 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

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

```json title="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`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `credential` | `Record<string, unknown>` | required | The browser’s AuthenticationResponseJSON. |

#### Response

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

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

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

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

```json title="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`)

| 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](https://machinemail.dev/docs/reference/api/objects#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

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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`.

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

| 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

| 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.                                                                                                                                                                   |
| `409`  | Conflicts with an existing resource.                                                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="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"
}'
```

```json title="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`.

| 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

| 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.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="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"
}'
```

```json title="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).

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.

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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

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

| 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

| 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.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="Response 200"
{
  "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

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

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.

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

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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`)

| 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

```bash title="Request"
curl "https://api.machinemail.dev/auth/pending"
```

```json title="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`.

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

| Field | Type | | Description |
| --- | --- | --- | --- |
| `Set-Cookie` | `string` |  | mm_signin=<handle>; HttpOnly; SameSite=Lax; Path=/auth (the sign-in in progress, 10 minutes), and mm_session once signed in. |

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

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

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

```json title="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.

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

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

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

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

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

```json title="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`)

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="Response 200"
{
  "ok": true,
  "ended": 1
}
```

## Sign Up (Deprecated)

```http
POST /auth/signup
```

<Callout type="warn">Deprecated.</Callout>

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 |
| --- | --- | --- | --- |
| `Set-Cookie` | `string` |  | mm_session=<token>; 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. |
| `api_key` | `OneTimeApiKey` | required | An API key with its plaintext secret, returned once. |

`user` is a [User](https://machinemail.dev/docs/reference/api/objects#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](https://machinemail.dev/docs/reference/api/objects#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. |

`api_key` is a [OneTimeApiKey](https://machinemail.dev/docs/reference/api/objects#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                                                                                                                                                                                                                                                                                          |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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

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

```json title="Response 201"
{
  "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"
  },
  "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"
  }
}
```

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

| 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

```bash title="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

| 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

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

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="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`)

| 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

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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`.

| 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

| 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.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="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"
}'
```

```json title="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`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `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](https://machinemail.dev/docs/reference/errors).

#### Example

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

```json title="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`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `credential` | `Record<string, unknown>` | required | The browser’s AuthenticationResponseJSON. |

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

| 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.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="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"
  }
}'
```

```json title="Response 200"
{
  "ok": true,
  "recent_until": "2026-10-01T12:00:00.000Z"
}
```

## Preview an Unsubscribe Link

```http
GET /auth/unsubscribe
```

Who a product email’s unsubscribe link is for (the address, masked) and whether they get product emails now. Reading it changes nothing: unsubscribing takes `POST /auth/unsubscribe`, or the one-click request mail clients send.

The token is the one in the unsubscribe link of the email (`/unsubscribe/marketing?t=<token>`). An unknown or malformed token is `404`.

Rate limited per client IP: `429` with `Retry-After`.

**Authentication:** None.

#### Query parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `t` | `string` | required | The token from the unsubscribe link in the email (/unsubscribe/marketing?t=<token>). (min length 1, max length 200) |

#### Response

**200** Who the link is for (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `email` | `string` | required | The address the link was sent to, masked (a••@example.com). |
| `product_emails` | `boolean` | required | The address gets getting-started emails now. |

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

```bash title="Request"
curl "https://api.machinemail.dev/auth/unsubscribe?t=string"
```

```json title="Response 200"
{
  "email": "agent@machinemail.dev",
  "product_emails": false
}
```

## Unsubscribe or Resubscribe with a Link

```http
POST /auth/unsubscribe
```

Turns product emails off (`product_emails: false`) or back on for the person an unsubscribe link was sent to, without signing in.

Product emails are the getting-started emails MachineMail sends to new accounts. Sign-in codes, invitations and account and security emails are not product emails and always arrive.

The token is the one in the unsubscribe link of the email (`/unsubscribe/marketing?t=<token>`). An unknown or malformed token is `404`.

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 |
| --- | --- | --- | --- |
| `token` | `string` | required | The token from the unsubscribe link in the email (/unsubscribe/marketing?t=<token>). (min length 1, max length 200) |
| `product_emails` | `boolean` | required | false unsubscribes the address the link was sent to; true resubscribes it. |

#### Response

**200** The setting now (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `email` | `string` | required | The address the link was sent to, masked (a••@example.com). |
| `product_emails` | `boolean` | required | The address gets getting-started emails now. |

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

```bash title="Request"
curl -X POST "https://api.machinemail.dev/auth/unsubscribe" \
  -H "Content-Type: application/json" \
  -d '{
  "token": "string",
  "product_emails": false
}'
```

```json title="Response 200"
{
  "email": "agent@machinemail.dev",
  "product_emails": false
}
```
