Skip to content

Revoking tokens

POST /oauth/revoke_token/ implements RFC 7009 token revocation. Use it to give up an access or refresh token your app no longer needs, and — this is the important case — to force a token out of service immediately after a credential compromise, instead of waiting for it to expire on its own.

Request

POST /oauth/revoke_token/
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

token=<access_or_refresh_token>
token_type_hint=access_token|refresh_token   (optional)

Auth is the same HTTP Basic client_id:client_secret as /oauth/token/. A public client (no secret) sends client_id in the body instead, exactly as it does at the token endpoint.

token_type_hint is an optimisation, not a requirement — omit it and the server tries both token types. Set it when you know which kind you're revoking; it saves a lookup.

curl -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d token="$RT" \
  -d token_type_hint=refresh_token \
  https://api.lucibook.co.uk/oauth/revoke_token/
import requests

resp = requests.post(
    "https://api.lucibook.co.uk/oauth/revoke_token/",
    auth=(client_id, client_secret),
    data={"token": refresh_token, "token_type_hint": "refresh_token"},
    timeout=10,
)

Response

200 on success — including when the token you sent was already invalid, expired, or unknown. This is RFC 7009 §2.2, not a LuciBook peculiarity: the endpoint's job is to guarantee the token is not usable afterwards, and a token that was never usable in the first place already satisfies that. Don't treat a 200 as proof the token existed; treat a non-200 as your own request being malformed (bad client auth, missing token).

What gets revoked

  • Revoking a refresh token revokes it, the access token it is currently paired with, and every active connection it authorised (see below). It does not reach back and revoke earlier refresh tokens from the same rotation lineage — each refresh already invalidated the one before it (see Refresh-token rotation semantics), so by the time you hold the current refresh token there is only one live one to revoke.
  • Revoking an access token revokes only that access token — no connection is touched. Its refresh token, if any, is untouched and can still be used to mint a new access token — call revoke again with that refresh token if you want the whole pair, and the connection(s) it authorised, gone.

Revoking a refresh token also revokes the connection(s)

Unlike an access-token revoke, revoking a refresh token revokes every active connection that (your app, that user) holds. A bearer token here is scoped to (application, user) only — it carries no single org — so the same refresh token is valid against every org that user has connected your app to, and revoking it closes all of them, not just one. Concretely: revoking ONE refresh token revokes ALL of that user's active connections for your app across EVERY org, including connections currently backed by a completely DIFFERENT, still-live token chain of the same (your app, that user) pair — a second device or session that separately authorized the same user against the same app is not exempt. Each connection is stamped revoked_at, revoked_reason is recorded, and connection.revoked fires for every one — identical to what DELETE /api/v1/connections/{connection_id} does for a single connection. This closes the gap where revoking a token alone (the old behaviour) left the connection live, so a leaked client secret could revoke every token it had minted and then silently re-mint new ones against that same still-active connection via a fresh authorization-code or refresh exchange.

Only a token your app actually owns can trigger this: the endpoint checks the submitted token's application against the client authenticated on this request (its own secret, or its own client_id for a public client) and does nothing — no revoke, no cascade, still 200 — for a token that belongs to a different application. A leaked secret lets an attacker revoke tokens/connections for your own app's users, which rotating the secret (My Apps → app settings) stops going forward; it never lets them reach another app's tokens or connections through this endpoint.

Resubmitting the same token does not re-trigger the cascade. The cascade only fires on the call that actually moves a refresh token from live to revoked — a second POST with the same (now already-revoked) token string is still a 200, but it never cascades again, even from the owning client. This matters if a cascade fails partway through: one of this user's connections in a rare case (a database error revoking that one row) can be left live while everything else the token authorised is gone. Retrying the revoke call will not finish the job — the token is already revoked, so there is nothing left for a retry to key off. If you have reason to believe a cascade didn't fully complete (check for connection.revoked events you expected but didn't receive), revoke the specific remaining connection(s) directly instead:

DELETE /api/v1/connections/{connection_id}
Authorization: Bearer <access token>

That call needs a still-valid access token. If you no longer have one, ask the organisation's user to remove your app under Settings → Connected apps, which revokes the connection the same way.

— note this needs a still-live bearer token to authenticate, so do it with a different token/connection you still hold, or ask the user to reconnect and then clean up via Connections.

Revoking an access token does not touch any connection — same as before. It revokes only that access token; its refresh token, if any, can still mint a new access token, and the connection(s) it is scoped to stay active until you revoke the refresh token or the connection(s) directly:

DELETE /api/v1/connections/{connection_id}
Authorization: Bearer <access token>

(see Connections). That call also stamps revoked_at and fires connection.revoked — no read scope is required to subscribe to it, since a connection-scoped event only ever reaches an app that already held the connection. Rotating your client secret (My Apps → app settings) is the separate step for "this app's credential itself was compromised" — token/connection revocation does not touch client_secret.

There is no ordering hazard between the two calls any more: revoking the refresh token now revokes its connection(s) as part of the same call, so you don't need to call DELETE /api/v1/connections/{connection_id} separately just to make sure the connection dies too. The DELETE call still exists for the narrower case where you want to revoke ONE specific connection without touching the user's other tokens or connections for your app.

After revoking

Once you've decided the user needs to reconnect — token revoked, connection revoked, or both — send them back through the authorize flow with a fresh consent: see Register an app and Authorization Code + PKCE. There is no "resume" affordance; a revoked connection is re-established the same way the first one was created.