Token Introspection
The token introspection endpoint allows ZB services to validate access tokens and retrieve the associated metadata, including user identity, scopes, roles, and KYC tier. This is the recommended approach when services cannot verify JWTs locally or need real-time token status.
Introspect a token
Submit a token for validation. If the token is valid and has not expired, the response includes the full set of claims associated with that token. If the token is invalid, expired, or revoked, the response returns { "active": false } with no additional fields.
Request body
- Name
token- Type
- string
- Description
The access token to introspect. This can be either a user token or a client credentials token.
Response fields (active token)
- Name
active- Type
- boolean
- Description
trueif the token is valid and has not expired.
- Name
sub- Type
- string
- Description
The subject identifier. For user tokens this is the user's UUID; for client tokens it is the OAuth client id.
- Name
iss- Type
- string
- Description
The issuer URL of the token (the ZB ID base URL).
- Name
exp- Type
- integer
- Description
Token expiration time as a Unix timestamp.
- Name
iat- Type
- integer
- Description
Token issued-at time as a Unix timestamp.
- Name
scopes- Type
- array
- Description
Permission scopes granted to this token. Present on both token types.
- Name
roles- Type
- array | null
- Description
Roles assigned to the user. Present on user tokens; absent for client tokens.
- Name
kycTier- Type
- string | null
- Description
The user's KYC tier:
NONE,BASIC,STANDARD, orENHANCED. Present on user tokens; absent for client tokens.
- Name
tokenType- Type
- string | null
- Description
clientfor client credentials tokens. Absent (null) for user tokens.
- Name
clientId- Type
- string | null
- Description
The OAuth client id. Present on client tokens only.
- Name
aud- Type
- string | null
- Description
The token audience. Present on user tokens (the
clientIdsupplied at login, orzb-idby default). Absent for client tokens.
- Name
email- Type
- string | null
- Description
The user's email, when the account has one. Present on user tokens only.
- Name
phone- Type
- string | null
- Description
The user's phone, when the account has one. Present on user tokens only.
User tokens do not carry a tokenType value; distinguish them by the absence of tokenType (or by the presence of aud, roles, and kycTier). Client tokens have tokenType: "client" and a clientId.
Request
curl -X POST https://id.zb.co.zw/oauth/introspect \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
Response (active user token)
{
"active": true,
"sub": "550e8400-e29b-41d4-a716-446655440000",
"iss": "https://id.zb.co.zw",
"exp": 1717200000,
"iat": 1717113600,
"scopes": [
"banking:read",
"banking:write",
"profile:read",
"profile:write"
],
"roles": ["merchant"],
"kycTier": "ENHANCED",
"aud": "zb-id",
"email": "[email protected]",
"phone": "+263771234567"
}
Response (invalid/expired token)
{
"active": false
}
Introspect your chained token
Sign in on the Authentication page first. This panel drops that access token into the request body and validates it. With no token it returns active:false; with a valid one it returns the full claims set.
Runs against the ZB ID STAGING sandbox (id-staging.zb.co.zw). Register a throwaway test account; never use real credentials.
Usage patterns
Token introspection is the primary mechanism for ZB services to validate tokens they receive from clients. There are two common patterns depending on your architecture.
Pattern 1: API gateway validation
Place introspection at the API gateway layer so that individual microservices never need to validate tokens themselves.
- Client sends a request with
Authorization: Bearer <token>to the gateway. - The gateway calls
/oauth/introspectto validate the token. - If
activeistrue, the gateway forwards the request to the downstream service, attaching the resolved scopes and user ID as headers. - If
activeisfalse, the gateway returns401 Unauthorizedimmediately.
Pattern 2: Per-service validation
Each microservice validates tokens directly. This is simpler to set up but requires every service to have network access to ZB ID.
- The service receives a request with
Authorization: Bearer <token>. - The service calls
/oauth/introspectto validate the token. - The service checks that the required scopes are present in the response.
- If validation passes, the service processes the request.
Caching introspection results
To reduce latency and load on ZB ID, services may cache introspection results. Use the exp field to set the cache TTL, and never cache results longer than the token's remaining lifetime. Invalidate the cache immediately if you receive a 401 from any downstream call.
Gateway middleware (Python)
import requests
from functools import lru_cache
ZB_ID_URL = 'https://id.zb.co.zw'
def validate_token(token: str) -> dict | None:
"""Validate a token via introspection."""
response = requests.post(
f'{ZB_ID_URL}/oauth/introspect',
json={'token': token}
)
data = response.json()
if data.get('active'):
return data
return None
def require_scope(scope: str):
"""Decorator to enforce a required scope."""
def decorator(func):
def wrapper(request, *args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
claims = validate_token(token)
if not claims:
return Response(status=401)
if scope not in claims.get('scopes', []):
return Response(status=403)
request.user_id = claims['sub']
request.scopes = claims['scopes']
return func(request, *args, **kwargs)
return wrapper
return decorator
@require_scope('banking:read')
def get_account_balance(request, account_id):
# Token is valid and has banking:read scope
...
Express middleware (Node.js)
const ZB_ID_URL =
'https://id.zb.co.zw'
async function validateToken(token) {
const res = await fetch(
`${ZB_ID_URL}/oauth/introspect`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
}
)
const data = await res.json()
return data.active ? data : null
}
function requireScope(scope) {
return async (req, res, next) => {
const token = req.headers.authorization
?.replace('Bearer ', '')
const claims = await validateToken(token)
if (!claims) {
return res.status(401).json({
result: 'failed',
message: 'Invalid or expired token',
})
}
if (!claims.scopes.includes(scope)) {
return res.status(403).json({
result: 'failed',
message: `Missing required scope: ${scope}`,
})
}
req.userId = claims.sub
req.scopes = claims.scopes
next()
}
}
// Usage
app.get(
'/accounts/:id/balance',
requireScope('banking:read'),
(req, res) => {
// Token is valid and has banking:read
}
)
User vs client tokens
ZB ID issues two types of tokens. The tokenType field tells you which one you are looking at: it is client for client credentials tokens and absent for user tokens.
User tokens
Issued when a human user authenticates through the login, register, or refresh flow. These tokens carry:
- sub: The user's UUID
- roles: The user's assigned roles (for example,
["customer"],["admin", "staff"]) - scopes: Permissions derived from the user's roles
- kycTier: The user's KYC verification tier
- aud: The token audience (the
clientIdsupplied at login, orzb-id) - email / phone: Present when the account has that channel
- tokenType: Absent
User tokens represent a specific person and should be used to enforce user-level access controls.
Client tokens (tokenType: "client")
Issued via the OAuth client credentials grant, for service-to-service communication where no user context is needed. They carry:
- sub: The OAuth client id (not a user UUID)
- clientId: The OAuth client id
- scopes: The scopes granted to the client
- tokenType:
client - roles, kycTier, aud, email, phone: Not present
Client tokens represent an application, not a person. They are appropriate for background jobs, central KYC checks, and inter-service API calls.
Distinguishing tokens in your service
Treat a response with tokenType: "client" as a client token, and a response without a tokenType (equivalently, one carrying aud, roles, and kycTier) as a user token. An endpoint that returns user-specific data should reject client tokens, while a batch or KYC endpoint might accept only client tokens.
User token introspection
{
"active": true,
"sub": "550e8400-e29b-41d4-a716-446655440000",
"roles": ["merchant"],
"scopes": [
"banking:read",
"banking:write",
"profile:read",
"profile:write"
],
"kycTier": "ENHANCED",
"aud": "zb-id",
"email": "[email protected]",
"phone": "+263771234567",
"exp": 1717200000,
"iat": 1717113600
}
Client token introspection
{
"active": true,
"sub": "zbid_client_lending_svc",
"tokenType": "client",
"clientId": "zbid_client_lending_svc",
"scopes": [
"kyc:validate",
"kyc:credit-check",
"kyc:screen"
],
"exp": 1717200000,
"iat": 1717196400
}
Security tip: Never assume a token is a user token just because it has scopes. A user token has no tokenType; a client token has tokenType: "client". A client token with profile:read should not be used to fetch a specific user's profile without additional authorization logic.