agent auth.md
title: "Agent Auth" description: "MachineMail's authorization server: OAuth sign-in with PKCE and client registration for apps and MCP clients, and the auth.md agent onboarding endpoints." canonical: "https://machinemail.dev/docs/reference/api/agent-auth"
Agent Auth
MachineMail's authorization server: OAuth sign-in with PKCE and client registration for apps and MCP clients, and the auth.md agent onboarding endpoints.
Source: https://machinemail.dev/docs/reference/api/agent-auth
{"error": "...", "error_description": "..."}) instead of the uniform MachineMail error envelope.
| Method | Path | Summary |
|---|---|---|
GET |
/.well-known/jwks.json |
JSON Web Key Set |
GET |
/.well-known/oauth-authorization-server |
OAuth Authorization Server Metadata |
GET |
/.well-known/oauth-protected-resource |
OAuth Protected Resource Metadata |
GET |
/.well-known/oauth-protected-resource/{path} |
OAuth Protected Resource Metadata (Path-Suffixed) |
POST |
/agent/event/notify |
Receive Security Event Token |
POST |
/agent/identity |
Register Agent Identity |
POST |
/agent/identity/claim |
Start Claim Ceremony |
GET |
/auth.md |
Agent Onboarding Document |
GET |
/oauth2/authorize |
Authorization Endpoint |
POST |
/oauth2/register |
Register OAuth Client |
POST |
/oauth2/revoke |
Revoke Token |
POST |
/oauth2/token |
Token Endpoint |
JSON Web Key Set
GET /.well-known/jwks.json
The service’s public signing key (RFC 7517). Verifies the identity assertions and access tokens this service issues.
Authentication: None.
Response
200 JWKS (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Cache-Control |
string |
public, max-age=600 |
| Field | Type | Description | |
|---|---|---|---|
keys |
Record<string, unknown>[] |
required |
Errors
| Status | Meaning |
|---|---|
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/.well-known/jwks.json"
{
"keys": [
{
"key": "value"
}
]
}
OAuth Authorization Server Metadata
GET /.well-known/oauth-authorization-server
RFC 8414 plus the auth.md agent_auth block. identity_types_supported lists the registration types MachineMail accepts.
Authentication: None.
Response
200 The metadata (application/json)
| Field | Type | Description | |
|---|---|---|---|
resource |
string |
required | The protected resource: |
authorization_servers |
string[] |
required | |
scopes_supported |
("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 | |
bearer_methods_supported |
string[] |
required | header. |
issuer |
string |
required | |
authorization_endpoint |
string |
required | |
token_endpoint |
string |
required | |
registration_endpoint |
string |
required | |
revocation_endpoint |
string |
required | |
device_authorization_endpoint |
string |
required | |
jwks_uri |
string |
required | |
service_documentation |
string |
required | |
grant_types_supported |
string[] |
required | authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:device_code, urn:ietf:params:oauth:grant-type:jwt-bearer, urn:workos:agent-auth:grant-type:claim. |
response_types_supported |
string[] |
required | code. |
response_modes_supported |
string[] |
required | query. |
code_challenge_methods_supported |
string[] |
required | S256. |
token_endpoint_auth_methods_supported |
string[] |
required | none (public clients: every URL client ID, and registrations without a secret), client_secret_post and client_secret_basic (registrations that asked for a secret). Every client uses PKCE. |
revocation_endpoint_auth_methods_supported |
string[] |
required | Same as token_endpoint_auth_methods_supported. |
client_id_metadata_document_supported |
boolean |
required | true: an https URL is accepted as client_id (OAuth Client ID Metadata Document). |
authorization_response_iss_parameter_supported |
boolean |
required | true: authorization responses carry iss (RFC 9207). |
agent_auth |
object |
required |
Errors
| Status | Meaning |
|---|---|
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/.well-known/oauth-authorization-server"
{
"resource": "string",
"authorization_servers": ["string"],
"scopes_supported": ["inboxes.read"],
"bearer_methods_supported": ["string"],
"issuer": "string",
"authorization_endpoint": "string",
"token_endpoint": "string",
"registration_endpoint": "string",
"revocation_endpoint": "string",
"device_authorization_endpoint": "string",
"jwks_uri": "https://example.com",
"service_documentation": "string",
"grant_types_supported": ["string"],
"response_types_supported": ["string"],
"response_modes_supported": ["string"],
"code_challenge_methods_supported": ["string"],
"token_endpoint_auth_methods_supported": ["string"],
"revocation_endpoint_auth_methods_supported": ["string"],
"client_id_metadata_document_supported": false,
"authorization_response_iss_parameter_supported": false,
"agent_auth": {
"skill": "string",
"identity_endpoint": "string",
"claim_endpoint": "string",
"events_endpoint": "string",
"identity_types_supported": ["anonymous"],
"identity_assertion": {
"assertion_types_supported": ["string"]
},
"events_supported": ["string"]
}
}
OAuth Protected Resource Metadata
GET /.well-known/oauth-protected-resource
RFC 9728. Referenced by the WWW-Authenticate header on 401 responses from /v0 and /mcp. On the MCP host (mcp.machinemail.dev) it describes the MCP server there, the same as the path-suffixed form.
Authentication: None.
Response
200 The metadata (application/json)
Errors
| Status | Meaning |
|---|---|
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/.well-known/oauth-protected-resource"
{
"resource": "string",
"authorization_servers": ["string"],
"scopes_supported": ["inboxes.read"],
"bearer_methods_supported": ["string"],
"resource_name": "string",
"resource_logo_uri": "https://example.com",
"resource_documentation": "string"
}
OAuth Protected Resource Metadata (Path-Suffixed)
GET /.well-known/oauth-protected-resource/{path}
RFC 9728 metadata for a resource at a path, for example /.well-known/oauth-protected-resource/mcp. resource is <issuer>/<path>; everything else is the same as the root document. path may contain slashes. On the MCP host (https://mcp.machinemail.dev/mcp) only mcp is served and resource is that URL; authorization_servers still names the issuer.
Authentication: None.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
path |
string |
required | Resource path after the well-known name; may contain slashes (mcp, v0/inboxes). |
Response
200 The metadata (application/json)
Errors
| Status | Meaning |
|---|---|
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/.well-known/oauth-protected-resource/mcp"
Receive Security Event Token
POST /agent/event/notify
RFC 8935 push delivery from a trusted agent provider. The body is a signed SET (typ: secevent+jwt) with aud equal to the issuer, a unique jti, and an events claim containing https://schemas.workos.com/events/agent/auth/identity/assertion/revoked. Every registration for that (iss, sub) is revoked.
Authentication: None.
Request body
Content type: application/secevent+jwt. Required.
Type: string
Response
202 Accepted (empty body)
Errors
| Status | Meaning |
|---|---|
400 |
SET rejected. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/agent/event/notify" \
-H "Content-Type: application/secevent+jwt" \
--data-binary @body
Register Agent Identity
POST /agent/identity
Dispatches on type (auth.md). No API key is ever returned.
anonymous returns a pre-claim identity_assertion and the sandbox inbox (one ephemeral inbox on the shared domain); exchange the assertion at /oauth2/token for an access token.
service_auth withholds the assertion and returns a claim block until the named user (login_hint) confirms.
identity_assertion verifies an ID-JAG from a trusted provider. When the verified email belongs to an existing user it answers 401 interaction_required with a claim_token and claim block; when auth_time is too old, 401 login_required with max_age. Both set WWW-Authenticate: AgentAuth error="...".
Registration is rate limited per requester (an IPv4 address, or an IPv6 /64) and overall: 429 rate_limited with Retry-After. anonymous and service_auth share a cap per requester of at most 1 per rolling hour and 3 per rolling 24 hours (an IPv6 /48, all its /64s together, at most 10 per rolling 24 hours), and a platform-wide cap of about 100 per rolling hour and 1000 per rolling 24 hours. anonymous is also refused while about 500 unclaimed sandbox inboxes are live (Sandbox capacity is full right now); a claimed or expired registration frees its place.
Registrations from a network that was blocked for abuse are refused with 403 registration_blocked (no Retry-After: retrying does not help).
identity_assertion with a verified email that matches no account creates a new organization; when that email is a disposable email address it is refused with 403 disposable_email_not_allowed (param email): MachineMail doesn’t accept disposable addresses for new accounts.
Authentication: None.
Request body
Content type: application/json. Required.
| Field | Type | Description | |
|---|---|---|---|
type |
"anonymous" | "service_auth" | "identity_assertion" |
required | Registration type. |
name |
string |
Agent label (first 100 characters). Untrusted; shown as agent-described in the dashboard. | |
client_name |
string |
Alias of name. | |
login_hint |
string |
service_auth: email address of the user the agent acts for. Required for that type. | |
assertion_type |
string |
identity_assertion: urn:ietf:params:oauth:token-type:id-jag. Required for that type. | |
assertion |
string |
identity_assertion: the ID-JAG (signed JWT). Required for that type. |
Response
200 Registered (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Cache-Control |
string |
no-store |
| Field | Type | Description | |
|---|---|---|---|
registration_id |
string |
required | |
agent_id |
string |
required | Same value as registration_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. |
identity_assertion |
string |
Service-signed JWT to exchange at /oauth2/token. Absent for service_auth until the claim completes. | |
assertion_expires |
string (date-time) |
||
scopes |
("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")[] |
identity_assertion registrations: the granted scopes. | |
pre_claim_scopes |
("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")[] |
anonymous registrations: scopes before a human claims the agent (its sandbox inbox only). | |
post_claim_scopes |
("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")[] |
Scopes after the claim. | |
claim_url |
string |
Where to start the claim ceremony (/agent/identity/claim). | |
claim_token |
string |
Secret for /agent/identity/claim and the claim grant. | |
claim_token_expires |
string (date-time) |
||
claim |
object |
service_auth registrations: the ceremony is already started. | |
inbox |
object |
anonymous registrations: the ephemeral sandbox inbox. There is no API key; authenticate with the access token from /oauth2/token. |
Errors
| Status | Meaning |
|---|---|
400 |
OAuth error: invalid_request (including a body that fails validation), anonymous_not_enabled, service_auth_not_enabled, identity_assertion_not_enabled, invalid_issuer, invalid_signature, invalid_audience, expired, replay_detected, invalid_client_id, missing_verified_email. |
401 |
OAuth error interaction_required or login_required. |
403 |
OAuth error registration_blocked: registration is not available from this network. Or disposable_email_not_allowed: an identity_assertion would create a new organization for a disposable email address. |
429 |
OAuth error rate_limited. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/agent/identity" \
-H "Content-Type: application/json" \
-d '{
"type": "anonymous",
"name": "Production"
}'
{
"registration_id": "string",
"agent_id": "string",
"registration_type": "anonymous",
"identity_assertion": "string",
"assertion_expires": "2026-10-01T12:00:00.000Z",
"scopes": ["inboxes.read"],
"pre_claim_scopes": ["inboxes.read"],
"post_claim_scopes": ["inboxes.read"],
"claim_url": "https://example.com",
"claim_token": "string",
"claim_token_expires": "2026-10-01T12:00:00.000Z",
"claim": {
"user_code": "string",
"verification_uri": "https://example.com",
"expires_in": 1,
"interval": 1
},
"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"
}
}
Start Claim Ceremony
POST /agent/identity/claim
Called by the agent. Protocol form: {claim_token, email?}. Alternative: {agent_id} (or registration_id) with the registration’s access token or identity assertion as Authorization: Bearer.
Returns a 6-digit user_code and a verification_uri to show the human, who completes the claim in the dashboard (POST /auth/claim). The agent then polls /oauth2/token with the claim grant. Calling it again starts a fresh attempt.
Authentication: None.
Request body
Content type: application/json. Required.
Send claim_token (and optionally email), or agent_id with Bearer proof.
| Field | Type | Description | |
|---|---|---|---|
claim_token |
string |
Protocol form: the claim_token from registration. | |
email |
string |
Binds the claim to this human. For service_auth and identity_assertion it must match the bound email. | |
agent_id |
string |
Alternative to claim_token; needs the registration’s access token or identity assertion as Authorization: Bearer. | |
registration_id |
string |
Alias of agent_id. |
Response
200 Claim attempt started (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Cache-Control |
string |
no-store |
| Field | Type | Description | |
|---|---|---|---|
registration_id |
string |
required | |
agent_id |
string |
required | |
claim_attempt_id |
string |
required | |
status |
"initiated" |
required | |
expires_at |
string (date-time) |
required | When this attempt expires. |
claim_attempt |
ClaimBlock |
required | RFC 8628-shaped claim ceremony block. |
user_code |
string |
required | 6-digit code the human enters in the dashboard. |
verification_uri |
string |
required | https://machinemail.dev/claim?claim_attempt_token=cat_...: the page to send the human to. |
expires_in |
integer |
required | Seconds until user_code expires. |
interval |
integer |
required | Minimum seconds between /oauth2/token polls. |
claim_attempt is a ClaimBlock:
| Field | Type | Description | |
|---|---|---|---|
user_code |
string |
required | 6-digit code the human enters in the dashboard. |
verification_uri |
string |
required | https://machinemail.dev/claim?claim_attempt_token=cat_...: the page to send the human to. |
expires_in |
integer |
required | Seconds until user_code expires. |
interval |
integer |
required | Minimum seconds between /oauth2/token polls. |
Errors
| Status | Meaning |
|---|---|
400 |
OAuth error invalid_request. |
401 |
OAuth error invalid_claim_token: agent_id without a valid access token or identity assertion as Bearer. |
404 |
OAuth error invalid_claim_token: unknown or revoked registration. |
409 |
OAuth error claimed_or_in_flight: already claimed. |
410 |
OAuth error claim_expired: the registration expired before it was claimed. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/agent/identity/claim" \
-H "Content-Type: application/json" \
-d '{
"email": "agent@machinemail.dev"
}'
{
"registration_id": "string",
"agent_id": "string",
"claim_attempt_id": "string",
"status": "initiated",
"expires_at": "2026-10-01T12:00:00.000Z",
"claim_attempt": {
"user_code": "string",
"verification_uri": "https://example.com",
"expires_in": 1,
"interval": 1
},
"user_code": "string",
"verification_uri": "https://example.com",
"expires_in": 1,
"interval": 1
}
Agent Onboarding Document
GET /auth.md
Markdown instructions for agents (auth.md protocol): how to register, claim and authenticate. Lists the registration types MachineMail accepts.
Authentication: None.
Response
200 auth.md (text/markdown)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Cache-Control |
string |
public, max-age=300 |
Errors
| Status | Meaning |
|---|---|
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/auth.md"
Authorization Endpoint
GET /oauth2/authorize
OAuth 2.1 authorization code grant, opened in the person’s browser by an app or MCP client. Requires PKCE (code_challenge with code_challenge_method=S256) and the RFC 8707 resource: the MCP server’s URL (https://mcp.machinemail.dev/mcp, or the legacy <issuer>/mcp; a token for either works on both MCP endpoints), or <issuer>/ for the REST API and MCP.
A valid request is stored for 10 minutes and the browser is sent to the dashboard consent page, https://machinemail.dev/connect?request=<id> (sign-in first when needed). The person sees the client, where the response goes and the scopes, chooses the whole organization, one pod or one inbox, and approves or denies. The browser then returns to redirect_uri with code (single use, 2 minutes) or error=access_denied, plus state and iss (RFC 9207).
Errors: an unknown client_id, a redirect_uri the client did not register, or too many requests from one address (rate_limited) is shown on /connect?error=... and never redirected. Every other error goes back to redirect_uri as error (invalid_request, unsupported_response_type, invalid_scope, invalid_target) with error_description, state and iss.
Authentication: None.
Query parameters
| Field | Type | Description | |
|---|---|---|---|
response_type |
string |
Must be code. | |
client_id |
string |
From registration (oac_...), or an https URL of a Client ID Metadata Document. | |
redirect_uri |
string |
One of the client’s redirect URIs. May be omitted when the client registered exactly one. | |
code_challenge |
string |
PKCE (RFC 7636): base64url SHA-256 of the code verifier, 43 characters. Required. | |
code_challenge_method |
string |
Must be S256. | |
scope |
string |
Space-separated Scope values. Default: every scope the client may ask for. offline_access is accepted and ignored (refresh tokens are always issued); other unknown values are dropped, and a list of only unknown values counts as the default. | |
state |
string |
Returned unchanged with the response. | |
resource |
string | string[] |
RFC 8707 resource indicator: the MCP server’s URL (https://mcp.machinemail.dev/mcp, from its protected resource metadata; the legacy |
Response
302 To the consent page, to redirect_uri with an error, or to the error page.
Headers:
| Field | Type | Description | |
|---|---|---|---|
Location |
string |
https://machinemail.dev/connect?request=..., |
|
Cache-Control |
string |
no-store |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
500 |
Unexpected server error. |
Example
curl "https://api.machinemail.dev/oauth2/authorize"
Register OAuth Client
POST /oauth2/register
RFC 7591 Dynamic Client Registration for apps and MCP clients. Public clients by default (token_endpoint_auth_method none, no secret). A client that asks for client_secret_post or client_secret_basic is confidential: the response carries its client_secret (shown once, never expires: client_secret_expires_at 0) and the token and revocation endpoints then require it, by either method. Any other method is replaced with none. Every authorization uses PKCE, confidential or not. No registration access token is issued (registrations cannot be read or changed later; register again instead).
Clients that can host a Client ID Metadata Document should use its https URL as client_id instead of registering (client_id_metadata_document_supported in the server metadata). Registrations never used for an authorization are deleted after 7 days.
Rate limited per client IP and overall: 429 rate_limited with Retry-After.
Authentication: None.
Request body
Content type: application/json. Required.
RFC 7591 client metadata. Unknown members are ignored.
| Field | Type | Description | |
|---|---|---|---|
redirect_uris |
string[] |
required | Where the authorization response may be sent: https URLs, http loopback URLs (http://127.0.0.1, http://[::1] or http://localhost; any port is accepted at authorization time, RFC 8252), or a private-use scheme of a native app (cursor://...). No fragments. At most 10. (min items 1, max items 10) |
client_name |
string |
Shown on the consent page, marked as unverified. At most 200 characters. (max length 200) | |
client_uri |
string |
Home page of the client; shown on the consent page when it is https. (max length 2000) | |
logo_uri |
string |
Logo shown on the consent page when it is https. (max length 2000) | |
token_endpoint_auth_method |
string |
none (the default: a public client, PKCE only), client_secret_post or client_secret_basic (a confidential client: the response carries a client_secret, and the token endpoint accepts it either way). Any other value is replaced with none in the response (RFC 7591 section 3.2.1). | |
grant_types |
string[] |
authorization_code and refresh_token (the default). Must include authorization_code (invalid_client_metadata otherwise); other values are dropped from the registration. | |
response_types |
string[] |
code (the default). Other values: invalid_client_metadata. | |
scope |
string |
Space-separated Scope values the client may ask for. Default: every scope. (max length 2000) | |
application_type |
"native" | "web" |
native or web (OpenID Connect Dynamic Client Registration; required by MCP 2026-07-28). Default web. | |
software_id |
string |
Identifier of the client software. Stored, not verified. (max length 200) | |
software_version |
string |
Version of the client software. Stored, not verified. (max length 100) |
Response
201 Registered (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Cache-Control |
string |
no-store |
| Field | Type | Description | |
|---|---|---|---|
client_id |
string |
required | The new client identifier (oac_...). |
client_id_issued_at |
integer |
required | Seconds since the epoch. |
client_name |
string |
||
client_uri |
string |
||
logo_uri |
string |
||
redirect_uris |
string[] |
required | |
grant_types |
string[] |
required | |
response_types |
string[] |
required | |
token_endpoint_auth_method |
"none" | "client_secret_post" | "client_secret_basic" |
required | As requested when supported, otherwise none. |
client_secret |
string |
Confidential clients only (client_secret_post, client_secret_basic): shown once, stored only as a hash. Keep it secret. | |
client_secret_expires_at |
integer |
With client_secret: 0, the secret does not expire. | |
application_type |
"native" | "web" |
required | |
scope |
string |
||
software_id |
string |
||
software_version |
string |
Errors
| Status | Meaning |
|---|---|
400 |
OAuth error invalid_redirect_uri or invalid_client_metadata (RFC 7591 section 3.2.2), including a body that fails validation. |
429 |
OAuth error rate_limited. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/oauth2/register" \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": [
"string"
]
}'
{
"client_id": "my-client-id",
"client_id_issued_at": 1,
"client_name": "string",
"client_uri": "https://example.com",
"logo_uri": "https://example.com",
"redirect_uris": ["string"],
"grant_types": ["string"],
"response_types": ["string"],
"token_endpoint_auth_method": "none",
"client_secret": "string",
"client_secret_expires_at": 1,
"application_type": "native",
"scope": "string",
"software_id": "string",
"software_version": "string"
}
Revoke Token
POST /oauth2/revoke
RFC 7009. Idempotent: unknown tokens succeed too. Revoking an access token ends only that token; revoking a refresh token ends its whole grant (the app is disconnected); revoking an identity assertion (tried first with token_type_hint=identity_assertion) revokes the whole registration. A confidential client that names itself authenticates as at the token endpoint (401 invalid_client otherwise). Form-encoded or JSON bodies.
Authentication: None.
Request body
Content type: application/x-www-form-urlencoded or application/json. Required.
| Field | Type | Description | |
|---|---|---|---|
token |
string |
required | An access token, a refresh token or an identity assertion. |
token_type_hint |
string |
access_token, refresh_token or identity_assertion. A refresh token revokes its whole grant; an identity assertion its whole registration. Other values are ignored. | |
client_id |
string |
Refresh and access tokens of an app: when given, must be the client the token was issued to, or nothing is revoked. | |
client_secret |
string |
A confidential client authenticates as at the token endpoint (client_secret_post, or HTTP Basic). |
Response
200 Revoked (empty body)
Errors
| Status | Meaning |
|---|---|
400 |
OAuth error invalid_request. |
401 |
OAuth error invalid_client: a confidential client sent no or a wrong client_secret. |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/oauth2/revoke" \
--data-urlencode "token=string"
Token Endpoint
POST /oauth2/token
The token endpoint of the MachineMail authorization server. Public clients (token_endpoint_auth_method none: URL client IDs and registrations without a secret) send client_id in the body. Confidential clients (registered with client_secret_post or client_secret_basic) authenticate with HTTP Basic or client_secret in the body, either way; a missing or wrong secret is 401 invalid_client. PKCE applies to every client.
authorization_code redeems a code from /oauth2/authorize with its PKCE code_verifier and returns an access token bound to the requested resource and a refresh_token. refresh_token returns a new access token and a new refresh token, and the old one stops working: presenting it again within 60 seconds returns the same new refresh token (concurrent refreshes and retries are safe), later it revokes the whole grant (reuse detection). A code redeemed twice also revokes its grant.
urn:ietf:params:oauth:grant-type:device_code is the mm login device grant (same as /auth/cli/device/token; the response is a CliDeviceTokenResponse with an API key).
urn:ietf:params:oauth:grant-type:jwt-bearer (RFC 7523) exchanges an auth.md identity_assertion for an access token. urn:workos:agent-auth:grant-type:claim is polled with a claim_token until the human finishes the claim, then returns an access token and the post-claim identity_assertion.
Form-encoded (RFC 6749) or JSON bodies.
Authentication: None.
Request body
Content type: application/x-www-form-urlencoded or application/json. Required.
| Field | Type | Description | |
|---|---|---|---|
grant_type |
string |
required | authorization_code (with PKCE) and refresh_token for apps and MCP clients; urn:ietf:params:oauth:grant-type:device_code for mm login; urn:ietf:params:oauth:grant-type:jwt-bearer (RFC 7523) exchanges an assertion; urn:workos:agent-auth:grant-type:claim polls with a claim_token. Other values: unsupported_grant_type. |
code |
string |
authorization_code: the code from the authorization response. Single use; expires after 2 minutes. | |
redirect_uri |
string |
authorization_code: the redirect_uri of the authorization request, when it had one. | |
code_verifier |
string |
authorization_code: the PKCE code verifier (43 to 128 characters). | |
refresh_token |
string |
refresh_token: the current refresh token. Each use returns a new one; presenting a replaced one within 60 seconds of its replacement returns the current one, later it revokes the whole grant. | |
scope |
string |
refresh_token, optional: narrow the new access token to these scopes of the grant. | |
device_code |
string |
device_code: the device_code from /auth/cli/device/code. | |
assertion |
string |
jwt-bearer grant: the identity_assertion. | |
client_id |
string |
authorization_code and refresh_token: the client the code or token was issued to (invalid_grant otherwise); may instead come from an HTTP Basic Authorization header (client_secret_basic), or be left out (taken from the code or token). device_code: required. jwt-bearer, optional audience pin: must be |
|
client_secret |
string |
Confidential clients using client_secret_post (registered with a secret). Alternatively send Authorization: Basic base64(urlencode(client_id):urlencode(client_secret)) (client_secret_basic), not both. A missing or wrong secret: 401 invalid_client. Public clients need none; one they send is ignored. | |
resource |
string |
RFC 8707. authorization_code and refresh_token: must be the resource of the authorization request (invalid_target otherwise). jwt-bearer, optional audience pin: must be |
|
claim_token |
string |
Claim grant: the claim_token from registration. |
Response
200 Access token (an API key for the device grant) (application/json)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Cache-Control |
string |
no-store | |
Pragma |
string |
no-cache |
Type: TokenResponse | CliDeviceTokenResponse
Errors
| Status | Meaning |
|---|---|
400 |
OAuth error: invalid_request, unsupported_grant_type, invalid_grant, invalid_target, invalid_scope (authorization_code, refresh_token, jwt-bearer); authorization_pending, slow_down, expired_token, access_denied (claim and device grants). |
401 |
OAuth error invalid_client: a confidential client sent no or a wrong client_secret (with WWW-Authenticate: Basic when it used HTTP Basic), client_id does not match the assertion (jwt-bearer), or is not machinemail-cli (device grant). |
500 |
Unexpected server error. |
Example
curl -X POST "https://api.machinemail.dev/oauth2/token" \
--data-urlencode "grant_type=string"
{
"access_token": "string",
"token_type": "Bearer",
"expires_in": 1,
"scope": "string",
"refresh_token": "string",
"identity_assertion": "string",
"assertion_expires": "2026-10-01T12:00:00.000Z"
}