KYC Verification
ZB ID provides a central KYC module that other ZB services call to validate national IDs against the e-Gov registry, run FCB credit bureau searches, and screen individuals and organisations against World-Check AML watchlists. Every verification is persisted as an auditable record.
All KYC endpoints are service-to-service. Call them with a client credentials Bearer token that carries the required scope for the endpoint. See OAuth2 client credentials for how to obtain one, and Roles & Permissions for the KYC scopes.
Overview
The KYC endpoints live under /v1/kyc. Each one requires a specific scope on the calling client's token:
| Endpoint | Method | Scope |
|---|---|---|
/v1/kyc/validate-id | POST | kyc:validate |
/v1/kyc/credit-check | POST | kyc:credit-check |
/v1/kyc/screen-individual | POST | kyc:screen |
/v1/kyc/screen-corporate | POST | kyc:screen |
/v1/kyc/screen-result | POST | kyc:screen |
/v1/kyc/screen-toolkit | GET | kyc:screen |
/v1/kyc/screen-resolve | POST | kyc:screen |
/v1/kyc/verifications/{id} | GET | kyc:screen |
/v1/kyc/verifications | GET | kyc:screen |
Two behaviours reduce duplicate provider calls:
- National ID validation is cached for 30 days per national ID. A repeat validation within that window returns the stored record with
cacheHit: true. - Watchlist screening is deduplicated. An individual is matched by national ID or by a normalized
fullNameplus date of birth; an organisation by its normalized name. A matching prior case is reused withcacheHit: trueinstead of opening a new one.
Credit checks are never cached; each request calls the bureau fresh.
In non-production environments the upstream providers are stubbed, so these endpoints return deterministic sample data without contacting the real registries. Behaviour, request shapes, and response shapes are identical to production.
The verification object
Most KYC endpoints return a verification object describing the persisted record.
- Name
id- Type
- string (UUID)
- Description
Unique identifier for this verification record.
- Name
userId- Type
- string (UUID) | null
- Description
The linked user, if one was supplied.
nullfor pre-registration checks.
- Name
nationalId- Type
- string | null
- Description
The national ID for individuals, or the normalized identity key used for watchlist deduplication.
- Name
type- Type
- string
- Description
One of
EGOV,FCB, orWORLDCHECK.
- Name
status- Type
- string
- Description
One of
PENDING,VERIFIED,REJECTED,COMPLETED, orFAILED.
- Name
statusReason- Type
- string | null
- Description
A human-readable reason when the verification was rejected or failed.
- Name
externalRef- Type
- string | null
- Description
The provider's reference for a watchlist case, when one exists.
- Name
cacheHit- Type
- boolean
- Description
truewhen the record was served from cache or reused via deduplication rather than a fresh provider call.
- Name
requestSnapshot- Type
- object | null
- Description
The request payload sent to the provider, minus any injected credentials.
- Name
responsePayload- Type
- object | null
- Description
The provider's response, as stored.
- Name
createdAt- Type
- string (ISO 8601)
- Description
When the record was created.
Verification object
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"nationalId": "63-123456A78",
"type": "EGOV",
"status": "VERIFIED",
"statusReason": null,
"externalRef": null,
"cacheHit": false,
"requestSnapshot": {
"nationalId": "63-123456A78"
},
"responsePayload": {
"Status": "A"
},
"createdAt": "2026-07-10T09:12:00Z"
}
Validate a national ID
Validate a national ID against the e-Gov registry. Requires the kyc:validate scope. Results are cached for 30 days per national ID. When the ID is active and the linked user is currently at KYC tier NONE, the user's tier is raised to BASIC.
Request body
- Name
nationalId- Type
- string
- Description
The national ID to validate. Up to 20 characters.
- Name
userId- Type
- string (UUID)
- Description
Optional. Link this verification to an existing user so a successful check can raise their KYC tier.
An active ID returns status: "VERIFIED". An ID belonging to a deceased person, or one that is not active in the registry, returns status: "REJECTED" with a statusReason.
Request
curl -X POST https://id.zb.co.zw/v1/kyc/validate-id \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"nationalId": "63-123456A78",
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}'
Response
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"nationalId": "63-123456A78",
"type": "EGOV",
"status": "VERIFIED",
"statusReason": null,
"externalRef": null,
"cacheHit": false,
"requestSnapshot": { "nationalId": "63-123456A78" },
"responsePayload": { "Status": "A" },
"createdAt": "2026-07-10T09:12:00Z"
}
Run a credit check
Run an FCB credit bureau search. Requires the kyc:credit-check scope. This endpoint always calls the bureau fresh; results are never cached. Bureau credentials are injected server-side and are never part of the request or the stored snapshot.
Request body
- Name
dob- Type
- string
- Description
Date of birth,
YYYY-MM-DD.
- Name
names- Type
- string
- Description
The subject's given names.
- Name
surname- Type
- string
- Description
The subject's surname.
- Name
nationalId- Type
- string
- Description
National ID, up to 20 characters.
- Name
gender- Type
- string
- Description
MorF.
- Name
searchPurpose- Type
- string
- Description
The reason for the search.
- Name
married- Type
- string
- Description
Marital status.
- Name
nationality- Type
- string
- Description
Nationality.
- Name
streetno- Type
- string
- Description
Street number.
- Name
streetname- Type
- string
- Description
Street name.
- Name
suburb- Type
- string
- Description
Suburb.
- Name
city- Type
- string
- Description
City.
- Name
mobile- Type
- string
- Description
Mobile number.
- Name
propertyDensity- Type
- string
- Description
Property density.
- Name
propertyStatus- Type
- string
- Description
Property status.
- Name
occupationClass- Type
- string
- Description
Occupation class.
- Name
employer- Type
- string
- Description
Employer.
- Name
employerIndustry- Type
- string
- Description
Employer industry.
- Name
salaryBand- Type
- string
- Description
Salary band.
- Name
loanPurpose- Type
- string
- Description
Loan purpose.
- Name
loanAmount- Type
- string
- Description
Loan amount.
- Name
driversLicence- Type
- string
- Description
Optional. Driver's licence number.
- Name
passport- Type
- string
- Description
Optional. Passport number.
- Name
building- Type
- string
- Description
Optional. Building name.
- Name
pbag- Type
- string
- Description
Optional. Private bag.
- Name
telephone- Type
- string
- Description
Optional. Landline number.
- Name
indEmail- Type
- string
- Description
Optional. The subject's email address.
- Name
userId- Type
- string (UUID)
- Description
Optional. Link the verification to an existing user.
A successful bureau response returns status: "COMPLETED"; any other bureau code, or a provider error, returns status: "FAILED".
Request
curl -X POST https://id.zb.co.zw/v1/kyc/credit-check \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"dob": "1990-01-15",
"names": "John",
"surname": "Moyo",
"nationalId": "63-123456A78",
"gender": "M",
"searchPurpose": "1",
"married": "1",
"nationality": "1",
"streetno": "12",
"streetname": "Samora Machel Ave",
"suburb": "Avondale",
"city": "Harare",
"mobile": "+263771234567",
"propertyDensity": "1",
"propertyStatus": "1",
"occupationClass": "1",
"employer": "Example Ltd",
"employerIndustry": "1",
"salaryBand": "3",
"loanPurpose": "1",
"loanAmount": "5000"
}'
Response
{
"id": "0d3f5a9e-1c22-4b7a-9d2e-7a6f8b1c2d34",
"userId": null,
"nationalId": "63-123456A78",
"type": "FCB",
"status": "COMPLETED",
"statusReason": null,
"externalRef": null,
"cacheHit": false,
"createdAt": "2026-07-10T09:20:00Z"
}
Screen an individual
Open a World-Check AML screening case for an individual. Requires the kyc:screen scope. If a case already exists for the same national ID, or the same normalized fullName plus date of birth, that case is reused (cacheHit: true).
Request body
- Name
fullName- Type
- string
- Description
The person's full name. Up to 80 characters.
- Name
dateOfBirth- Type
- string
- Description
Date of birth,
YYYY-MM-DD.
- Name
gender- Type
- string
- Description
MALEorFEMALE.
- Name
nationality- Type
- string
- Description
ISO 3166-1 alpha-2 country code (for example,
ZW).
- Name
nationalId- Type
- string
- Description
Optional. National ID, up to 20 characters. Improves deduplication.
- Name
userId- Type
- string (UUID)
- Description
Optional. Link the verification to an existing user.
A newly opened case is returned with status: "PENDING" while screening results are resolved.
Request
curl -X POST https://id.zb.co.zw/v1/kyc/screen-individual \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"fullName": "John Moyo",
"dateOfBirth": "1990-01-15",
"gender": "MALE",
"nationality": "ZW",
"nationalId": "63-123456A78"
}'
Response
{
"id": "b2d1a7f0-3c44-4e91-8f2a-1b9c6d5e4a30",
"userId": null,
"nationalId": "63-123456A78",
"type": "WORLDCHECK",
"status": "PENDING",
"statusReason": null,
"externalRef": "3d9c1a8b7e6f5d4c3b2a1908f",
"cacheHit": false,
"createdAt": "2026-07-10T09:30:00Z"
}
Screen an organisation
Open a World-Check AML screening case for an organisation. Requires the kyc:screen scope. Cases are deduplicated by the normalized organisation name; a matching prior case is reused (cacheHit: true).
Request body
- Name
fullName- Type
- string
- Description
The organisation's name. Up to 80 characters.
- Name
nationality- Type
- string
- Description
ISO 3166-1 alpha-2 country code (for example,
ZW).
- Name
userId- Type
- string (UUID)
- Description
Optional. Link the verification to an existing user.
Request
curl -X POST https://id.zb.co.zw/v1/kyc/screen-corporate \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"fullName": "Example Holdings Ltd",
"nationality": "ZW"
}'
Response
{
"id": "f4a2c6b8-9d10-4e23-8a71-2c3d4e5f6a70",
"userId": null,
"nationalId": "example holdings ltd",
"type": "WORLDCHECK",
"status": "PENDING",
"statusReason": null,
"externalRef": "9a8b7c6d5e4f3a2b1c0d9e8f7",
"cacheHit": false,
"createdAt": "2026-07-10T09:35:00Z"
}
Retrieve screening results
Fetch the stored screening results for a World-Check case. Requires the kyc:screen scope. Returns the raw World-Check result map keyed by caseSystemId.
Request body
- Name
caseSystemId- Type
- string
- Description
The World-Check case system id whose results you want to read.
Request
curl -X POST https://id.zb.co.zw/v1/kyc/screen-result \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"caseSystemId": "3d9c1a8b7e6f5d4c3b2a1908f"
}'
Response
{
"caseSystemId": "3d9c1a8b7e6f5d4c3b2a1908f",
"results": [],
"resolution": null
}
Fetch the screening toolkit
Fetch the World-Check resolution toolkit: the statuses, risks, and reasons available when resolving a case. Requires the kyc:screen scope. Use the returned ids when recording a resolution decision.
Request
curl https://id.zb.co.zw/v1/kyc/screen-toolkit \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response
{
"statuses": [
{ "id": "st-positive", "name": "POSITIVE" },
{ "id": "st-possible", "name": "POSSIBLE" },
{ "id": "st-false", "name": "FALSE" },
{ "id": "st-unspecified", "name": "UNSPECIFIED" }
],
"risks": [
{ "id": "rk-high", "name": "HIGH" },
{ "id": "rk-medium", "name": "MEDIUM" },
{ "id": "rk-low", "name": "LOW" },
{ "id": "rk-unknown", "name": "UNKNOWN" }
],
"reasons": [
{ "id": "rs-full", "name": "FULL_MATCH" },
{ "id": "rs-partial", "name": "PARTIAL_MATCH" },
{ "id": "rs-no", "name": "NO_MATCH" },
{ "id": "rs-unknown", "name": "UNKNOWN" }
]
}
The exact status, risk, and reason ids come from the live toolkit. Fetch them at runtime rather than hard-coding the values shown here.
Resolve a screening case
Record a resolution decision on a World-Check case. Requires the kyc:screen scope. Supply the ids from the screening toolkit for the status, risk, and reason. The decision is forwarded to the provider, and its boolean outcome is returned as success.
When you also pass a verificationId, and the provider accepts the resolution, the linked verification record is transitioned:
- A FALSE status (a cleared false positive) moves the verification to
VERIFIED. - A POSITIVE status moves it to
REJECTED. - A POSSIBLE or UNSPECIFIED status leaves it
PENDING.
On a successful resolve, the resolutionRemark is stored on the verification and the resolution outcome is merged into its responsePayload. If the supplied verificationId does not match a record, the resolve still returns success from the provider, but no verification is transitioned and none is returned.
Request body
- Name
caseSystemId- Type
- string
- Description
The World-Check case system id being resolved.
- Name
resultIds- Type
- array
- Description
One or more result ids from the case being resolved. At least one is required.
- Name
statusId- Type
- string
- Description
The resolution status id from the toolkit (for example, the id for
FALSEorPOSITIVE).
- Name
riskId- Type
- string
- Description
The risk id from the toolkit.
- Name
reasonId- Type
- string
- Description
The reason id from the toolkit.
- Name
resolutionRemark- Type
- string
- Description
Optional. A free-text remark stored against the resolution. Truncated to 255 characters when persisted on the verification.
- Name
verificationId- Type
- string (UUID)
- Description
Optional. The KYC verification record to transition on a successful resolve.
Request
curl -X POST https://id.zb.co.zw/v1/kyc/screen-resolve \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"caseSystemId": "3d9c1a8b7e6f5d4c3b2a1908f",
"resultIds": ["res-1"],
"statusId": "st-false",
"riskId": "rk-low",
"reasonId": "rs-no",
"resolutionRemark": "Cleared: not the same person",
"verificationId": "b2d1a7f0-3c44-4e91-8f2a-1b9c6d5e4a30"
}'
Response
{
"success": true,
"verification": {
"id": "b2d1a7f0-3c44-4e91-8f2a-1b9c6d5e4a30",
"userId": null,
"nationalId": "63-123456A78",
"type": "WORLDCHECK",
"status": "VERIFIED",
"statusReason": "Cleared: not the same person",
"externalRef": "3d9c1a8b7e6f5d4c3b2a1908f",
"cacheHit": false,
"createdAt": "2026-07-10T09:30:00Z"
}
}
Omit verificationId to record the resolution with the provider only. The response is then { "success": true } with no verification object.
Get a verification by ID
Fetch a single verification record by its id. Requires the kyc:screen scope. Returns the verification object. An unknown id returns an error.
Path parameters
- Name
id- Type
- string (UUID)
- Description
The id of the verification record to retrieve.
Request
curl https://id.zb.co.zw/v1/kyc/verifications/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"nationalId": "63-123456A78",
"type": "EGOV",
"status": "VERIFIED",
"statusReason": null,
"externalRef": null,
"cacheHit": false,
"requestSnapshot": { "nationalId": "63-123456A78" },
"responsePayload": { "Status": "A" },
"createdAt": "2026-07-10T09:12:00Z"
}
List verifications
List the verification records for a national ID, most recent first. Requires the kyc:screen scope. Filter by verification type with the optional type query parameter. Returns an array of verification objects.
The nationalId is matched against the value stored on each record, which for watchlist screenings may be the normalized identity key rather than a literal national ID.
Query parameters
- Name
nationalId- Type
- string
- Description
Required. The national ID, or World-Check identity key, to look up.
- Name
type- Type
- string
- Description
Optional. Restrict the results to one type:
EGOV,FCB, orWORLDCHECK.
Request
curl "https://id.zb.co.zw/v1/kyc/verifications?nationalId=63-123456A78&type=EGOV" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response
[
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"nationalId": "63-123456A78",
"type": "EGOV",
"status": "VERIFIED",
"statusReason": null,
"externalRef": null,
"cacheHit": false,
"requestSnapshot": { "nationalId": "63-123456A78" },
"responsePayload": { "Status": "A" },
"createdAt": "2026-07-10T09:12:00Z"
}
]
Omit type to return every verification for the national ID across all types.