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.
These docs are interactive. The panels below run live against the ZB ID staging sandbox. Register a throwaway account, then sign in, and the access token carries across the token-scoped panels on this and other pages.
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,
ACTIVEOAuth client. See Token audience.
- Name
channel- Type
- string
- Description
Optional. A free-form label for the origin of the request (for example,
webormobile). Recorded on the session and in the audit log.
Behaviour
- At least one of
emailorphoneis required. If both are omitted the API returns400with the messageEither an email or a phone number is required. - If the email is already registered, the API returns
409withEmail already registered, log in with that email instead. - If the phone is already registered, the API returns
409withPhone number already registered, log in with that phone instead. - On success the response is
201 Createdand contains the token pair and a user summary.
Request
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"]
}
}
You can register with just an email or just a phone. Supply whichever channels you want the user to log in with; either one on its own is enough.
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.
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
- The client sends
POST /auth/loginwithemail(orphone) andpassword. - The API validates the credentials and returns an access token and a refresh token.
- The client includes the access token in the
Authorization: Bearer <token>header on every subsequent request. - When the access token expires, the client sends the refresh token to
POST /auth/token/refreshto 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,
ACTIVEOAuth 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
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"]
}
}
Logging in with the phone is identical: send { "phone": "+263771234567", "password": "..." }. Whichever channel you use, the token's sub claim is the same user UUID.
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.
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
clientIdis supplied it must reference an existing OAuth client whose status isACTIVE. An unknown or inactive client id returns400with the messageUnknown clientId. Validation happens before any account is created, so a badclientIdnever leaves a half-created user behind. - When
clientIdis omitted, the token'sauddefaults tozb-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
audclaim 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"
}
Register the client through the OAuth Clients API before referencing it here. Tokens issued without a clientId carry "aud": "zb-id".
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.
After 5 failed login attempts the account is locked for 30 minutes. Suspended and deactivated accounts also fail login with 403 ACCOUNT_LOCKED.
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"
}
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
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"
}
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
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
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."
}
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
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"
}
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
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
- Register an OAuth client through the OAuth Clients API to receive a
client_idandclient_secret. - Send a form-encoded
POSTto/oauth/tokenwithgrant_type=client_credentialsand your credentials, either as form fields or via a BasicAuthorizationheader. - The API returns an access token scoped to the permissions granted to your client.
- 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
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"
}
Requested scopes are intersected with the scopes granted to your client, so the returned scope can be narrower than what you asked for. Cache the token and request a new one before it expires rather than on every call.
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:
| Claim | Type | Description |
|---|---|---|
sub | string | The user's UUID |
iss | string | Token issuer (the ZB ID issuer identifier, https://id.zb.co.zw) |
aud | string | Token audience: the clientId supplied at login, or zb-id by default |
iat | number | Issued-at time as a Unix timestamp |
exp | number | Expiration time as a Unix timestamp |
jti | string | Unique token identifier for revocation tracking |
phone | string | The user's phone number. Present only when the account has a phone |
email | string | The user's email. Present only when the account has an email |
roles | array | Roles assigned to the user (for example, ["customer"]) |
scopes | array | Permission scopes derived from the user's roles |
kyc_tier | string | The user's KYC tier: NONE, BASIC, STANDARD, or ENHANCED |
Client token claims
Tokens issued through POST /oauth/token with grant_type=client_credentials contain:
| Claim | Type | Description |
|---|---|---|
sub | string | The OAuth client id |
iss | string | Token issuer (the ZB ID issuer identifier, https://id.zb.co.zw) |
iat | number | Issued-at time as a Unix timestamp |
exp | number | Expiration time as a Unix timestamp |
jti | string | Unique token identifier |
token_type | string | Always client for client tokens |
client_id | string | The OAuth client id |
scopes | array | Permission 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"]
}
Never trust decoded token claims on the client side for authorization decisions. Validate tokens server-side by verifying the JWT signature against the JWKS or by calling the /oauth/introspect endpoint.