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

Get user KYC state

[GET] /api/v2/user/kyc

Returns where a user stands on KYC, the rules for their country, and the documents they may submit. Call it before every order — it is the only way to know whether the order you are about to create will be refused.

It also hands you hosted KYC links, so you can send the user to our pages instead of collecting their documents yourself. See Letting Fonbnk collect it.

Request

type QueryParams = {
    userEmail: string;                 // required
    countryIsoCode: string;            // required, the country of the order
    kycCountryIsoCode?: string;        // the country whose documents to offer, if the user picks one
    depositCurrencyType?: CurrencyType;// all four together, or none
    payoutCurrencyType?: CurrencyType;
    depositAmountUsd?: number;
    payoutAmountUsd?: number;
    callbackUrl?: string;              // baked into kycUrls, see below
    callbackBtnText?: string;
}

userEmail is URL-decoded before it is validated, so percent-encode a plus-addressed email: user%2Btag@example.com. A literal + in a query string decodes to a space and the request fails with User email must be a valid email address.

countryIsoCode is the country of the order. It drives requiredKycType and kycSettings.

kycCountryIsoCode is only for flows where the user chooses which country to verify in, and it is the country whose documents come back in kycDocuments. Send it as a country other than countryIsoCode and two things happen: requiredKycType is forced to advanced, and every basic document is dropped from kycDocuments. Leave it out and neither rule runs — you get the order country's documents, basic ones included. Two upper-case letters; anything else is a 400.

callbackUrl puts a "Back to website" button on the hosted KYC page, pointing at your own site. It must be an absolute https URL, 2000 characters or fewer. The button appears once the user has submitted a check, not before. callbackBtnText relabels it, 40 characters or fewer; the default label is "Back to website". Neither param changes anything else in the response, and the order placeholders that callbackUrl supports on the widget's success page do not apply here — there is no order.

The four amount params turn the answer into a pre-check. Send depositCurrencyType, payoutCurrencyType, depositAmountUsd and payoutAmountUsd together and requiredKycType accounts for the order you are about to create, not just the user's history. Send none of them and you get the history-only answer. Sending some but not all is rejected with 400.

Request URL example (history only):

Request URL example (pre-checking a $120 on-ramp):

Response

The types are on the Types page. The decision flow that uses them is on KYC flow.

The one field to branch on

requiredKycType is the answer. null means proceed. Otherwise compare it with passedKycType: if the user is already at that tier or above, proceed; if not, they must submit the matching document from kycDocuments first. A user who has passed advanced always gets null — advanced satisfies every rule everywhere.

The rest, in the order you will need it

  • kycSettings — the country's rules, with one filter already applied: when the country does not require off-ramp KYC (offrampKycRequired: false) the crypto-deposit rules are dropped before you see them. The rest of the matching is still yours. Each rule carries its own operationType and currencyType and applies only to the leg that matches both — a {deposit, fiat} rule is about the money coming in on an on-ramp, and says nothing about a crypto deposit. Each rule is per-order (min/max in USD), aggregate (maxAmountUsd / maxOrdersCount over the user's lifetime of successful orders), or both at once. When several fire, the highest tier wins.

  • How the two rule kinds combine — the aggregate rule reaches below the per-order min, which surprises people. When you send amounts, the order you are about to create is counted into the aggregate first and the test is strictly greater than. South Africa's on-ramp rules read "basic from $3" per order and "$1 lifetime" in aggregate, so a first order of $2 clears the per-order rule and still needs basic: $0 + $2 is more than $1. Send no amounts and the aggregate is tested against history alone, with greater-than-or-equal.

  • kycDocuments — one entry per enabled document for the selected country, each with the requiredFields to collect. Pick the one whose type matches requiredKycType and post it to Submit user KYC. A country can have documents on file for a tier and none of them enabled: Nigeria today enables exactly one, the basic BVN. So an NG order that comes back needing advanced has nothing to submit — send that user to support rather than to a form.

  • kycUrls — a link per tier that sends the user to our own pages to verify. One entry per tier the country can satisfy today, so Nigeria answers basic and no advanced while the BVN is its only enabled document. An absent field means that tier has no document, not that it is already passed. The whole object is absent in three cases: the user has reached reachedKycLimit, KYC is switched off for your account, or your project needs a URL signature and has no active secret. message says which of the last two applies. The links are covered in full on KYC flow.

  • reachedKycLimit — true once the user has three submissions on file that are not invalid. It is a lifetime count, not a queue: an approved or a rejected record still occupies its slot, so waiting does not clear it. Once a user is at the cap only support, voiding a record, can free one. Do not tell the user to wait for something to resolve.

  • passedKycCountryIsoCode — the country of the user's latest approved record. A basic pass is scoped to the country it was earned in and satisfies orders in that country only, which is why requiredKycType can be advanced for a user who already shows passedKycType: "basic". Advanced is global.

  • currentKycStatus — the status of the latest submission. Alongside the four values on the Types page, older records carry approved-legacy, and that is a pass. A polling loop written as status === "approved" never finishes for those users: branch on passedKycType, or treat any status beginning with approved as approved.

  • currentKycPhase — reserved for a two-phase Nigerian advanced flow that is built but switched off. It is unset on every production record today. Do not build UI that depends on it.

  • isNgBvnBlocked / isNgBvnSupportLocked — Nigeria only. isNgBvnBlocked is true when we hold no approved NG BVN for the user; the fix is the BVN document. isNgBvnSupportLocked narrows that to users who may not self-serve it — anyone whose passedKycType is basic without an approved NG BVN, whether that basic was earned abroad or in Nigeria on a document that is no longer enabled. Send those to support. One more case belongs there too: a user with passedKycType: "advanced" and isNgBvnBlocked: true is offered the BVN form but submit rejects them with User already passed advanced KYC, so support is the only route for them as well.

  • Direction matters, and this endpoint does not know it. There is no order type in the request, so both NG BVN flags are reported for either direction. Order creation enforces the rule on every on-ramp, and on an off-ramp only where offrampKycRequired is true — which is nowhere in Nigeria today. On an NG off-ramp, ignore both flags.

  • offrampKycRequired — the order country's switch, as a boolean. false means off-ramp orders there need no KYC, and that the crypto-deposit rules have already been stripped from kycSettings.

  • message — present when KYC is switched off for your account, and when a hosted KYC link could not be signed. The KYC fields come back empty in the first case.

Response example. Nigeria returns one document, because the BVN is the only enabled Nigerian document, and one link for the same reason:

That single Nigerian rule reads: any on-ramp of $1 or more needs basic, and separately, basic is required once the user's lifetime successful on-ramp volume passes $1. In practice, one small order and then KYC. Nigeria has a second rule for crypto deposits, but it is stripped from this response because Nigeria does not require off-ramp KYC. Every country's numbers differ — see KYC for the current table, and always read kycSettings at runtime.

Last updated