Skip to content
machinemail

CLI Auth

Signing the CLI in with the browser (RFC 8628 device authorization grant).

MethodPathSummary
GET/auth/cli/devicePreview CLI Sign-In
POST/auth/cli/device/approveApprove CLI Sign-In
POST/auth/cli/device/codeStart CLI Sign-In
POST/auth/cli/device/denyDeny CLI Sign-In
POST/auth/cli/device/tokenPoll 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

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

Response

200 The pending sign-in (application/json)

user_codestringRequired
The code, formatted XXXX-XXXX.
device_idstringRequired
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_idstringRequired
Always machinemail-cli.
client_namestring | nullRequired
What the CLI says it is. Self-reported.
hostnamestring | nullRequired
The machine the CLI says it runs on. Self-reported.
key_namestringRequired
Name of the key approval creates: CLI on <hostname>.
permissionsstring[]Required
Permissions of that key: ["*"] (every permission except inbound_raw).
created_atstring (date-time)Required
When the CLI asked for the code.
expires_atstring (date-time)Required
When the code expires.

Errors

StatusMeaning
400Invalid request: error.type is validation and error.param names the field.
401Missing, invalid, expired or revoked credential.
429Rate limited.
500Unexpected 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 user’s organization (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.

Authentication: the dashboard mm_session cookie.

Request body

Content type: application/json.

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

Response

200 Approved (application/json)

status"approved" | "denied"Required
key_namestringRequired
Name of the key the CLI receives (approved) or would have received (denied).

Errors

StatusMeaning
400Invalid request: error.type is validation and error.param names the field.
401Missing, invalid, expired or revoked credential.
429Rate limited.
500Unexpected 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 (<PUBLIC_URL>/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.

client_idstringRequired
Always machinemail-cli.
client_namestring
Shown to the human on the approval page, for example MachineMail CLI 0.4.0. At most 100 characters. (max length 100)
hostnamestring
The machine the CLI runs on. Names the key (CLI on <hostname>) and is shown on the approval page. At most 253 characters. (max length 253)

Response

200 Codes issued (application/json)

Headers:

Cache-Controlstring
no-store
device_codestringRequired
Secret the CLI polls the token endpoint with. Never show it.
user_codestringRequired
Code the human checks on the approval page: 8 letters as XXXX-XXXX (consonants only, no look-alikes).
verification_uristringRequired
<PUBLIC_URL>/cli: where the human approves.
verification_uri_completestringRequired
verification_uri with ?code=<user_code>, for opening in a browser.
expires_inintegerRequired
Seconds until both codes expire (600).
intervalintegerRequired
Minimum seconds between polls of the token endpoint (5).

Errors

StatusMeaning
400OAuth error invalid_request.
401OAuth error invalid_client.
429OAuth error rate_limited.
500Unexpected 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.

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

Response

200 Denied (application/json)

status"approved" | "denied"Required
key_namestringRequired
Name of the key the CLI receives (approved) or would have received (denied).

Errors

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

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.

grant_typestringRequired
urn:ietf:params:oauth:grant-type:device_code.
device_codestringRequired
The device_code from the device authorization response.
client_idstringRequired
Always machinemail-cli.

Response

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

Headers:

Cache-Controlstring
no-store
Pragmastring
no-cache
access_tokenstringRequired
The new API key (mm_live_...). Returned once; send it as Authorization: Bearer.
token_type"Bearer"Required
api_key_idstringRequired
ID of the key, for DELETE /v0/api-keys/{api_key_id} on logout.
key_namestringRequired
Name of the key: CLI on <hostname>.
organizationOrganizationRequired
The organization the key belongs to.
userUserRequired
The person who approved the sign-in.

organization is a Organization:

organization_idstringRequired
ID of the organization.
namestringRequired
Name of the organization.
slugstringRequired
URL-safe name of the organization.
plan"free" | "pro" | "enterprise"Required
Plan of the organization.
inbox_countintegerRequired
Number of inboxes in the credential scope.
domain_countintegerRequired
Number of domains in the organization.
usageobject
Usage. Always present on GET /v0/organizations and PATCH /v0/organizations.
shared_domainstring
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.
updated_atstring (date-time)Required
Time at which the organization was last updated.
created_atstring (date-time)Required
Time at which the organization was created.

user is a User:

user_idstringRequired
ID of the user.
emailstringRequired
Email address the user signs in with.
namestring | null
Display name, when set.
organization_idstringRequired
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_atstring (date-time)Required
When the user was created.

Errors

StatusMeaning
400OAuth error: authorization_pending, slow_down, expired_token, access_denied, invalid_grant, unsupported_grant_type, invalid_request.
500Unexpected 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,
    "usage": {
      "message_count": 1,
      "storage_bytes": 2048,
      "messages_received_30d": 1,
      "messages_sent_30d": 1
    },
    "shared_domain": "string",
    "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"
  }
}