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

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.

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: / (the REST API and MCP), or / 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, /mcp is the legacy MCP alias.
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 /oauth2/authorize: authorization code grant with PKCE (S256).
token_endpoint string required
registration_endpoint string required /oauth2/register: Dynamic Client Registration (RFC 7591), public or confidential clients.
revocation_endpoint string required
device_authorization_endpoint string required /auth/cli/device/code: the device grant of the MachineMail CLI (client_id machinemail-cli only).
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 /mcp and the MCP host’s origin are also accepted), or / (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:

Field Type Description
Location string https://machinemail.dev/connect?request=..., ?error=...&state=...&iss=... or https://machinemail.dev/connect?error=....
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"
}