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.
Resolve a token
Section titled “Resolve a token”curl "$PH_CONNECT_URL/api/v1/kyc/tokens/a1b2c3d4e5f6a7b8c9d0e1f2" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD"{ "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.
What each token is worth
Section titled “What each token is worth”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.
Attach tokens to a Team’s KYC
Section titled “Attach tokens to a Team’s KYC”Send the tokens under kyc_tokens on a
KYC submission. All three slots are optional.
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:
{ "error_code": "invalid_argument", "message": "kyc_tokens.tin is not a verification token issued by Axxa Connect"}Approval without review
Section titled “Approval without review”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:
{ "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.
Upgrading an existing Team
Section titled “Upgrading an existing Team”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.
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" } }'Lifetime
Section titled “Lifetime”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.

