Skip to content

Introspecting tokens

POST /oauth/introspect/ implements RFC 7662 token introspection. Use it to check, server-side, whether an access token you're holding is still valid, and to read back its scope and expiry — without waiting for a 401 from a resource endpoint to find out.

Client auth

Authenticate as your app, the same way as /oauth/token/ and /oauth/revoke_token/. HTTP Basic is preferred:

Authorization: Basic base64(client_id:client_secret)

Body credentials are a supported fallback (RFC 6749 §2.3.1 shape, "NOT RECOMMENDED" but accepted) for clients that can't send a Basic header:

client_id=<client_id>&client_secret=<client_secret>

alongside token= in the same form-encoded body. A request that fails both — no valid Basic header and no valid body credentials — never reaches token lookup at all; the endpoint answers before that.

Request

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

token=<access_token>

GET /oauth/introspect/?token=<access_token> (with the same Authorization header) works identically — both methods are gated the same way.

curl -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d token="$ACCESS_TOKEN" \
  https://api.lucibook.co.uk/oauth/introspect/
import requests

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

Response

A successfully authenticated call always gets 200, with a JSON body. The only field every one of those responses carries is active:

{"active": true, "scope": "bills:read contacts:read", "exp": 1790186013, "client_id": "..."}

When active is false, that is the entire body — no other keys:

{"active": false}

A call that fails client authentication — bad or missing Basic/body credentials — never reaches token lookup and gets a plain 403 with no body. RFC 7662 §2.3 specifies 401 for invalid client credentials; LuciBook currently returns 403, so handle both as "check your client credentials".

Fields on an active response — django-oauth-toolkit's IntrospectTokenView.get_token_response (oauth2_provider/views/ introspect.py), with username removed by LuciBook (see below):

Field Always present? Meaning
active yes true
scope yes space-separated scope string on the token
exp yes Unix timestamp the token expires at
client_id yes your app's client_id

There is no token_type, sub, or iat field — this endpoint returns exactly the fields above, nothing more.

A refresh-token string passed as token= isn't recognised by this endpoint at all (django-oauth-toolkit's introspection view only ever looks up access tokens) and gets the same {"active": false} as any other unrecognised string.

No username either

Toolkit's own response includes a username field when the token has a resource-owner user attached — on LuciBook that's the user's email address (USERNAME_FIELD = "email"), and no introspection scope grants access to it. LuciBook strips this field from every response, including for your own token.

You can only introspect your own app's tokens

LuciBookIntrospectTokenView (apps/integrations/routers/oauth.py) resolves which application actually authenticated the request, and checks it against the token's owning application before returning anything. A token that exists but belongs to a different application gets exactly the same response as a token that doesn't exist at all:

{"active": false}

— never a 403/404 that would tell you the token exists but isn't yours. This also covers a token that's expired or was revoked: all three cases (foreign, expired, revoked) collapse to the same {"active": false}, by design — RFC 7662 §2.2 never distinguishes them, so this endpoint doesn't either.

Tokens carry no org claim

A LuciBook bearer token is scoped to (application, user), not to a single organisation — the same token can be valid against every org that user has connected your app to. This response never includes org_id or org_name; if you need to know which organisation(s) a token's connection(s) belong to, call:

GET /api/v1/connections
Authorization: Bearer <access token>

See Connections for that endpoint's shape.