Token introspection and revocation
Access tokens are signed, so a resource server can verify one offline against the JWKS. That is fast, and it cannot see a decision made after the token was minted: a revoked grant, a killed agent, a password change. Introspection is the call that can. Revocation is how a client ends a grant on purpose.
Both are advertised in the discovery document as
introspection_endpoint and revocation_endpoint.
Introspection — is this token live now?
POST /api/v1/oauth/introspect, form-encoded: token (and an optional token_type_hint, which is
ignored). It answers for access tokens of your own organization.
Who may ask
A resource server of the same organization, holding the authz:check scope — the scope
/authz/service-check already asks of a resource server.
Authenticate in one of two ways:
| Way | Detail |
|---|---|
| A bearer token | Authorization: Bearer <token> (or DPoP) where the token is a client_credentials access token whose scope includes authz:check. Its DPoP binding is honoured, and its client must still be enabled. |
| Your client's credentials | client_secret_basic or client_secret_post of a confidential client that is allowed the authz:check scope and the client_credentials grant. |
Grant the scope by adding authz:check to the client's allowed scopes (Organization → OAuth
Clients). A caller that is not authenticated, has no authz:check, or is a public client gets a
401 or 403 and learns nothing about the token. Failed secrets count against the same
brake as the token endpoint (429 with Retry-After).
The answer
A token that is live for your organization (aud is zynth-services for a platform token, or
your resource identifier for one minted for your API):
{
"active": true,
"scope": "openid email",
"client_id": "zc_…",
"sub": "5f1c…",
"tid": "9a07…",
"aud": "zynth-services",
"iss": "zynth-auth",
"exp": 1790000000,
"iat": 1789999100,
"token_type": "Bearer"
}
token_type is DPoP for a sender-constrained token, with
cnf: {"jkt": "…"} (RFC 9449 §6.2); it never claims Bearer for a bound token.
isshere is the access token's issuer (zynth-auth), not the OIDC issuer in the discovery document, which belongs to id_tokens. They are different documents; see whichissto expect. Do not feed this value to a verifier configured from discovery.
Everything else is exactly this, and nothing more:
{ "active": false }
active: false carries no reason, deliberately — the same shape for every token that is not
live for your organization:
- a bad signature, an expired token, a refresh token (a resource server never holds one), garbage;
- a token whose audience is neither the platform nor a resource registered in the token's organization;
- a token of another organization;
- a revoked session, or a person who no longer stands — membership ended, status changed, password changed, sessions revoked everywhere;
- a disabled or deleted issuing client;
- an agent that is retired, killed, or whose containment epoch is later than the token.
Liveness is decided by the same checks the platform enforces its own API with, so a token this call
calls active is one the platform would honour at that instant. That includes the
agent kill switch: a contained
agent's token still verifies offline until it expires, and introspection answers active: false at
once.
Using it well
- An
active: trueanswer is true at the instant it was given, not for the token's lifetime. Cache it for seconds, not minutes, and never longer than the token'sexp. - Rate limits. Introspection has its own per-client allowance, resolved from your plan: free 600,
pro 6,000 and enterprise 60,000 requests per 5-minute window, negotiable per organization. It applies
whichever way you authenticate (a
client_credentialsbearer token or the client's secret), so switching between them does not buy more, and introspecting does not spend the client's/tokenallowance, nor the reverse. Past the allowance you get429withRetry-After; the limit fails closed. A resource server that introspects at volume should cache each answer for a few seconds (see above) and verify offline in between. - Introspecting every request makes Zynth's availability part of yours. A common pattern: verify offline on every request, introspect on a short timer or before a sensitive action.
- Do not decode the access token for its claims; take them from the introspection response. Access token claims are not part of the contract.
curl -s https://<issuer>/api/v1/oauth/introspect \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$ACCESS_TOKEN"
Revocation — a client ends its own grant
POST /api/v1/oauth/revoke, form-encoded: token and an optional token_type_hint (ignored). The
client authenticates exactly as at the token endpoint: a confidential client with its secret, a
public client by naming its client_id.
| You send | What happens |
|---|---|
| A refresh token or a user access token issued to your client | The grant's session is revoked. Every refresh and access token of that grant stops at once — at the platform API, at /userinfo, and at introspection. 200 with {}. |
A client_credentials access token | 400 unsupported_token_type. There is no session to end, and a 200 would claim a revocation that did not happen. Disable the client to stop that token. |
| A token that is invalid, expired, unknown, or issued to another client | 200 with {}, and nothing is done. |
The last row is a deliberate difference from the letter of RFC 7009 §2.1, which says another client's token is "refused". Answering it differently from garbage would tell a client that a token it should not hold is real, so every such request gets the same answer.
Revocation ends the tokens, not the person's consent: the application stays under Account → Connected apps until they disconnect it, so a later sign-in that asks for the same scopes does not show the consent screen again.
Which to use
| You want to… | Use |
|---|---|
| Stop one signed-in person's session at an application | Revocation from that application |
| Know, at a resource server, that a token has been cut off | Introspection |
| Stop an agent everywhere, immediately | POST /api/v1/agents/{agent_id}/kill-switch — introspection then reports its tokens inactive |
Stop a client_credentials token | Disable the client, or let the 15-minute token expire |