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.


POST/oauth/introspect

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

    true if 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, or ENHANCED. Present on user tokens; absent for client tokens.

  • Name
    tokenType
    Type
    string | null
    Description

    client for 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 clientId supplied at login, or zb-id by 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.

Request

POST
/oauth/introspect
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
}
POST/oauth/introspectTry it

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.

No token yet. Sign in above, or paste one below.
Query parameters
Request body (JSON)

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.

  1. Client sends a request with Authorization: Bearer <token> to the gateway.
  2. The gateway calls /oauth/introspect to validate the token.
  3. If active is true, the gateway forwards the request to the downstream service, attaching the resolved scopes and user ID as headers.
  4. If active is false, the gateway returns 401 Unauthorized immediately.

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.

  1. The service receives a request with Authorization: Bearer <token>.
  2. The service calls /oauth/introspect to validate the token.
  3. The service checks that the required scopes are present in the response.
  4. 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 clientId supplied at login, or zb-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
}

Was this page helpful?