cli auth.md


title: "CLI Auth" description: "The device authorization endpoints (RFC 8628) behind CLI sign-in: start a sign-in, approve or deny it in the browser, and poll until the CLI receives its key." canonical: "https://machinemail.dev/docs/reference/api/cli-auth"

CLI Auth

The device authorization endpoints (RFC 8628) behind CLI sign-in: start a sign-in, approve or deny it in the browser, and poll until the CLI receives its key.

Source: https://machinemail.dev/docs/reference/api/cli-auth

Method Path Summary
GET /auth/cli/device Preview CLI Sign-In
POST /auth/cli/device/approve Approve CLI Sign-In
POST /auth/cli/device/code Start CLI Sign-In
POST /auth/cli/device/deny Deny CLI Sign-In
POST /auth/cli/device/token Poll CLI Sign-In

Preview CLI Sign-In

GET /auth/cli/device

Called by the dashboard /cli page for a signed-in human before approving. Returns what the CLI reported (client, hostname; self-reported) and the key approval would create; never the device code. 404 for an unknown, decided or expired code. Dashboard session only: API keys are not accepted. Every call counts against a per-user and per-IP limit on code lookups (brute-force brake): 429 with Retry-After.

Authentication: the dashboard mm_session cookie.

Query parameters

Field Type Description
user_code string required The code the CLI printed (XXXX-XXXX); case, spaces and dashes are ignored.

Response

200 The pending sign-in (application/json)

Field Type Description
user_code string required The code, formatted XXXX-XXXX.
device_id string required Opaque ID of this sign-in (cld_...), not a secret: the device_id of the session.cli.device stream events that report it approved, denied or redeemed.
client_id string required Always machinemail-cli.
client_name string | null required What the CLI says it is. Self-reported.
hostname string | null required The machine the CLI says it runs on. Self-reported.
key_name string required Name of the key approval creates: CLI on .
permissions string[] required Permissions of that key: ["*"] (every permission except inbound_raw).
created_at string (date-time) required When the CLI asked for the code.
expires_at string (date-time) required When the code expires.

Errors

Status Meaning
400 Invalid request: error.type is validation and error.param names the field.
401 Missing, invalid, expired or revoked credential.
429 Rate limited.
500 Unexpected server error.

Error bodies use the error envelope.

Example

curl "https://api.machinemail.dev/auth/cli/device?user_code=string" \
  -H "Cookie: mm_session=$SESSION"
{
  "user_code": "string",
  "device_id": "string",
  "client_id": "my-client-id",
  "client_name": "string",
  "hostname": "string",
  "key_name": "string",
  "permissions": ["string"],
  "created_at": "2026-10-01T12:00:00.000Z",
  "expires_at": "2026-10-01T12:00:00.000Z"
}

Approve CLI Sign-In

POST /auth/cli/device/approve

Approves a pending code for the signed-in user. The CLI's next poll receives a new API key named CLI on <hostname> in the organization the session is signed in to (every permission except inbound_raw), listed under API keys and revocable there. Dashboard session only. Every call counts against a per-user and per-IP limit on code lookups (brute-force brake): 429 with Retry-After.

A person in several organizations creates the key in 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.

Authentication: the dashboard mm_session cookie.

Request body

Content type: application/json.

Field Type Description
user_code string required The code the CLI printed (XXXX-XXXX); case, spaces and dashes are ignored.
organization_id string The organization the key is for, as the 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 create the key in 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
key_name string required Name of the key the CLI receives (approved) or would have received (denied).

Errors

Status Meaning
400 Invalid request: error.type is validation and error.param names the field.
401 Missing, invalid, expired or revoked credential.
409 Conflicts with an existing resource.
429 Rate limited.
500 Unexpected server error.

Error bodies use the error envelope.

Example

curl -X POST "https://api.machinemail.dev/auth/cli/device/approve" \
  -H "Cookie: mm_session=$SESSION" \
  -H "Content-Type: application/json" \
  -d '{
  "user_code": "string"
}'
{
  "status": "approved",
  "key_name": "string"
}

Start CLI Sign-In

POST /auth/cli/device/code

RFC 8628 device authorization request, made by mm login. Returns a device_code for the CLI to poll with and a user_code (XXXX-XXXX) for the human to approve at verification_uri (https://machinemail.dev/cli) while signed in to the dashboard. Both expire after 10 minutes.

Form-encoded or JSON body. client_id must be machinemail-cli (401 invalid_client otherwise). Rate limited per client IP: 429 rate_limited with Retry-After.

Authentication: None.

Request body

Content type: application/x-www-form-urlencoded or application/json. Required.

Field Type Description
client_id string required Always machinemail-cli.
client_name string Shown to the human on the approval page, for example MachineMail CLI 0.4.0. At most 100 characters. (max length 100)
hostname string The machine the CLI runs on. Names the key (CLI on ) and is shown on the approval page. At most 253 characters. (max length 253)

Response

200 Codes issued (application/json)

Headers:

Field Type Description
Cache-Control string no-store
Field Type Description
device_code string required Secret the CLI polls the token endpoint with. Never show it.
user_code string required Code the human checks on the approval page: 8 letters as XXXX-XXXX (consonants only, no look-alikes).
verification_uri string required https://machinemail.dev/cli: where the human approves.
verification_uri_complete string required verification_uri with ?code=, for opening in a browser.
expires_in integer required Seconds until both codes expire (600).
interval integer required Minimum seconds between polls of the token endpoint (5).

Errors

Status Meaning
400 OAuth error invalid_request.
401 OAuth error invalid_client.
429 OAuth error rate_limited.
500 Unexpected server error.

Example

curl -X POST "https://api.machinemail.dev/auth/cli/device/code" \
  --data-urlencode "client_id=my-client-id"
{
  "device_code": "string",
  "user_code": "string",
  "verification_uri": "https://example.com",
  "verification_uri_complete": "string",
  "expires_in": 1,
  "interval": 1
}

Deny CLI Sign-In

POST /auth/cli/device/deny

Denies a pending code: the CLI's next poll gets access_denied and no key is created. Dashboard session only. Every call counts against a per-user and per-IP limit on code lookups (brute-force brake): 429 with Retry-After.

Authentication: the dashboard mm_session cookie.

Request body

Content type: application/json.

Response

200 Denied (application/json)

Errors

Error bodies use the error envelope.

Example

curl -X POST "https://api.machinemail.dev/auth/cli/device/deny" \
  -H "Cookie: mm_session=$SESSION" \
  -H "Content-Type: application/json" \
  -d '{
  "user_code": "string"
}'
{
  "status": "approved",
  "key_name": "string"
}

Poll CLI Sign-In

POST /auth/cli/device/token

RFC 8628 token request with grant_type=urn:ietf:params:oauth:grant-type:device_code. Poll no faster than interval seconds until the human decides.

After approval the next poll returns a new organization API key (CLI on <hostname>, every permission except inbound_raw, attributed to the approving user) as access_token, once: the device code is then spent.

Errors (RFC 6749 section 5.2 bodies, status 400): authorization_pending (keep polling), slow_down (polled faster than the interval; add 5 seconds), expired_token (start again), access_denied (denied in the browser), invalid_grant (unknown or already used device code), unsupported_grant_type, invalid_request.

Authentication: None.

Request body

Content type: application/x-www-form-urlencoded or application/json. Required.

Field Type Description
grant_type string required urn:ietf:params:oauth:grant-type:device_code.
device_code string required The device_code from the device authorization response.
client_id string required Always machinemail-cli.

Response

200 The new API key, returned once (application/json)

Headers:

Field Type Description
Cache-Control string no-store
Pragma string no-cache
Field Type Description
access_token string required The new API key (mm_live_...). Returned once; send it as Authorization: Bearer.
token_type "Bearer" required
api_key_id string required ID of the key, for DELETE /v0/api-keys/{api_key_id} on logout.
key_name string required Name of the key: CLI on .
organization Organization required The organization the key belongs to.
user User required The person who approved the sign-in.

organization is a Organization:

Field Type Description
organization_id string required ID of the organization.
name string required Name of the organization.
slug string required URL-safe name of the organization, chosen from its name when it is created. It never changes, not even when the organization is renamed. {slug}@ on the shared domain is reserved for this organization: no other organization can create that inbox.
plan "free" | "pro" | "team" | "scale" required Plan of the organization.
inbox_count integer required Number of inboxes in the credential scope.
domain_count integer required Number of domains in the organization.
domain_limit integer Custom domains included in the plan. The Free plan cannot add more; paid plans can, and each extra domain is billed. Always present on GET /v0/organizations and PATCH /v0/organizations.
usage object Usage. Always present on GET /v0/organizations and PATCH /v0/organizations.
shared_domain string Domain every organization can create inboxes on, used when an inbox is created without domain. Always present on GET /v0/organizations and PATCH /v0/organizations.
undo_send boolean required Undo send in the dashboard: when true (the default), a message sent from the MachineMail dashboard waits 5 seconds before it goes out, so it can be undone. Only the dashboard reads it: the API, the MCP server and the CLI always send at once.
default_inbox_id string | null required The default inbox of the organization (its inbox_id). A new organization gets {slug}@ on the shared domain, named after the organization, as its default inbox; when that address cannot be used (a reserved name), it starts without one. The dashboard lists the default inbox first. Change it with PATCH /v0/organizations. null when there is none: it was cleared, its inbox was deleted, or (for a pod or inbox credential) it is outside the credential scope.
updated_at string (date-time) required Time at which the organization was last updated.
created_at string (date-time) required Time at which the organization was created.

user is a User:

Field Type Description
user_id string required ID of the user.
email string required Email address the user signs in with.
name string | null Display name, when set.
organization_id string required ID of the organization this record is about: the session’s active organization, or the organization listed.
role "owner" | "admin" | "member" required Role in the organization. Owners and admins invite, change roles and remove members; only owners grant or remove the owner role, and an organization always keeps one owner.
created_at string (date-time) required When the user was created.

Errors

Status Meaning
400 OAuth error: authorization_pending, slow_down, expired_token, access_denied, invalid_grant, unsupported_grant_type, invalid_request.
500 Unexpected server error.

Example

curl -X POST "https://api.machinemail.dev/auth/cli/device/token" \
  --data-urlencode "grant_type=string" \
  --data-urlencode "device_code=string" \
  --data-urlencode "client_id=my-client-id"
{
  "access_token": "string",
  "token_type": "Bearer",
  "api_key_id": "key_01k6x3e8mr0000h4r0ty6c9k1n",
  "key_name": "string",
  "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"
  },
  "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"
  }
}