Supermail
Authorization

Token

POST /oauth/token — exchange an authorization code or refresh an access token — and POST /oauth/revoke.

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

The token endpoint. Supports two grant types: authorization_code (exchange the code from the authorize step) and refresh_token (rotate an expiring access token). Every response carries Cache-Control: no-store.

Client authentication

Confidential clients authenticate with their secret, either as HTTP Basic auth or in the request body:

HTTP Basic
Authorization: Basic base64(client_id:client_secret)
Body
client_id=app_01H...&client_secret=cs_9f3a...

Public clients send only client_id. Every client, public or confidential, also sends the PKCE verifier for an authorization-code exchange.

private_key_jwt (RFC 7523)

A confidential app may instead register a public Ed25519 key set and never hold a secret (RFC 20). Pass client_jwks (1–4 public Ed25519 JWKs with unique kids, no private members) to App/create with confidential: true; no client_secret is minted. Replace the keys later with App/update client_jwks (same validation). Authenticate each token or revocation request with a signed assertion instead of a secret:

Body
client_id=app_01H...
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<compact EdDSA JWT>
PartRequirement
Headeralg = EdDSA; kid names a registered key; no crit
iss, subBoth exactly the client_id (client_id in the body is optional but must match)
audA single string: exactly the token endpoint URL from discovery (<issuer>/oauth/token), also at /oauth/revoke
expIn the future, at most 300 seconds after iat
iat, nbfAt most 60 seconds in the future
jti16–256 printable characters, single use until exp

An assertion combined with a secret or a Basic header, a replayed jti, or a secret presented for a private_key_jwt app is refused with invalid_client. Discovery lists private_key_jwt in token_endpoint_auth_methods_supported.

OAuth 2.1 does not repeat redirect_uri in the token request. Supermail still requires and exact-matches it during authorization.

Authorization code grant

FieldRequiredDescription
grant_typeyesauthorization_code
codeyesThe code from the authorize redirect
code_verifieryesThe 43–128 character PKCE verifier matching the S256 challenge
client_idyesYour application's id
client_secretconfidentialIf not using Basic auth
Request
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=ac_9f3a...
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&client_id=app_01H...
Response
{
  "token_type": "Bearer",
  "access_token": "<jwt>",
  "expires_in": 600,
  "refresh_token": "rt_5b2c...",
  "id_token": "<jwt>",
  "scope": "openid mail.read accounts.read"
}

Refresh token grant

FieldRequiredDescription
grant_typeyesrefresh_token
refresh_tokenyesThe current refresh token
client_idyesYour application's id
client_secretconfidentialIf not using Basic auth
Request
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=rt_5b2c...&client_id=app_01H...

The response has the same shape as above and includes a new refresh_token.

Refresh tokens rotate: each refresh returns a new token and invalidates the old one. Reusing a consumed refresh token is treated as theft and revokes the entire token family. Always persist the newest refresh token and discard the previous.

Error responses

Failures follow OAuth 2.1 — an HTTP error status with a JSON body:

{ "error": "invalid_grant", "error_description": "authorization code expired" }

Common error values: invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope.

Revocation

POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded

RFC 7009 token revocation, for app-side cleanup (e.g. at logout). Authenticate as the client (a secret, or a private_key_jwt assertion whose aud is the token endpoint URL) and pass the token:

Request
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=rt_5b2c...&client_id=app_01H...
Response
200 OK
Cache-Control: no-store

The endpoint returns 200 OK for any valid client — even for an unknown or already-invalid token — with an empty body. This is per RFC 7009: revocation never discloses whether a token existed.

On this page