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