How to: handle a revoked token

Two different things can end a token's access, and an integration needs to tell them apart: the token itself dying, and one grant it relied on narrowing or ending while the token keeps working for everything else it can still reach.

A grant narrowing: scope_change

When a grant ends, by revocation, relinquishment, or expiry, the change feed carries it as a scope_change entry with revoked_at set (SYNC-02). A consumer learns the narrowing from this entry, never from a row silently disappearing off a later read:

{
  "seq": 5102,
  "op": "scope_change",
  "entity": "grant",
  "entity_id": "99887766554433221100ffeeddccbbaa",
  "person_id": "1a2b3c4d5e6f70819203a4b5c6d7e8f9",
  "origin_consumer_id": "01020304050607080910111213141516",
  "external_ref": null,
  "occurred_at": "2026-08-20T18:00:03.000Z",
  "revoked_at": "2026-08-20T18:00:03.000Z"
}

The token is fine here. It just no longer reaches this one person; every other grant it holds still works, and further polling stays correct. Nothing to re-authenticate.

The token itself: token_revoked

When an operator revokes a token on the team surface, every call the token makes afterward answers 401, distinctly from token_invalid, so an integration can tell "this specific token stopped working" from "this string was never a valid token":

HTTP/1.1 401 Unauthorized

{
  "type": "https://xcathletes.org/docs/errors/token_revoked",
  "title": "Token revoked",
  "status": 401
}

The re-auth path

token_revoked is never recoverable through the API; there is no "renew this token" call a revoked token can make on itself (renewal only extends an unrevoked token's own expiry). A human, your team's operator, signs back in at /team/login if their session has lapsed, mints a fresh token at /team/tokens, and hands it to your integration out of band.

Treat token_revoked as a stop-and-alert-a-human condition, not something to retry. scope_change, on the other hand, needs nothing from a human: apply it like any other feed entry and keep polling.