> For the complete documentation index, see [llms.txt](https://docs.fonbnk.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fonbnk.com/server-to-server/api-endpoints/get-user-kyc-state.md).

# Get user KYC state

## <mark style="color:$success;">\[GET]</mark> /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.

{% hint style="warning" %}
**Two things to know before your first call.**

**It writes.** An email we have not seen before is registered as a Fonbnk end user by this `GET`. [Submit user KYC](/server-to-server/api-endpoints/submit-user-kyc.md) answers `404 User not found` for an unknown email, so this call is what brings the user into being.

**It needs the create-users permission.** Without it every call answers `403 This feature is not available for this merchant, please contact support`. The same permission guards create order, confirm order, cancel order, trigger intermediate action, submit KYC and generate user auth tokens. Quotes and the discovery endpoints are **not** guarded, so you can price an order and only meet the wall here. Ask support to switch it on before you build against these endpoints.
{% endhint %}

### Request

```typescript
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;
}
```

**`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`.

**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`.

{% hint style="success" %}
Pass the amounts. It is one call instead of evaluating `kycSettings` yourself, and it cannot drift from what order creation enforces.

To make it agree exactly, send the deposit amount **before** fees and the payout amount **after** fees — that is the pair order creation evaluates. Both come straight off a quote.
{% endhint %}

Request URL example (history only):

{% code overflow="wrap" %}

```
GET /api/v2/user/kyc?userEmail=user@example.com&countryIsoCode=NG
```

{% endcode %}

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

{% code overflow="wrap" %}

```
GET /api/v2/user/kyc?userEmail=user@example.com&countryIsoCode=NG&depositCurrencyType=fiat&depositAmountUsd=120&payoutCurrencyType=crypto&payoutAmountUsd=118
```

{% endcode %}

### Response

{% code overflow="wrap" expandable="true" %}

```typescript
type Response = {
  passedKycType?: KycType;              // highest tier the user has passed
  requiredKycType: KycType | null;      // what this order needs; null means nothing to do
  reachedKycLimit: boolean;             // true at 3 submissions on file; lifetime, not a queue
  currentKycType?: KycType;             // tier of the latest submission
  currentKycStatus?: KycStatus;         // status of the latest submission
  currentKycStatusDescription?: string; // human-readable reason for that status
  currentKycPhase?: KycPhase;           // reserved; unset in production today
  passedKycCountryIsoCode?: string;     // country the latest approved record was earned in
  kycDocuments: KycDocument[];          // enabled documents for the selected country
  kycSettings: KycSetting[];            // the country's rules, off-ramp filter already applied
  offrampKycRequired: boolean;          // whether this country requires KYC to sell crypto
  isNgBvnBlocked: boolean;              // Nigeria only: no NG BVN on file
  isNgBvnSupportLocked: boolean;        // ...and the user cannot self-serve it
  message?: string;                     // set when KYC is switched off for your account
}
```

{% endcode %}

The types are on the [Types](/server-to-server/types.md) page. The decision flow that uses them is on [KYC flow](/server-to-server/kyc-flow.md).

#### 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](/server-to-server/api-endpoints/submit-user-kyc.md). 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.
* **`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. The KYC fields come back empty and you can skip the whole flow.

Response example. Nigeria returns one document, because the BVN is the only enabled Nigerian document:

{% code overflow="wrap" expandable="true" %}

```json
{
    "passedKycType": "basic",
    "requiredKycType": null,
    "reachedKycLimit": false,
    "currentKycType": "basic",
    "currentKycStatus": "approved",
    "currentKycStatusDescription": "Exact Match",
    "passedKycCountryIsoCode": "NG",
    "offrampKycRequired": false,
    "isNgBvnBlocked": false,
    "isNgBvnSupportLocked": false,
    "kycDocuments": [
        {
            "_id": "67da90f0b6f2529a09645219",
            "title": "BVN",
            "value": "BVN",
            "type": "basic",
            "requiredFields": [
                { "key": "first_name", "type": "string", "label": "First Name", "required": true },
                { "key": "last_name", "type": "string", "label": "Last Name", "required": true },
                { "key": "dob", "type": "date", "label": "Date of birth", "required": true },
                {
                    "key": "id_number",
                    "type": "string",
                    "label": "BVN Number",
                    "required": true,
                    "format": "00000000000",
                    "regexp": "^[0-9]{11}$"
                }
            ]
        }
    ],
    "kycSettings": [
        {
            "operationType": "deposit",
            "currencyType": "fiat",
            "type": "basic",
            "min": 1,
            "max": "Infinity",
            "maxAmountUsd": 1
        }
    ]
}
```

{% endcode %}

{% hint style="info" %}
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](/kyc.md) for the current table, and always read `kycSettings` at runtime.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.fonbnk.com/server-to-server/api-endpoints/get-user-kyc-state.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
