Authentication

ZB ID supports two authentication models: user authentication for end-user applications and OAuth2 client credentials for service-to-service integrations. A user identity is a single account (identified by a UUID) that can carry an email, a phone number, or both. Email and phone are interchangeable login channels bound to that one identity. This guide covers registration, login, token audience, password management, and the structure of issued tokens.


POST/auth/register

Registration

Create a new user account. You must supply at least one login channel, an email address, a phone number, or both, together with a password. The account is created with the default customer role and a KYC tier of NONE.

Attributes

  • Name
    email
    Type
    string
    Description

    The user's email address. Optional if a phone number is supplied. Stored lowercased and trimmed, so login is case-insensitive.

  • Name
    phone
    Type
    string
    Description

    The user's phone number. Optional if an email is supplied. Must match ^\+?[0-9]{10,15}$ (for example, +263771234567).

  • Name
    password
    Type
    string
    Description

    The account password. Must be between 8 and 128 characters.

  • Name
    firstName
    Type
    string
    Description

    Optional. The user's first name.

  • Name
    lastName
    Type
    string
    Description

    Optional. The user's last name.

  • Name
    clientId
    Type
    string
    Description

    Optional. The OAuth client requesting the token. When supplied it must reference an existing, ACTIVE OAuth client. See Token audience.

  • Name
    channel
    Type
    string
    Description

    Optional. A free-form label for the origin of the request (for example, web or mobile). Recorded on the session and in the audit log.

Behaviour

  • At least one of email or phone is required. If both are omitted the API returns 400 with the message Either an email or a phone number is required.
  • If the email is already registered, the API returns 409 with Email already registered, log in with that email instead.
  • If the phone is already registered, the API returns 409 with Phone number already registered, log in with that phone instead.
  • On success the response is 201 Created and contains the token pair and a user summary.

Request

POST
/auth/register
curl -X POST https://id.zb.co.zw/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "phone": "+263771234567",
    "password": "secureP@ss1",
    "firstName": "Tatenda",
    "lastName": "Moyo"
  }'

Response (201)

{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "sT9kQ2mVr8...",
  "tokenType": "Bearer",
  "expiresIn": 900,
  "user": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "phone": "+263771234567",
    "email": "[email protected]",
    "firstName": "Tatenda",
    "lastName": "Moyo",
    "kycTier": "NONE",
    "roles": ["customer"]
  }
}
POST/auth/registerTry it

Register a throwaway test account

Runs live against staging. Use a made-up email like [email protected]; the response includes an access token that the token-scoped panels on other pages will reuse.

Runs against the ZB ID STAGING sandbox (id-staging.zb.co.zw). Register a throwaway test account; never use real credentials.

Query parameters
Request body (JSON)

POST/auth/login

User login

Authenticate with either an email or a phone number, plus the account password. Email lookups are case-insensitive and trimmed, so [email protected] resolves to the same identity as [email protected]. On success the API returns a new access and refresh token pair.

Authentication flow

  1. The client sends POST /auth/login with email (or phone) and password.
  2. The API validates the credentials and returns an access token and a refresh token.
  3. The client includes the access token in the Authorization: Bearer <token> header on every subsequent request.
  4. When the access token expires, the client sends the refresh token to POST /auth/token/refresh to obtain a new token pair.

Attributes

  • Name
    email
    Type
    string
    Description

    The email registered on the account. Supply either this or phone.

  • Name
    phone
    Type
    string
    Description

    The phone number registered on the account. Supply either this or email.

  • Name
    password
    Type
    string
    Description

    The account password.

  • Name
    clientId
    Type
    string
    Description

    Optional. The OAuth client requesting the token. When supplied it must reference an existing, ACTIVE OAuth client. See Token audience.

  • Name
    channel
    Type
    string
    Description

    Optional. A free-form label for the origin of the request.

When both email and phone are supplied, the email is used to resolve the account. Invalid credentials return 401 INVALID_CREDENTIALS with the message Invalid credentials, regardless of whether the account exists.

Request

POST
/auth/login
curl -X POST https://id.zb.co.zw/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "secureP@ss1"
  }'

Response

{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "sT9kQ2mVr8...",
  "tokenType": "Bearer",
  "expiresIn": 900,
  "user": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "phone": "+263771234567",
    "email": "[email protected]",
    "firstName": "Tatenda",
    "lastName": "Moyo",
    "kycTier": "BASIC",
    "roles": ["customer"]
  }
}
POST/auth/loginTry it

Sign in and seed a token

Sign in with the account you registered above. A successful login captures the access token so the token-scoped panels (current user, sessions, introspection) can chain from it automatically.

Runs against the ZB ID STAGING sandbox (id-staging.zb.co.zw). Register a throwaway test account; never use real credentials.

Query parameters
Request body (JSON)

Token audience

Both /auth/login and /auth/register accept an optional clientId. This binds the issued access token to a specific OAuth client through the JWT aud (audience) claim, which lets downstream services confirm a token was minted for them.

  • When clientId is supplied it must reference an existing OAuth client whose status is ACTIVE. An unknown or inactive client id returns 400 with the message Unknown clientId. Validation happens before any account is created, so a bad clientId never leaves a half-created user behind.
  • When clientId is omitted, the token's aud defaults to zb-id.
  • The resolved audience is captured on the session at login and is preserved across refresh token rotation, so a later change to the client does not break an active session's refresh.
  • The aud claim serialises as a plain JSON string (not an array).

Login with a clientId

{
  "email": "[email protected]",
  "password": "secureP@ss1",
  "clientId": "zbid_client_a1b2c3d4e5f6"
}

Resulting token aud claim

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "aud": "zbid_client_a1b2c3d4e5f6",
  "iss": "https://id.zb.co.zw"
}

Account lockout

To protect against brute-force attacks, ZB ID locks an account after repeated failed login attempts.

  • Threshold: 5 consecutive failed attempts.
  • Lockout duration: 30 minutes.
  • Reset: The failed-attempt counter resets after a successful login, and the lock clears automatically once the lockout window expires.

Login attempts are rate limited per identifier (the email or phone submitted), not per source IP address. While an account is locked, login requests for that identity return 403 ACCOUNT_LOCKED regardless of whether the password is correct.


Session management

ZB ID limits the number of concurrent active sessions per user account.

  • Maximum concurrent sessions: 3.
  • When a user logs in and already has 3 active sessions, the oldest session is automatically revoked.
  • Users can view and revoke individual sessions through the Sessions endpoints, or revoke every session at once with POST /auth/logout/all.

Each session records the channel, IP address, user agent, and the resolved token audience (clientId) captured at login.

Logout response

{
  "message": "Logged out successfully"
}

Logout all response

{
  "message": "All sessions revoked"
}

POST/auth/password/change

Password management

Authenticated users can change their password by providing their current password alongside the new one. This endpoint requires a valid access token in the Authorization header.

Required attributes

  • Name
    currentPassword
    Type
    string
    Description

    The user's current password. This is verified before the change is applied.

  • Name
    newPassword
    Type
    string
    Description

    The new password. Must be between 8 and 128 characters, and different from the current password.

After a successful password change, all of the user's active sessions are revoked. The user must log in again on other devices.

Request

POST
/auth/password/change
curl -X POST https://id.zb.co.zw/auth/password/change \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "currentPassword": "secureP@ss1",
    "newPassword": "newSecureP@ss2"
  }'

Response

{
  "message": "Password changed successfully"
}

POST/auth/password/forgot

Password reset

Users who have forgotten their password use a two-step, email-based reset: first request a reset link, then submit the emailed token with the new password. Both endpoints are public (no access token required).

Step 1: request a reset link

POST /auth/password/forgot emails a reset link to the account if one exists. To avoid revealing whether an email is registered, this endpoint always returns 200 with the same message whether or not the account exists.

  • Name
    email
    Type
    string
    Description

    The email address to send the reset link to. Required.

  • Name
    clientId
    Type
    string
    Description

    Optional. Selects which app's branding and reset-link base the email uses. Defaults to product-intel.

Step 2: reset the password

The emailed link carries a single-use, time-limited token. Submit it with the new password to POST /auth/password/reset.

  • Name
    token
    Type
    string
    Description

    The reset token from the emailed link. Required.

  • Name
    newPassword
    Type
    string
    Description

    The new password. Must be between 8 and 128 characters. Required.

Reset-link requests are rate limited to 5 per hour per IP address.

Step 1: request link

POST
/auth/password/forgot
curl -X POST https://id.zb.co.zw/auth/password/forgot \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]"
  }'

Response (always 200)

{
  "message": "If an account exists, a reset link has been sent."
}

Step 2: reset password

POST
/auth/password/reset
curl -X POST https://id.zb.co.zw/auth/password/reset \
  -H "Content-Type: application/json" \
  -d '{
    "token": "b7f3c9e1-2a4d-4c8e-9f1a-6d2e5b8c7a30",
    "newPassword": "newSecureP@ss2"
  }'

Response

{
  "message": "Password reset successfully."
}

POST/auth/deactivate

Deactivating an account

An authenticated user can deactivate their own account. This requires a valid access token and re-confirmation of the account password. A deactivated account can no longer log in (login returns 403 ACCOUNT_LOCKED).

Required attributes

  • Name
    password
    Type
    string
    Description

    The account's current password, re-entered to confirm the action. Required.

Request

POST
/auth/deactivate
curl -X POST https://id.zb.co.zw/auth/deactivate \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "password": "secureP@ss1"
  }'

Response

{
  "message": "Account deactivated"
}

POST/auth/token/refresh

Refreshing tokens

Access tokens are short-lived. When one expires, exchange the refresh token for a fresh access and refresh token pair without asking the user to log in again.

Required attributes

  • Name
    refreshToken
    Type
    string
    Description

    The refresh token from the most recent login or refresh call.

ZB ID uses refresh token rotation. Each refresh issues a new access token and a new refresh token, and the old refresh token is immediately invalidated. If a previously used refresh token is submitted again, every session for that user is revoked as a replay-detection measure. Always store and use the latest refresh token.

The token audience (aud) resolved at login is carried through every rotation, so refreshed tokens keep the same audience without re-validating the client.

Request

POST
/auth/token/refresh
curl -X POST https://id.zb.co.zw/auth/token/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "sT9kQ2mVr8..."
  }'

Response

{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "b4Xr7pW1nZ...",
  "tokenType": "Bearer",
  "expiresIn": 900,
  "user": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "phone": "+263771234567",
    "email": "[email protected]",
    "firstName": "Tatenda",
    "lastName": "Moyo",
    "kycTier": "BASIC",
    "roles": ["customer"]
  }
}

OAuth2 client credentials

For service-to-service communication where no user context is needed, ZB ID supports the OAuth2 client credentials grant as defined in RFC 6749 Section 4.4.

How it works

  1. Register an OAuth client through the OAuth Clients API to receive a client_id and client_secret.
  2. Send a form-encoded POST to /oauth/token with grant_type=client_credentials and your credentials, either as form fields or via a Basic Authorization header.
  3. The API returns an access token scoped to the permissions granted to your client.
  4. Include the token in the Authorization: Bearer <token> header when calling other ZB ID endpoints.

When to use client credentials

  • Backend services that need to introspect tokens or look up user data.
  • Microservices communicating within the ZB platform.
  • Automated systems (cron jobs, data pipelines) that operate without a user session.
  • Central KYC checks (see the KYC endpoints).

Client credentials tokens do not carry a user identity. They are associated with the client application itself and carry only the scopes granted to that client.

Request

POST
/oauth/token
curl -X POST https://id.zb.co.zw/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=zbid_client_a1b2c3d4e5f6" \
  -d "client_secret=your_client_secret" \
  -d "scope=kyc:validate kyc:screen"

Response

{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tokenType": "Bearer",
  "expiresIn": 900,
  "scope": "kyc:validate kyc:screen"
}

Token structure

ZB ID issues RS256-signed JSON Web Tokens for both flows. The claim set differs depending on how the token was issued.

User token claims

Tokens issued through POST /auth/login, POST /auth/register, and POST /auth/token/refresh contain:

ClaimTypeDescription
substringThe user's UUID
issstringToken issuer (the ZB ID issuer identifier, https://id.zb.co.zw)
audstringToken audience: the clientId supplied at login, or zb-id by default
iatnumberIssued-at time as a Unix timestamp
expnumberExpiration time as a Unix timestamp
jtistringUnique token identifier for revocation tracking
phonestringThe user's phone number. Present only when the account has a phone
emailstringThe user's email. Present only when the account has an email
rolesarrayRoles assigned to the user (for example, ["customer"])
scopesarrayPermission scopes derived from the user's roles
kyc_tierstringThe user's KYC tier: NONE, BASIC, STANDARD, or ENHANCED

Client token claims

Tokens issued through POST /oauth/token with grant_type=client_credentials contain:

ClaimTypeDescription
substringThe OAuth client id
issstringToken issuer (the ZB ID issuer identifier, https://id.zb.co.zw)
iatnumberIssued-at time as a Unix timestamp
expnumberExpiration time as a Unix timestamp
jtistringUnique token identifier
token_typestringAlways client for client tokens
client_idstringThe OAuth client id
scopesarrayPermission scopes granted to the client

Client tokens do not carry an aud, roles, kyc_tier, phone, or email claim.

Decoded user token payload

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "iss": "https://id.zb.co.zw",
  "aud": "zb-id",
  "iat": 1717148800,
  "exp": 1717149700,
  "jti": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
  "phone": "+263771234567",
  "email": "[email protected]",
  "roles": ["customer"],
  "scopes": ["profile:read", "profile:write", "banking:read"],
  "kyc_tier": "BASIC"
}

Decoded client token payload

{
  "sub": "zbid_client_a1b2c3d4e5f6",
  "iss": "https://id.zb.co.zw",
  "iat": 1717148800,
  "exp": 1717149700,
  "jti": "c1d2e3f4-a5b6-0000-1111-222233334444",
  "token_type": "client",
  "client_id": "zbid_client_a1b2c3d4e5f6",
  "scopes": ["kyc:validate", "kyc:screen"]
}

Was this page helpful?