Need to manage API keys, view logs, or check usage?Open the Developer portal →

Revoking access

How a grant ends, how to end one yourself, and the two ways your access can narrow without anyone telling you.

Circa's tokens are opaque and stored server-side, which means revocation is immediate — there is no signed token floating around that stays valid until it expires. The next request after a revocation fails.

Three ways access ends

  1. The user revokes you. Every Circa user has an authorized-apps list in their profile menushowing each app's logo, name, scopes, granted teams, when it was granted and when it was last used, with a revoke button. Revoking the grant cascades to every access and refresh token issued from it.
  2. You revoke yourself, by calling POST /public/oauth/revoke. Do this when a user disconnects Circa from inside your product — leaving a live grant behind after your user thinks they disconnected is a bad look and an unnecessary liability.
  3. Circa suspends the app. An app moved to suspendedstops working for all users. This is a platform-safety action; you will hear from Circa, and the reason appears in the app's review history in the Developer portal.

Revoking a token yourself

POST/public/oauth/revoke

An RFC 7009 revocation endpoint. Authenticate your app the same two ways as the token endpoint (HTTP Basic or client_secret in the body) and post the token you want dead as application/x-www-form-urlencoded.

ParameterTypeDescription
tokenrequiredstringThe token to revoke — either a circa_oat_… access token or a circa_ort_… refresh token.
token_type_hintstringOptional hint: access_token or refresh_token. Circa can tell from the prefix, so this is only a lookup optimization.
client_idrequiredstringYour circa_cid_…. May be omitted from the body if you use HTTP Basic.
client_secretstringYour circa_cs_…, unless you use HTTP Basic.
curl -s -X POST 'https://api.circa.ai/public/oauth/revoke' \
  -u "$CIRCA_CLIENT_ID:$CIRCA_CLIENT_SECRET" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'token=circa_ort_3fA6cHu8Zw5nQ2vJ0sYb4pR1tK7mLxD' \
  -d 'token_type_hint=refresh_token'
ℹ
Any known token returns 200 and an empty body
As RFC 7009 requires, the endpoint responds 200 with {} whether the token was live, already revoked, expired, or never existed — so it cannot be used to probe which token values are valid. Do not branch on the response. Fire it, then delete your stored tokens either way, and make the call idempotent on your side.

Two things still fail loudly, because they are your bugs rather than the token's state: omitting token is a 400 with { error: "invalid_request", error_description: "Missing required parameter 'token'." }, and bad client credentials are a 401 with invalid_client — the same OAuth2 error shape the token endpoint uses.

Revoking a refresh token takes the whole grant with it — every access and refresh token issued under it — so a single call is enough to disconnect a user cleanly. Revoking an access tokenkills only that one token; the grant and the refresh token survive, and your next refresh will hand you a working access token again. Revoke the refresh token when you mean "disconnect".

Lookups are scoped to the app you authenticated as, so a token issued to a different client is treated as unknown and returns 200 without being revoked.

The two narrowing cases you will hit

These are the failures that surprise integrations, because in both the token itself is still perfectly valid — nothing told you anything changed until a call failed.

1. The user left a granted team → 403 for that team only

A grant records the teams the user consented to. But Circa also checks, on every single request, that the granting user is still an active member of the team in x-team-id. A token must never outlive the membership that authorized it.

So when someone leaves a team — resigns, is removed by an admin, changes employer — the team silently drops out of your effective access. Same token, same grant, same scopes; that one team now returns 403 while every other granted team keeps working.

403Forbidden
{
  "statusCode": 403,
  "message": "The user who authorized 'Acme LCA' is no longer a member of team '4b6f9c28-6f4e-4e7f-a36d-2ff989045d10'."
}

A team that was never in the grant in the first place is also a 403, with a different message — 'Acme LCA' was not granted access to team '…'. Both mean the same thing for your code: that team is not available.

Handle it as a per-team condition, not a per-connection one:

  • Do not log the user out or throw the token away — the other teams are fine.
  • Mark that team as unavailable in your UI and stop scheduling work against it.
  • Re-read GET /public/teams (requires teams:read) to get the current set rather than trusting the list you cached at connection time.
  • If they need that team back, send them through the authorization flow again once their membership is restored.

2. The grant is gone → 401 everywhere

If the user revoked you, you revoked yourself, reuse detection fired, or the app was suspended, the token is dead for all teams:

401Unauthorized
{
  "error": "invalid_token",
  "statusCode": 401,
  "message": "The authorization behind this token has been revoked."
}

Every 401 on a /public/* call carries error: "invalid_token"; the message says which case it is — The access token is invalid., The access token has expired., The access token has been revoked., The authorization behind this token has been revoked. or This application has been suspended.

Expiry is the ordinary case — the access token simply aged past its 8 hours. Do not parse the message to tell them apart. On 401, try exactly one refresh.

  • Refresh succeeds → it was expiry. Retry the call with the new token.
  • Refresh fails with invalid_grant → the grant is gone. Stop. Mark the connection disconnected, drop the stored tokens, and show a reconnect prompt. Do not retry in a loop; nothing about it is transient.
⚠
Distinguish the two codes carefully. 403 means this team — narrow your scope of work and keep going. 401 means this token — refresh once, then treat it as disconnected. Conflating them either logs users out for no reason or keeps a dead integration retrying forever.

Disconnecting well

  • Give users a disconnect button in your product, and make it call the revoke endpoint rather than just forgetting the tokens locally.
  • Delete stored access and refresh tokens as soon as a grant is dead. They are credentials with no remaining value.
  • Expect to be revoked at any moment. Every scheduled job should tolerate its connection disappearing between runs.
  • When you reconnect a user, request the complete set of scopes and teams you need — a new consent replaces the old one rather than merging with it.