CLI Auth · MachineMail Docs

Preview CLI Sign-In

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

http

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

Request

curl "https://api.machinemail.dev/auth/cli/device?user_code=string" \
  -H "Cookie: mm_session=$SESSION"

Response 200

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

http

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 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. 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 Description
status `"approved"
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

Request

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

Response 200

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

Start CLI Sign-In

http

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

Headings Description
Cache-Control string no-store
Field 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

Request

curl -X POST "https://api.machinemail.dev/auth/cli/device/code" \
  --data-urlencode "client_id=my-client-id"

Response 200

{
  "device_code": "string",
  "user_code": "string",
  "verification_uri": "https://example.com",
  "verification_uri_complete": "string",
  "expires_in": 1,
  "interval": 1
}

Deny CLI Sign-In

http

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.

Field Description
user_code string Required

The code the CLI printed (XXXX-XXXX); case, spaces and dashes are ignored.

Response

200 Denied (application/json)

Field Description
status `"approved"
key_name string Required

Name of the key the CLI receives (approved) or would have received (denied).

Errors

Error bodies use the error envelope.

Example

Request

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

Response 200

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

Poll CLI Sign-In

http

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

Headings Description
Cache-Control string no-store
Pragma string no-cache
Field 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 | user | User Required

... (additional fields omitted for brevity)

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

Request

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"

Response 200

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