Skip to content
machinemail

Agent Auth

The MachineMail authorization server: OAuth sign-in for apps and MCP clients (authorization code with PKCE, client registration) and the auth.md protocol for agent onboarding.

Note

These endpoints implement the auth.md agent onboarding protocol. They use OAuth-style error bodies ({"error": "...", "error_description": "..."}) instead of the uniform MachineMail error envelope.
MethodPathSummary
GET/.well-known/jwks.jsonJSON Web Key Set
GET/.well-known/oauth-authorization-serverOAuth Authorization Server Metadata
GET/.well-known/oauth-protected-resourceOAuth Protected Resource Metadata
GET/.well-known/oauth-protected-resource/{path}OAuth Protected Resource Metadata (Path-Suffixed)
POST/agent/event/notifyReceive Security Event Token
POST/agent/identityRegister Agent Identity
POST/agent/identity/claimStart Claim Ceremony
GET/auth.mdAgent Onboarding Document
GET/oauth2/authorizeAuthorization Endpoint
POST/oauth2/registerRegister OAuth Client
POST/oauth2/revokeRevoke Token
POST/oauth2/tokenToken Endpoint

JSON Web Key Set

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

Cache-Controlstring
public, max-age=600
keysRecord<string, unknown>[]Required

Errors

StatusMeaning
500Unexpected server error.

Example

Request
curl "https://api.machinemail.dev/.well-known/jwks.json"
Response 200
{
  "keys": [
    {
      "key": "value"
    }
  ]
}

OAuth Authorization Server Metadata

http
GET /.well-known/oauth-authorization-server

RFC 8414 plus the auth.md agent_auth block. identity_types_supported lists only the registration types enabled on this deployment (identity_assertion requires configured agent providers).

Authentication: None.

Response

200 The metadata (application/json)

resourcestringRequired
The protected resource: <issuer>/ (the REST API and MCP), or <issuer>/<path> for a path-suffixed document. On the MCP server’s own host, the MCP server’s URL (https://mcp.machinemail.dev/mcp); on the issuer, <issuer>/mcp is the legacy MCP alias.
authorization_serversstring[]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_supportedstring[]Required
header.
issuerstringRequired
authorization_endpointstringRequired
<issuer>/oauth2/authorize: authorization code grant with PKCE (S256).
token_endpointstringRequired
registration_endpointstringRequired
<issuer>/oauth2/register: Dynamic Client Registration (RFC 7591), public or confidential clients.
revocation_endpointstringRequired
device_authorization_endpointstringRequired
<issuer>/auth/cli/device/code: the device grant of the MachineMail CLI (client_id machinemail-cli only).
jwks_uristringRequired
service_documentationstringRequired
grant_types_supportedstring[]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_supportedstring[]Required
code.
response_modes_supportedstring[]Required
query.
code_challenge_methods_supportedstring[]Required
S256.
token_endpoint_auth_methods_supportedstring[]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_supportedstring[]Required
Same as token_endpoint_auth_methods_supported.
client_id_metadata_document_supportedbooleanRequired
true: an https URL is accepted as client_id (OAuth Client ID Metadata Document).
authorization_response_iss_parameter_supportedbooleanRequired
true: authorization responses carry iss (RFC 9207).
agent_authobjectRequired

Errors

StatusMeaning
500Unexpected server error.

Example

Request
curl "https://api.machinemail.dev/.well-known/oauth-authorization-server"
Response 200
{
  "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

http
GET /.well-known/oauth-protected-resource

RFC 9728. Referenced by the WWW-Authenticate header on 401 responses from /v0 and /mcp. On the dedicated MCP host (MACHINEMAIL_MCP_URL, e.g. mcp.machinemail.dev) it describes the MCP server there, the same as the path-suffixed form.

Authentication: None.

Response

200 The metadata (application/json)

resourcestringRequired
The protected resource: <issuer>/ (the REST API and MCP), or <issuer>/<path> for a path-suffixed document. On the MCP server’s own host, the MCP server’s URL (https://mcp.machinemail.dev/mcp); on the issuer, <issuer>/mcp is the legacy MCP alias.
authorization_serversstring[]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_supportedstring[]Required
header.
resource_namestringRequired
resource_logo_uristringRequired
resource_documentationstringRequired
The /auth.md document, or the MCP guide for <issuer>/mcp.

Errors

StatusMeaning
500Unexpected server error.

Example

Request
curl "https://api.machinemail.dev/.well-known/oauth-protected-resource"
Response 200
{
  "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)

http
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 dedicated MCP host (MACHINEMAIL_MCP_URL, e.g. https://mcp.machinemail.dev/mcp) only mcp is served and resource is that URL; authorization_servers still names the issuer.

Authentication: None.

Path parameters

pathstringRequired
Resource path after the well-known name; may contain slashes (mcp, v0/inboxes).

Response

200 The metadata (application/json)

resourcestringRequired
The protected resource: <issuer>/ (the REST API and MCP), or <issuer>/<path> for a path-suffixed document. On the MCP server’s own host, the MCP server’s URL (https://mcp.machinemail.dev/mcp); on the issuer, <issuer>/mcp is the legacy MCP alias.
authorization_serversstring[]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_supportedstring[]Required
header.
resource_namestringRequired
resource_logo_uristringRequired
resource_documentationstringRequired
The /auth.md document, or the MCP guide for <issuer>/mcp.

Errors

StatusMeaning
500Unexpected server error.

Example

Request
curl "https://api.machinemail.dev/.well-known/oauth-protected-resource/mcp"
Response 200
{
  "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"
}

Receive Security Event Token

http
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

StatusMeaning
400SET rejected.
500Unexpected server error.

Example

Request
curl -X POST "https://api.machinemail.dev/agent/event/notify" \
  -H "Content-Type: application/secevent+jwt" \
  --data-binary @body

Register Agent Identity

http
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 per-hour and a per-24-hours cap per requester, counted over rolling windows (deployment defaults 3 and 10; MachineMail staff can change them, and the error message states the cap in force).

Authentication: None.

Request body

Content type: application/json. Required.

type"anonymous" | "service_auth" | "identity_assertion"Required
Registration type.
namestring
Agent label (first 100 characters). Untrusted; shown as agent-described in the dashboard.
client_namestring
Alias of name.
login_hintstring
service_auth: email address of the user the agent acts for. Required for that type.
assertion_typestring
identity_assertion: urn:ietf:params:oauth:token-type:id-jag. Required for that type.
assertionstring
identity_assertion: the ID-JAG (signed JWT). Required for that type.

Response

200 Registered (application/json)

Headers:

Cache-Controlstring
no-store
registration_idstringRequired
agent_idstringRequired
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_assertionstring
Service-signed JWT to exchange at /oauth2/token. Absent for service_auth until the claim completes.
assertion_expiresstring (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_urlstring
Where to start the claim ceremony (/agent/identity/claim).
claim_tokenstring
Secret for /agent/identity/claim and the claim grant.
claim_token_expiresstring (date-time)
claimobject
service_auth registrations: the ceremony is already started.
inboxobject
anonymous registrations: the ephemeral sandbox inbox. There is no API key; authenticate with the access token from /oauth2/token.

Errors

StatusMeaning
400OAuth 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.
401OAuth error interaction_required or login_required.
429OAuth error rate_limited.
500Unexpected server error.

Example

Request
curl -X POST "https://api.machinemail.dev/agent/identity" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "anonymous",
  "name": "Production"
}'
Response 200
{
  "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

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

claim_tokenstring
Protocol form: the claim_token from registration.
emailstring
Binds the claim to this human. For service_auth and identity_assertion it must match the bound email.
agent_idstring
Alternative to claim_token; needs the registration’s access token or identity assertion as Authorization: Bearer.
registration_idstring
Alias of agent_id.

Response

200 Claim attempt started (application/json)

Headers:

Cache-Controlstring
no-store
registration_idstringRequired
agent_idstringRequired
claim_attempt_idstringRequired
status"initiated"Required
expires_atstring (date-time)Required
When this attempt expires.
claim_attemptClaimBlockRequired
RFC 8628-shaped claim ceremony block.
user_codestringRequired
6-digit code the human enters in the dashboard.
verification_uristringRequired
<PUBLIC_URL>/claim?claim_attempt_token=cat_...: the page to send the human to.
expires_inintegerRequired
Seconds until user_code expires.
intervalintegerRequired
Minimum seconds between /oauth2/token polls.

claim_attempt is a ClaimBlock:

user_codestringRequired
6-digit code the human enters in the dashboard.
verification_uristringRequired
<PUBLIC_URL>/claim?claim_attempt_token=cat_...: the page to send the human to.
expires_inintegerRequired
Seconds until user_code expires.
intervalintegerRequired
Minimum seconds between /oauth2/token polls.

Errors

StatusMeaning
400OAuth error invalid_request.
401OAuth error invalid_claim_token: agent_id without a valid access token or identity assertion as Bearer.
404OAuth error invalid_claim_token: unknown or revoked registration.
409OAuth error claimed_or_in_flight: already claimed.
410OAuth error claim_expired: the registration expired before it was claimed.
500Unexpected server error.

Example

Request
curl -X POST "https://api.machinemail.dev/agent/identity/claim" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "agent@machinemail.dev"
}'
Response 200
{
  "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

http
GET /auth.md

Markdown instructions for agents (auth.md protocol): how to register, claim and authenticate. Lists only the registration types enabled on this deployment.

Authentication: None.

Response

200 auth.md (text/markdown)

Headers:

Cache-Controlstring
public, max-age=300

Errors

StatusMeaning
500Unexpected server error.

Example

Request
curl "https://api.machinemail.dev/auth.md"

Authorization Endpoint

http
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, <PUBLIC_URL>/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

response_typestring
Must be code.
client_idstring
From registration (oac_...), or an https URL of a Client ID Metadata Document.
redirect_uristring
One of the client’s redirect URIs. May be omitted when the client registered exactly one.
code_challengestring
PKCE (RFC 7636): base64url SHA-256 of the code verifier, 43 characters. Required.
code_challenge_methodstring
Must be S256.
scopestring
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.
statestring
Returned unchanged with the response.
resourcestring | string[]
RFC 8707 resource indicator: the MCP server’s URL (https://mcp.machinemail.dev/mcp, from its protected resource metadata; the legacy <issuer>/mcp and the MCP host’s origin are also accepted), or <issuer>/ (the REST API and MCP). Default (omitted): the MCP server. May be repeated when every value names the same resource (a token has one audience).

Response

302 To the consent page, to redirect_uri with an error, or to the error page.

Headers:

Locationstring
<PUBLIC_URL>/connect?request=..., <redirect_uri>?error=...&state=...&iss=... or <PUBLIC_URL>/connect?error=....
Cache-Controlstring
no-store

Errors

StatusMeaning
400Invalid request: error.type is validation and error.param names the field.
500Unexpected server error.

Example

Request
curl "https://api.machinemail.dev/oauth2/authorize"

Register OAuth Client

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

redirect_urisstring[]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_namestring
Shown on the consent page, marked as unverified. At most 200 characters. (max length 200)
client_uristring
Home page of the client; shown on the consent page when it is https. (max length 2000)
logo_uristring
Logo shown on the consent page when it is https. (max length 2000)
token_endpoint_auth_methodstring
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_typesstring[]
authorization_code and refresh_token (the default). Must include authorization_code (invalid_client_metadata otherwise); other values are dropped from the registration.
response_typesstring[]
code (the default). Other values: invalid_client_metadata.
scopestring
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_idstring
Identifier of the client software. Stored, not verified. (max length 200)
software_versionstring
Version of the client software. Stored, not verified. (max length 100)

Response

201 Registered (application/json)

Headers:

Cache-Controlstring
no-store
client_idstringRequired
The new client identifier (oac_...).
client_id_issued_atintegerRequired
Seconds since the epoch.
client_namestring
client_uristring
logo_uristring
redirect_urisstring[]Required
grant_typesstring[]Required
response_typesstring[]Required
token_endpoint_auth_method"none" | "client_secret_post" | "client_secret_basic"Required
As requested when supported, otherwise none.
client_secretstring
Confidential clients only (client_secret_post, client_secret_basic): shown once, stored only as a hash. Keep it secret.
client_secret_expires_atinteger
With client_secret: 0, the secret does not expire.
application_type"native" | "web"Required
scopestring
software_idstring
software_versionstring

Errors

StatusMeaning
400OAuth error invalid_redirect_uri or invalid_client_metadata (RFC 7591 section 3.2.2), including a body that fails validation.
429OAuth error rate_limited.
500Unexpected server error.

Example

Request
curl -X POST "https://api.machinemail.dev/oauth2/register" \
  -H "Content-Type: application/json" \
  -d '{
  "redirect_uris": [
    "string"
  ]
}'
Response 201
{
  "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

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

tokenstringRequired
An access token, a refresh token or an identity assertion.
token_type_hintstring
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_idstring
Refresh and access tokens of an app: when given, must be the client the token was issued to, or nothing is revoked.
client_secretstring
A confidential client authenticates as at the token endpoint (client_secret_post, or HTTP Basic).

Response

200 Revoked (empty body)

Errors

StatusMeaning
400OAuth error invalid_request.
401OAuth error invalid_client: a confidential client sent no or a wrong client_secret.
500Unexpected server error.

Example

Request
curl -X POST "https://api.machinemail.dev/oauth2/revoke" \
  --data-urlencode "token=string"

Token Endpoint

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

grant_typestringRequired
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.
codestring
authorization_code: the code from the authorization response. Single use; expires after 2 minutes.
redirect_uristring
authorization_code: the redirect_uri of the authorization request, when it had one.
code_verifierstring
authorization_code: the PKCE code verifier (43 to 128 characters).
refresh_tokenstring
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.
scopestring
refresh_token, optional: narrow the new access token to these scopes of the grant.
device_codestring
device_code: the device_code from /auth/cli/device/code.
assertionstring
jwt-bearer grant: the identity_assertion.
client_idstring
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: the registration id.
client_secretstring
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.
resourcestring
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 <issuer>/.
claim_tokenstring
Claim grant: the claim_token from registration.

Response

200 Access token (an API key for the device grant) (application/json)

Headers:

Cache-Controlstring
no-store
Pragmastring
no-cache

Type: TokenResponse | CliDeviceTokenResponse

Errors

StatusMeaning
400OAuth 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).
401OAuth 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).
500Unexpected server error.

Example

Request
curl -X POST "https://api.machinemail.dev/oauth2/token" \
  --data-urlencode "grant_type=string"
Response 200
{
  "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"
}