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 ownoperationTypeandcurrencyTypeand 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/maxin USD), aggregate (maxAmountUsd/maxOrdersCountover 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 therequiredFieldsto collect. Pick the one whosetypematchesrequiredKycTypeand 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 needingadvancedhas 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 answersbasicand noadvancedwhile 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 reachedreachedKycLimit, KYC is switched off for your account, or your project needs a URL signature and has no active secret.messagesays which of the last two applies. The links are covered in full on KYC flow.reachedKycLimit—trueonce the user has three submissions on file that are notinvalid. 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 whyrequiredKycTypecan beadvancedfor a user who already showspassedKycType: "basic". Advanced is global.currentKycStatus— the status of the latest submission. Alongside the four values on the Types page, older records carryapproved-legacy, and that is a pass. A polling loop written asstatus === "approved"never finishes for those users: branch onpassedKycType, or treat any status beginning withapprovedas 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.isNgBvnBlockedistruewhen we hold no approved NG BVN for the user; the fix is the BVN document.isNgBvnSupportLockednarrows that to users who may not self-serve it — anyone whosepassedKycTypeisbasicwithout 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 withpassedKycType: "advanced"andisNgBvnBlocked: trueis offered the BVN form but submit rejects them withUser 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
offrampKycRequiredistrue— which is nowhere in Nigeria today. On an NG off-ramp, ignore both flags.offrampKycRequired— the order country's switch, as a boolean.falsemeans off-ramp orders there need no KYC, and that the crypto-deposit rules have already been stripped fromkycSettings.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:
Last updated

