Skip to content

Verification tokens

Every check returns a lookup_id. That string is a verification token: a durable, auditable reference to one check, what it asked and what came back.

POST /api/v1/kyc/verify/kra_pin → { "lookup_id": "a1b2c3d4e5f6a7b8c9d0e1f2", … }
│
┌─────────────────┴─────────────────┐
▼ ▼
GET /api/v1/kyc/tokens/{token} POST /api/v2/account_kycs
(read back what it proved) ("kyc_tokens": { "tin": "…" })

A token is not a bearer credential. It is an opaque handle that only works for the merchant that created it, and it carries no authority on its own.

Terminal window
curl "$PH_CONNECT_URL/api/v1/kyc/tokens/a1b2c3d4e5f6a7b8c9d0e1f2" \
-u "$PH_API_USERNAME:$PH_API_PASSWORD"
200 OK
{
"token": "a1b2c3d4e5f6a7b8c9d0e1f2",
"check": "kra_pin",
"category": "tax_id",
"country": "KE",
"status": "verified",
"verification_status": "VERIFIED",
"verified": true,
"actor_type": "C",
"subject_name": "ACME TRADING LIMITED",
"identifier_masked": "P00•••••00X",
"reference": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"source": "cache",
"account_id": 63,
"verified_at": "2026-09-18T14:26:17Z",
"expires_at": "2027-01-09T22:26:17Z"
}
Field Notes
verified Boolean shorthand for status == "verified". Read this.
category What kind of check it was — this decides what the token is worth.
actor_type C company / I individual. Absent when the check could not tell.
identifier_masked The identifier that was checked, masked — e.g. P00•••••00X.
verified_at When the check ran.
expires_at When the underlying cached result lapses.

Add ?data=true to include the raw provider payload. It is withheld by default so that resolving a token does not re-expose personal data you do not need.

A token belonging to another merchant returns 404, exactly as an unknown token does — the API never confirms that someone else’s token exists.

A Team’s KYC tier is decided by what the tokens actually prove, not by what the submission claims:

Tokens attached Tier Reasoning
Phone number alone 2 A verified line identifies a person
National ID alone 2 Identity confirmed, nothing about a business
National ID + company tax ID (actor_type: "C") 3 A verified company with a verified officer
National ID + individual tax ID ("I") 2 Tier 3 and above are for companies only
Any token whose check came back not_verified — Contributes nothing

Bank account, passport and BVN checks are useful in their own right but do not feed the tier.

Send the tokens under kyc_tokens on a KYC submission. All three slots are optional.

Terminal window
curl -X POST "$PH_AUTH_URL/api/v2/account_kycs" \
-u "$PH_API_USERNAME:$PH_API_PASSWORD" \
-H "Content-Type: application/json" \
-d '{
"account_id": 63,
"entity_type": "business",
"country_code": "KE",
"country": "Kenya",
"contact_information": {
"first_name": "Jane",
"last_name": "Doe",
"identification_number": 12345678,
"identification_type": "national_id",
"phone_number": "+254712345678"
},
"entity_information": {
"entity_name": "Vendor A Ltd"
},
"kyc_tokens": {
"national_id": "9f2c61ab4d7e4a0c8b115d3e",
"phone_number": "c41e77d9b0a24f6e89305ab2",
"tin": "a1b2c3d4e5f6a7b8c9d0e1f2"
}
}'
Slot Expects a token from
kyc_tokens.national_id national_id_*, nin
kyc_tokens.phone_number phone, phone_status_global
kyc_tokens.tin kra_pin, tin_global

PayHero resolves each token against Axxa Connect before accepting it. A token it has never issued is rejected outright:

400 Bad Request
{
"error_code": "invalid_argument",
"message": "kyc_tokens.tin is not a verification token issued by Axxa Connect"
}

A submission backed by verified tokens does not wait for a reviewer. It comes back already approved, at the tier the tokens justify, and the Team and its organisation move to that tier immediately:

200 OK
{
"account_kyc": {
"id": 501,
"account_id": 63,
"status": "approved",
"kyc_tier": 3,
"verification_information": {
"actor_type": "C",
"verified_kyc_tier": 3,
"kyc_tokens": {
"national_id": "9f2c61ab4d7e4a0c8b115d3e",
"tin": "a1b2c3d4e5f6a7b8c9d0e1f2"
},
"verifications": [
{
"kind": "national_id",
"token": "9f2c61ab4d7e4a0c8b115d3e",
"check": "national_id_ke",
"category": "national_id",
"verified": true,
"actor_type": "I",
"subject_name": "JANE WANJIRU DOE"
},
{
"kind": "tin",
"token": "a1b2c3d4e5f6a7b8c9d0e1f2",
"check": "kra_pin",
"category": "tax_id",
"verified": true,
"actor_type": "C",
"subject_name": "ACME TRADING LIMITED"
}
]
}
}
}

The resolved verification_information stays on the record, so what the tier was granted on is auditable long after the tokens’ cached results have lapsed.

A submission sent without tokens behaves exactly as it always has: status: "pending", awaiting review.

POST /api/v2/account_kycs/upgrade accepts kyc_tokens too. The tokens have to cover the whole of what you asked for — request tier 3 with only an individual tax ID and it goes to a reviewer instead.

Terminal window
curl -X POST "$PH_AUTH_URL/api/v2/account_kycs/upgrade" \
-u "$PH_API_USERNAME:$PH_API_PASSWORD" \
-H "Content-Type: application/json" \
-d '{
"account_id": 63,
"upgrade_information": { "upgrade_to": 3 },
"kyc_tokens": {
"national_id": "9f2c61ab4d7e4a0c8b115d3e",
"tin": "a1b2c3d4e5f6a7b8c9d0e1f2"
}
}'

A token stays resolvable indefinitely — expires_at describes the cached result, not the token. Once it passes, the token still resolves and still shows what the check found; only a new check on the same identifier would go back to the provider.

Attach tokens reasonably soon after running them. A token minted a year ago is still honoured, but it attests to what was true a year ago.