For the complete documentation index, see llms.txt. This page is also available as Markdown.

KYC flow

Most flows need the user's KYC level checked before you create an order. Two endpoints do the work: Get user KYC state tells you what is needed, Submit user KYC satisfies it.

You have a choice about who collects the documents. Either you collect them and post them yourself, or you send the user a link and we collect them. The check, the rules and the result are the same; only step 5 differs.

The short version

  1. Call Get user KYC state with the user's email, the order's country, and the amounts of the order you are about to create.

  2. If isNgBvnBlocked is true and you are creating a Nigerian on-ramp, stop — handle the BVN gate first (below). It is a separate check from requiredKycType.

  3. If requiredKycType is null, go to quoting.

  4. Otherwise compare requiredKycType with passedKycType. Already at that tier or above? Proceed.

  5. Not yet? Either collect the documents yourself, or send the user a hosted link:

    • Collect them yourself. Pick the document from kycDocuments whose type matches requiredKycType, collect its requiredFields, and post it to Submit user KYC.

    • Let us collect them. Send the user kycUrls[requiredKycType] from the same response. See Letting Fonbnk collect it.

  6. Wait until passedKycType reaches the tier — by polling, or with the kyc webhook. This is the same either way.

Letting Fonbnk collect it

kycUrls on the Get user KYC state response holds one link per tier. Send the user the one that matches requiredKycType. The link opens our widget, asks for the email we already hold for them, sends a one-time code to it, and then shows the document form for that tier.

You do not build a form, you never handle identity documents, and you read the result exactly as before: poll passedKycType, or take the kyc webhook.

Send the user back to your site with callbackUrl, and relabel the button with callbackBtnText. Both are query params on Get user KYC state, and they are baked into the links. The button appears once the user has submitted a check.

  • A tier with no enabled document in that country. kycUrls.advanced is absent in Nigeria today, because the basic BVN is the only enabled Nigerian document. An absent field means the tier cannot be satisfied there, not that the user has passed it.

  • A user at reachedKycLimit. The page could show them nothing but a support message, so no link is issued.

  • A project with no active URL signature secret. message names the fix.

Two things the user may see that you did not ask for

  • In Nigeria, an advanced link shows the BVN form first. The Nigerian BVN gate is separate from the tier, so a user with no BVN on file is asked for it before anything else. They then do the advanced check. That is two records and two webhooks, with passedKycType: "basic" in between. A user who is isNgBvnSupportLocked cannot self-serve the BVN at all, so send them to support rather than a link.

  • The user can change the country they verify in. The page offers our supported KYC countries. Verifying in a country other than the order country forces advanced, which is the same rule the API applies.

Watch for the traps

reachedKycLimit does not clear by waiting. It counts every verification attempt on file that has not been voided — approved and rejected ones included, not just in-flight ones. Three attempts and the user can never submit again until support voids one. When it is true, route the user to support rather than telling them to wait.

Poll passedKycType, not currentKycStatus. Some historical records carry a status value outside the documented set, so a loop that waits for currentKycStatus === "approved" can hang. Waiting for passedKycType to reach the tier you need is the reliable test.

An advanced-verified user can be BVN-blocked with nothing to submit. requiredKycType is null for anyone who has passed advanced, but the Nigerian BVN gate is separate and can still be set. Such a user cannot self-serve — submitting any document is refused with "User already passed advanced KYC". Send them to support.

Reading kycSettings yourself

You only need this if you cannot send the amounts — for a pricing page with no amount yet, say.

Each entry carries an operationType, a currencyType and the type (tier) it demands. {deposit, fiat} is the on-ramp (the user pays fiat in); {deposit, crypto} is the off-ramp (the user sends crypto in). Match the entry against the leg you care about, then:

  • Per-order rule — min and max are set. It fires when that leg's USD value is in [min, max). max may be the string "Infinity".

  • Aggregate rule — maxAmountUsd and/or maxOrdersCount are set. It fires when the user's lifetime successful volume or count in that bucket, plus the order you are pre-checking, exceeds the threshold. The order counts before it exists.

  • An entry can be both at once. Both branches are evaluated.

  • When several rules fire, the highest tier wins.

Two rules that surprise people

Off-ramp KYC is a per-country switch, and mostly off. When a country does not require KYC to sell crypto, its {deposit, crypto} rules are dropped from kycSettings before you see them. That is the only filtering applied — the on-ramp rules always come back and you still have to match them against your leg yourself. offrampKycRequired tells you which case you are in. Today South Africa is the only country where it is on, but read the flag rather than assuming.

Basic does not cross borders. A basic pass counts only in the country it was earned in — passedKycCountryIsoCode says where. A user with passedKycType: "basic" ordering elsewhere gets requiredKycType: "advanced". Advanced is global.

The Nigerian BVN gate

A Nigerian order needs a BVN on file, and this is checked separately from requiredKycType.

  • isNgBvnBlocked is computed without a direction. Honour it on an on-ramp. On an off-ramp, honour it only when offrampKycRequired is true for that country — and for Nigeria it is false today, so a Nigerian off-ramp is not gated even though the flag is set.

  • isNgBvnSupportLocked means the user has a basic pass that is not an approved Nigerian BVN. That includes a basic earned in Nigeria with a NIN or voter ID, not only one earned abroad. They cannot self-serve the BVN — send them to support.

Nigeria currently offers exactly one enabled KYC document, BVN. If requiredKycType comes back as advanced for a Nigerian order there is no document that can satisfy it — raise it with support rather than looping. kycUrls.advanced is absent there for the same reason.

Response fields

  • requiredKycType ("basic" | "advanced" | null) — the tier this order needs. null means nothing to do. Branch on this.

  • passedKycType ("basic" | "advanced" | undefined) — the highest tier the user has passed.

  • passedKycCountryIsoCode (string | undefined) — where that pass was earned.

  • reachedKycLimit (boolean) — three un-voided attempts on file. Support only.

  • currentKycType / currentKycStatus / currentKycStatusDescription — the latest submission's tier, status and reason. Treat the status set as open-ended.

  • currentKycPhase — reserved for a Nigerian two-phase advanced flow that is currently switched off. Not emitted today.

  • kycDocuments — the enabled documents for the selected country, each with _id, type, title, value and requiredFields.

  • kycUrls ({ basic?: string; advanced?: string } | undefined) — hosted KYC links, one per tier the country can satisfy.

  • kycSettings — the country's rules, with the off-ramp rules dropped when the switch is off.

  • offrampKycRequired (boolean) — whether this country requires KYC to sell crypto.

  • isNgBvnBlocked / isNgBvnSupportLocked (boolean) — the Nigerian BVN gate.

  • message (string | undefined) — present when KYC is switched off for your account, and when a hosted KYC link could not be signed.

Sample response

A brand-new Nigerian user, pre-checked for a $120 on-ramp. Optional fields with no value are omitted rather than returned as null.

One document, because BVN is the only one enabled for Nigeria. One link, for the same reason. One rule, because Nigeria's crypto-deposit rule was dropped — offrampKycRequired is false. And isNgBvnBlocked is true because this user has no BVN on file.

Submitting

Basic documents take a name, a date of birth and an ID number. Advanced documents take base64 images instead of the number — image_type_id 2 for the selfie, 3 for the document front, 7 for the back — with the whole body under 10 MB. Full request examples are on Submit user KYC.

Last updated