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
{"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
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-ControlstringkeysRecord<string, unknown>[]RequiredErrors
| Status | Meaning |
|---|---|
500 | Unexpected server error. |
Example
OAuth Authorization Server Metadata
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)
resourcestringRequiredauthorization_serversstring[]Requiredscopes_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")[]Requiredbearer_methods_supportedstring[]RequiredissuerstringRequiredauthorization_endpointstringRequiredtoken_endpointstringRequiredregistration_endpointstringRequiredrevocation_endpointstringRequireddevice_authorization_endpointstringRequiredjwks_uristringRequiredservice_documentationstringRequiredgrant_types_supportedstring[]Requiredresponse_types_supportedstring[]Requiredresponse_modes_supportedstring[]Requiredcode_challenge_methods_supportedstring[]Requiredtoken_endpoint_auth_methods_supportedstring[]Requiredrevocation_endpoint_auth_methods_supportedstring[]Requiredclient_id_metadata_document_supportedbooleanRequiredauthorization_response_iss_parameter_supportedbooleanRequiredagent_authobjectRequiredErrors
| Status | Meaning |
|---|---|
500 | Unexpected server error. |
Example
OAuth Protected Resource Metadata
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)
resourcestringRequiredauthorization_serversstring[]Requiredscopes_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")[]Requiredbearer_methods_supportedstring[]Requiredresource_namestringRequiredresource_logo_uristringRequiredresource_documentationstringRequiredErrors
| Status | Meaning |
|---|---|
500 | Unexpected server error. |
Example
OAuth Protected Resource Metadata (Path-Suffixed)
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
pathstringRequiredResponse
200 The metadata (application/json)
resourcestringRequiredauthorization_serversstring[]Requiredscopes_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")[]Requiredbearer_methods_supportedstring[]Requiredresource_namestringRequiredresource_logo_uristringRequiredresource_documentationstringRequiredErrors
| Status | Meaning |
|---|---|
500 | Unexpected server error. |
Example
Receive Security Event Token
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
Register 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"Requirednamestringclient_namestringlogin_hintstringassertion_typestringassertionstringResponse
200 Registered (application/json)
Headers:
Cache-Controlstringregistration_idstringRequiredagent_idstringRequiredregistration_type"anonymous" | "service_auth" | "identity_assertion"Requiredidentity_assertionstringassertion_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")[]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")[]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")[]claim_urlstringclaim_tokenstringclaim_token_expiresstring (date-time)claimobjectinboxobjectErrors
| 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. |
429 | OAuth error rate_limited. |
500 | Unexpected server error. |
Example
Start Claim Ceremony
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_tokenstringemailstringagent_idstringregistration_idstringResponse
200 Claim attempt started (application/json)
Headers:
Cache-Controlstringregistration_idstringRequiredagent_idstringRequiredclaim_attempt_idstringRequiredstatus"initiated"Requiredexpires_atstring (date-time)Requireduser_codestringRequiredverification_uristringRequiredexpires_inintegerRequiredintervalintegerRequiredclaim_attempt is a ClaimBlock:
user_codestringRequiredverification_uristringRequiredexpires_inintegerRequiredintervalintegerRequiredErrors
| 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
Agent Onboarding Document
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-ControlstringErrors
| Status | Meaning |
|---|---|
500 | Unexpected server error. |
Example
Authorization Endpoint
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_typestringclient_idstringredirect_uristringcode_challengestringcode_challenge_methodstringscopestringstatestringresourcestring | string[]Response
302 To the consent page, to redirect_uri with an error, or to the error page.
Headers:
LocationstringCache-ControlstringErrors
| Status | Meaning |
|---|---|
400 | Invalid request: error.type is validation and error.param names the field. |
500 | Unexpected server error. |
Example
Register OAuth Client
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[]Requiredclient_namestringclient_uristringlogo_uristringtoken_endpoint_auth_methodstringgrant_typesstring[]response_typesstring[]scopestringapplication_type"native" | "web"software_idstringsoftware_versionstringResponse
201 Registered (application/json)
Headers:
Cache-Controlstringclient_idstringRequiredclient_id_issued_atintegerRequiredclient_namestringclient_uristringlogo_uristringredirect_urisstring[]Requiredgrant_typesstring[]Requiredresponse_typesstring[]Requiredtoken_endpoint_auth_method"none" | "client_secret_post" | "client_secret_basic"Requiredclient_secretstringclient_secret_expires_atintegerapplication_type"native" | "web"Requiredscopestringsoftware_idstringsoftware_versionstringErrors
| 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
Revoke Token
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.
tokenstringRequiredtoken_type_hintstringclient_idstringclient_secretstringResponse
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
Token Endpoint
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_typestringRequiredcodestringredirect_uristringcode_verifierstringrefresh_tokenstringscopestringdevice_codestringassertionstringclient_idstringclient_secretstringresourcestringclaim_tokenstringResponse
200 Access token (an API key for the device grant) (application/json)
Headers:
Cache-ControlstringPragmastringType: 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. |