> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hyparrow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Identity Verification

> Verify identities, businesses, and bank accounts in Nigeria with BVN, NIN, CAC, NUBAN, AML, and more — billed via subscription quota or pay-as-you-go wallet balance.

## Overview

The Hyparrow Identity Verification (KYC) API lets you confirm the identity of
people and businesses and resolve bank account details in real time. It wraps
our verification network behind a single, consistent interface so you can:

* **Verify individuals** — BVN, NIN, voter's card, phone number, driver's
  license, and international passport.
* **Verify businesses** — CAC company lookups, shareholders, directors, secretary,
  and TIN.
* **Run risk checks** — domestic and global AML screening, credit history, and
  face comparison.
* **Resolve bank accounts** — NUBAN account-name lookup, lookup by bank code, and
  bank guessing.

Verifications are exposed under three groups, all under the base path
`/api/v1`:

| Group                 | Path prefix       | What it is                                      |
| --------------------- | ----------------- | ----------------------------------------------- |
| Account & billing     | `/kyc/*`          | Balance, plans, pricing, subscriptions, history |
| Verification option A | `/kyc/identity/*` | One verification option in our network          |
| Verification option B | `/kyc/verify/*`   | A second verification option in our network     |
| Account lookup        | `/kyc/nuban/*`    | NUBAN bank-account resolution                   |

<Note>
  The `/kyc/identity` and `/kyc/verify` groups are simply two verification
  options offered by our verification network. Some products (for example BVN
  and NIN) appear in both — pick the one whose response shape and pricing best
  fits your use case.
</Note>

## Authentication

Every request must be authenticated with your API key and secret, sent as
headers:

```bash theme={null}
X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxx
X-API-Secret: sk_live_xxxxxxxxxxxxxxxxxxxx
```

The base URL is:

```
https://api.hyparrow.cloud/api/v1
```

<Note>
  **Sandbox testing.** Point your integration at
  `https://sandbox.hyparrow.cloud/api/v1` and use a test key (prefixed
  `pk_test_` / `sk_test_`). Sandbox calls are not charged and never hit the live
  verification network. In the sandbox:

  * **Any identifier of all ones** (`11111111111`) → the not-found failure path.
  * **Any other identifier**, including all zeros, → a success, with an identity
    deterministically derived from the value you sent.

  These magic values are a **sandbox-only** feature. Production has no test
  identifiers: every BVN or NIN you send there is a live lookup against the
  verification network, and unknown values fail like any other. See
  [Sandbox](/sandbox) for the full list.
</Note>

<Warning>
  There is deliberately no production identifier that returns a synthetic
  success. Exercise your success path in the sandbox — do not use a real
  person's BVN as a test fixture.
</Warning>

## Billing model

Verification calls are billed in one of two ways. On every call we authorize
billing **before** hitting the verification network:

1. **Subscription quota.** If you have an active subscription whose plan covers
   the API you're calling and quota remains, the call **consumes one unit of
   quota** and costs nothing extra.
2. **Pay-as-you-go (PAYG).** If you have no subscription covering that API, the
   per-call price is **deducted from your wallet balance** (in NGN).

### How fallthrough works

When a subscription's quota for a specific API is exhausted, the plan's
`overflowBehavior` decides what happens:

* `hard_block` (default) → the call is rejected with **402 Payment Required**
  (`Quota exceeded for this API on your current plan`). Subscribe again or wait
  for the period to renew.
* `payg_fallthrough` → the call falls through to PAYG and the per-call price is
  charged to your wallet.

If you hold any active subscription but **none of your plans cover** the API you
called, the request is hard-blocked with a 402 — you need a plan that includes
that API. If you have **no subscriptions at all**, every call is PAYG.

<Warning>
  **Automatic refunds.** Billing is authorized up front. If the verification
  network then returns anything other than a record — an unknown identifier
  (**404**) as much as a genuine outage (**502**) — we roll the charge back. You
  are never charged for a failed verification, however many times you repeat it.
  A request rejected by validation (**400**) is never authorized in the first
  place.

  * **PAYG** — the amount is credited back to your wallet as a transaction
    referenced `VER-REFUND-<apiCode>-<timestamp>`, visible on
    `GET /transactions` and described as
    `Verification refund: <apiCode> (upstream failure)`.
  * **Subscription** — the consumed quota unit is credited back to the
    subscription, so `quotaUsed` returns to its previous value.

  The original call still appears in `GET /kyc/history` with
  `"status": "failed"`, so a refunded attempt is auditable from both sides.
</Warning>

### Quota alerts

Subscriptions send email alerts at **80%** and **100%** quota usage for each API
so you're never surprised by a hard block.

## Typical flow

<Steps>
  <Step title="Check your balance (and optionally subscribe)">
    Call `GET /kyc/balance` to see your wallet balance. Browse `GET /kyc/plans`
    and `GET /kyc/pricing` to compare a subscription against PAYG. If a plan
    fits your volume, subscribe with `POST /kyc/subscribe`.
  </Step>

  <Step title="Call a verification endpoint">
    Send a `POST` to the verification you need — for example
    `/kyc/identity/bvn/basic`. Billing is handled automatically: quota first,
    then PAYG.
  </Step>

  <Step title="Read the result">
    A successful response returns `{ "success": true, "data": { ... } }`, where
    `data` is the verification network's own reply forwarded unmodified — so the
    identity fields sit one level deeper, at `data.data`. See
    [Response envelope](#response-envelope) before you write your parser. Use
    `GET /kyc/history` to audit past calls, what they cost, and how they were
    billed.
  </Step>
</Steps>

## Account & billing endpoints

All billing endpoints are authenticated with your API key and secret.

| Method | Path                 | Description                                 |
| ------ | -------------------- | ------------------------------------------- |
| `GET`  | `/kyc/balance`       | Current wallet balance (NGN)                |
| `GET`  | `/kyc/plans`         | Available subscription plans                |
| `GET`  | `/kyc/pricing`       | Per-API PAYG pricing                        |
| `GET`  | `/kyc/subscription`  | Your most recent active subscription        |
| `GET`  | `/kyc/subscriptions` | All active (stackable) subscriptions        |
| `POST` | `/kyc/subscribe`     | Subscribe to a plan (`{ "planId": "..." }`) |
| `GET`  | `/kyc/history`       | Paginated verification call history         |

### Check balance

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.hyparrow.cloud/api/v1/kyc/balance \
    -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
    -H "X-API-Secret: sk_live_xxxxxxxxxxxx"
  ```
</CodeGroup>

```json Response theme={null}
{
  "success": true,
  "data": {
    "balance": 14500.00
  }
}
```

### List plans

```json Response theme={null}
{
  "success": true,
  "data": [
    {
      "id": "8f2c0b1a-9d4e-4a7b-bc31-2f6e5d0a1c44",
      "name": "Starter KYC",
      "description": "1,000 BVN + 1,000 NIN verifications per month",
      "price": 25000.00,
      "intervalDays": 30,
      "quotas": {
        "bvn_basic": 1000,
        "nin": 1000
      },
      "isActive": true
    }
  ]
}
```

The `quotas` object maps each **API code** (the internal identifier you also see
in `/kyc/pricing` and `/kyc/history`) to the number of included calls for the
plan period.

### Subscribe to a plan

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.hyparrow.cloud/api/v1/kyc/subscribe \
    -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
    -H "X-API-Secret: sk_live_xxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{ "planId": "8f2c0b1a-9d4e-4a7b-bc31-2f6e5d0a1c44" }'
  ```
</CodeGroup>

<ParamField body="planId" type="string" required>
  The UUID of the plan to subscribe to. Get valid IDs from `GET /kyc/plans`.
</ParamField>

```json Response theme={null}
{
  "success": true,
  "data": {
    "id": "a1b2c3d4-0000-1111-2222-333344445555",
    "clientId": "c0ffee00-0000-0000-0000-000000000001",
    "planId": "8f2c0b1a-9d4e-4a7b-bc31-2f6e5d0a1c44",
    "status": "active",
    "periodStart": "2026-06-30T10:00:00Z",
    "periodEnd": "2026-07-30T10:00:00Z",
    "quotaUsed": {},
    "overflowBehavior": "hard_block"
  }
}
```

Subscribing debits the plan price from your wallet. If your balance is too low
you get a **402**:

```json 402 Response theme={null}
{
  "success": false,
  "error": "Insufficient wallet balance. Please fund your wallet to subscribe."
}
```

<Note>
  Subscriptions are **stackable** — you can hold several at once, and quota is
  drawn from whichever active plan covers the API being called. Use
  `GET /kyc/subscriptions` to see them all.
</Note>

### Call history

`GET /kyc/history` supports `page` (default `1`) and `limit` (default `20`, max
`100`) query parameters.

```json Response theme={null}
{
  "success": true,
  "data": [
    {
      "id": "f0e1d2c3-...",
      "clientId": "c0ffee00-...",
      "apiCode": "bvn_basic",
      "group": "identity",
      "billingType": "sub",
      "subId": "a1b2c3d4-...",
      "amountCharged": 0,
      "status": "success",
      "createdAt": "2026-07-28T10:05:11Z"
    }
  ],
  "meta": { "total": 1, "page": 1, "limit": 20 }
}
```

`billingType` is `sub` when the call drew from quota (then `amountCharged` is
`0`) or `payg` when it was charged to the wallet. `group` is the endpoint group
that served the call — `identity`, `verify` or `nuban`. `status` is `success` or
`failed`; a failed call is always refunded, so it will have a matching
`VER-REFUND-` entry on `GET /transactions`.

## Response envelope

<Warning>
  **Identity fields are nested two levels deep, not one.** Every verification
  endpoint forwards the verification network's own response as the `data` field
  of the Hyparrow envelope. Read the record at **`data.data`** — reading `data`
  gives you the network's status envelope, and a client that expects the fields
  there reads `undefined` for all of them and silently records an empty
  identity.
</Warning>

Every verification response has this two-layer structure:

```json theme={null}
{
  "success": true,          // Hyparrow: did the call complete?
  "data": {                 // ── the verification network's envelope ──
    "success": true,
    "statusCode": 200,
    "message": "Bvn details retrieved successfully",
    "response_code": "00",  // "00" = success
    "data": { ... }         // ← the record you want
  }
}
```

The outer `success` and the HTTP status are Hyparrow's contract and are stable.
Everything inside `data` originates upstream: the envelope keys above are
consistent in practice, but the fields of the innermost `data` vary by product
(BVN basic returns different fields to a CAC lookup or an AML screen), and field
names are the network's, not ours. Treat unknown keys as additive.

## Verification option A — `/kyc/identity/*`

All endpoints are `POST` and return `{ "success": true, "data": { ... } }` with
the record at `data.data`, as described above.

| Path                               | Body                                                                |
| ---------------------------------- | ------------------------------------------------------------------- |
| `/kyc/identity/bvn/basic`          | `{ "bvn": "..." }`                                                  |
| `/kyc/identity/bvn/basic-igree`    | `{ "bvn": "...", "dateOfBirth": "1990-01-01" }`                     |
| `/kyc/identity/bvn/advanced`       | `{ "bvn": "..." }`                                                  |
| `/kyc/identity/bvn/advanced-igree` | `{ "bvn": "...", "dateOfBirth": "1990-01-01" }`                     |
| `/kyc/identity/bvn/with-face`      | `{ "bvn": "...", "imageUrl": "..." }`                               |
| `/kyc/identity/bvn/with-phone`     | `{ "phoneNumber": "..." }`                                          |
| `/kyc/identity/nin`                | `{ "nin": "..." }`                                                  |
| `/kyc/identity/nin/with-face`      | `{ "nin": "...", "imageUrl": "..." }`                               |
| `/kyc/identity/voters-card`        | `{ "vin": "..." }`                                                  |
| `/kyc/identity/phone/basic`        | `{ "phoneNumber": "..." }`                                          |
| `/kyc/identity/phone/advanced`     | `{ "phoneNumber": "..." }`                                          |
| `/kyc/identity/cac/basic`          | `{ "rcNumber": "...", "companyName": "...", "companyType": "..." }` |
| `/kyc/identity/cac/advanced`       | `{ "rcNumber": "...", "companyName": "...", "companyType": "..." }` |

### Example: BVN basic

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.hyparrow.cloud/api/v1/kyc/identity/bvn/basic \
    -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
    -H "X-API-Secret: sk_live_xxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{ "bvn": "22222222222" }'
  ```

  ```python Python theme={null}
  import requests

  resp = requests.post(
      "https://api.hyparrow.cloud/api/v1/kyc/identity/bvn/basic",
      headers={
          "X-API-Key": "pk_live_xxxxxxxxxxxx",
          "X-API-Secret": "sk_live_xxxxxxxxxxxx",
      },
      json={"bvn": "22222222222"},
  )
  print(resp.json())
  ```

  ```javascript Node.js theme={null}
  const resp = await fetch(
    "https://api.hyparrow.cloud/api/v1/kyc/identity/bvn/basic",
    {
      method: "POST",
      headers: {
        "X-API-Key": "pk_live_xxxxxxxxxxxx",
        "X-API-Secret": "sk_live_xxxxxxxxxxxx",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ bvn: "22222222222" }),
    }
  );
  const data = await resp.json();
  ```
</CodeGroup>

```json Response theme={null}
{
  "success": true,
  "data": {
    "success": true,
    "statusCode": 200,
    "message": "Bvn details retrieved successfully",
    "response_code": "00",
    "data": {
      "bvn": "22222222222",
      "firstName": "JOHN",
      "middleName": "ADE",
      "lastName": "DOE",
      "dateOfBirth": "1990-01-01",
      "gender": "Male",
      "phoneNumber1": "08012345678",
      "phoneNumber2": "",
      "image": "/9j/4AAQSkZJRgABAQ… (~18,000 characters)"
    }
  }
}
```

<ResponseField name="phoneNumber1" type="string">
  The primary phone number on the BVN record. Note the trailing `1` — there is
  **no** field named `phoneNumber`.
</ResponseField>

<ResponseField name="phoneNumber2" type="string">
  A secondary phone number, when the BVN record carries one. Frequently an empty
  string — most records have only one number. Always present in the response.
</ResponseField>

<ResponseField name="image" type="string">
  A base64-encoded JPEG passport photograph of the BVN holder, returned on every
  successful lookup. It is typically **around 18KB of characters** and dominates
  the response size.

  <Warning>
    This is biometric personal data. It arrives whether or not you asked for it,
    and there is currently no way to opt out. If you do not need it, drop it
    before it reaches your logs, your audit tables or your database — persisting
    it carries retention and data-protection obligations you may not intend to
    take on.
  </Warning>
</ResponseField>

`/kyc/identity/bvn/advanced` returns the same envelope with a larger inner
record.

### Example: NIN

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.hyparrow.cloud/api/v1/kyc/identity/nin \
    -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
    -H "X-API-Secret: sk_live_xxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{ "nin": "12345678901" }'
  ```
</CodeGroup>

```json Response theme={null}
{
  "success": true,
  "data": {
    "success": true,
    "statusCode": 200,
    "message": "Nin details retrieved successfully",
    "response_code": "00",
    "data": {
      "nin": "12345678901",
      "firstName": "JANE",
      "lastName": "OKAFOR",
      "dateOfBirth": "1988-05-12",
      "gender": "Female",
      "stateOfOrigin": "Anambra"
    }
  }
}
```

<Note>
  The inner `data` fields above are representative, not exhaustive — the NIN
  record is passed through from the verification network and may include
  additional fields (including a base64 `image`). Run the call once in the
  sandbox against a real-shaped payload before you rely on a specific field.
</Note>

## Verification option B — `/kyc/verify/*`

All endpoints are `POST` and return `{ "success": true, "data": { ... } }`.

| Path                             | Body                                                                  |
| -------------------------------- | --------------------------------------------------------------------- |
| `/kyc/verify/nin`                | `{ "nin": "...", "firstName": "...", "lastName": "..." }`             |
| `/kyc/verify/nin/full`           | `{ "nin": "..." }`                                                    |
| `/kyc/verify/bvn/boolean-match`  | `{ "bvn": "...", "firstName": "...", "lastName": "..." }`             |
| `/kyc/verify/bvn/full`           | `{ "bvn": "..." }`                                                    |
| `/kyc/verify/bvn/igree/initiate` | `{ "bvn": "..." }`                                                    |
| `/kyc/verify/bvn/igree/otp`      | `{ "sessionId": "...", "method": "sms" }`                             |
| `/kyc/verify/bvn/igree/fetch`    | `{ "sessionId": "...", "otp": "..." }`                                |
| `/kyc/verify/tin`                | `{ "tin": "..." }`                                                    |
| `/kyc/verify/drivers-license`    | `{ "licenseNumber": "..." }`                                          |
| `/kyc/verify/passport`           | `{ "passportNumber": "...", "lastName": "...", "dob": "1990-01-01" }` |
| `/kyc/verify/cac`                | `{ "companyName": "..." }`                                            |
| `/kyc/verify/cac/shareholders`   | `{ "rcNumber": "..." }`                                               |
| `/kyc/verify/cac/secretary`      | `{ "rcNumber": "..." }`                                               |
| `/kyc/verify/cac/director`       | `{ "rcNumber": "..." }`                                               |
| `/kyc/verify/credit-history`     | `{ "bvn": "..." }`                                                    |
| `/kyc/verify/aml/domestic`       | `{ "fullName": "..." }`                                               |
| `/kyc/verify/aml/global`         | `{ "type": "person", "query": "..." }`                                |
| `/kyc/verify/face-comparison`    | `{ "image1": "...", "image2": "..." }`                                |
| `/kyc/verify/safetoken/otp`      | `{ "tokenId": "..." }`                                                |
| `/kyc/verify/safetoken/send`     | `{ "tokenId": "...", "mobileNo": "...", "email": "..." }`             |
| `/kyc/verify/address`            | `{ "street": "...", "lga": "...", "state": "...", "applicant": { } }` |

### Boolean name match

The `nin` and `bvn/boolean-match` endpoints in this group perform a
name-match check rather than returning a full record:

```json Response theme={null}
{
  "success": true,
  "data": {
    "matchStatus": "EXACT_MATCH",
    "firstNameMatch": true,
    "lastNameMatch": true
  }
}
```

### BVN iGree consent (3 steps)

Some BVN products require the user's explicit consent via OTP. The iGree flow
is a sequence:

<Steps>
  <Step title="Initiate">
    `POST /kyc/verify/bvn/igree/initiate` with `{ "bvn": "..." }` returns a
    `sessionId`.
  </Step>

  <Step title="Request OTP">
    `POST /kyc/verify/bvn/igree/otp` with `{ "sessionId": "...", "method": "sms" }`
    delivers an OTP to the BVN holder.
  </Step>

  <Step title="Fetch details">
    `POST /kyc/verify/bvn/igree/fetch` with `{ "sessionId": "...", "otp": "..." }`
    returns the consented BVN record.
  </Step>
</Steps>

## Account lookup — `/kyc/nuban/*`

| Method | Path                          | Body                                            |
| ------ | ----------------------------- | ----------------------------------------------- |
| `GET`  | `/kyc/nuban/banks`            | — (lists supported banks with codes & logos)    |
| `POST` | `/kyc/nuban/lookup`           | `{ "accountNumber": "..." }`                    |
| `POST` | `/kyc/nuban/lookup-with-bank` | `{ "accountNumber": "...", "bankCode": "..." }` |
| `POST` | `/kyc/nuban/guess-banks`      | `{ "accountNumber": "..." }`                    |

### Example: NUBAN lookup

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.hyparrow.cloud/api/v1/kyc/nuban/lookup \
    -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
    -H "X-API-Secret: sk_live_xxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{ "accountNumber": "0123456789" }'
  ```
</CodeGroup>

```json Response theme={null}
{
  "success": true,
  "data": {
    "account_number": "0123456789",
    "account_name": "JOHN ADE DOE",
    "banks": [
      { "bank": "Example Bank", "account_name": "JOHN ADE DOE" }
    ]
  }
}
```

To pin the lookup to one bank, use `/kyc/nuban/lookup-with-bank` and supply a
`bankCode` from `GET /kyc/nuban/banks`. Not sure which bank an account belongs
to? `/kyc/nuban/guess-banks` returns the likely candidates.

## Errors & edge cases

Every non-2xx response carries a **`code`** — a stable, machine-readable
classification — alongside a human-readable `error`. Switch on `code`. The
`error` string is prose intended for your logs and may be reworded at any time;
it is not part of the contract.

<Warning>
  **Changed for existing integrations.** An identifier with no record used to
  return `502` with the reason buried in prose. It now returns **`404` with
  `"code": "RECORD_NOT_FOUND"`**, and `502` is reserved for genuine upstream
  failures.

  If you branch on the HTTP status, add the `404` case. If you match on the text
  of `error` or `data.message` to detect a missing record — switch to `code`;
  the strings have changed and were never stable. Success responses are
  unaffected.
</Warning>

| HTTP  | `code`                 | Meaning                                                             | Retry?                     |
| ----- | ---------------------- | ------------------------------------------------------------------- | -------------------------- |
| `400` | `VALIDATION_ERROR`     | The request body was malformed or an identifier was the wrong shape | No — fix the request       |
| `401` | —                      | Missing or invalid API credentials                                  | No                         |
| `402` | `INSUFFICIENT_BALANCE` | PAYG call your wallet cannot cover                                  | Only after funding         |
| `402` | `QUOTA_EXCEEDED`       | Subscription quota spent, or plan doesn't cover this API            | Only after subscribing     |
| `404` | `RECORD_NOT_FOUND`     | The verification network holds no record for the identifier         | **No** — correct the input |
| `502` | `UPSTREAM_ERROR`       | The verification network failed or was unreachable                  | Yes, with backoff          |

<Note>
  **`404` and `502` are deliberately different statuses.** A mistyped BVN is a
  client error — nothing is broken — so it is a `404` and a conventional
  "retry on 5xx" policy leaves it alone. A `502` means the network genuinely
  failed and retrying is the right thing to do. You never need to inspect prose
  to tell them apart.
</Note>

<ResponseField name="404 Not Found" type="RECORD_NOT_FOUND">
  The verification network has no record for the identifier you supplied.
  **You are not charged** — the authorization is rolled back.

  ```json theme={null}
  {
    "success": false,
    "code": "RECORD_NOT_FOUND",
    "error": "no record was found for the details provided",
    "data": {
      "message": "Invalid BVN or BVN does not exist"
    }
  }
  ```

  `data.message` is the verification network's own explanation, always a
  **string** and safe to log. It is descriptive, not a contract — branch on
  `code`, not on this text. `data` is `null` when the network offered no
  explanation.
</ResponseField>

<ResponseField name="502 Bad Gateway" type="UPSTREAM_ERROR">
  The verification network failed, timed out, or was unreachable — or rejected
  us rather than your input. Nothing is wrong with your request; retry with
  backoff. **You are not charged.**

  ```json theme={null}
  {
    "success": false,
    "code": "UPSTREAM_ERROR",
    "error": "the verification network is temporarily unavailable",
    "data": null
  }
  ```
</ResponseField>

<ResponseField name="400 Bad Request" type="VALIDATION_ERROR">
  The request body was rejected before the verification network was called, so
  **the call is free** — a malformed identifier never costs you a lookup.
  `field` names the offending JSON property when the failure is attributable to
  one.

  ```json theme={null}
  {
    "success": false,
    "code": "VALIDATION_ERROR",
    "error": "bvn must be exactly 11 digits",
    "field": "bvn"
  }
  ```

  BVNs and NINs are validated as exactly 11 digits before dispatch, so
  `{"bvn":"1234"}` and `{"bvn":"abcdefghijk"}` are caught here rather than
  being spent upstream.

  A 400 is also returned if the authenticated identity has no API client to
  bill against:

  ```json theme={null}
  {
    "success": false,
    "code": "NO_CLIENT",
    "error": "you must create an API key before running verifications"
  }
  ```
</ResponseField>

<ResponseField name="402 Payment Required" type="INSUFFICIENT_BALANCE / QUOTA_EXCEEDED">
  Either a PAYG call your wallet balance cannot cover, or a subscription whose
  quota for that API is used up (on a `hard_block` plan) or which doesn't cover
  the API at all.

  ```json theme={null}
  {
    "success": false,
    "code": "INSUFFICIENT_BALANCE",
    "error": "Insufficient verification balance"
  }
  ```

  ```json theme={null}
  {
    "success": false,
    "code": "QUOTA_EXCEEDED",
    "error": "Quota exceeded for this API on your current plan"
  }
  ```
</ResponseField>

<ResponseField name="401 Unauthorized" type="Authentication required">
  The API key/secret headers are missing, or don't match a key.

  ```json theme={null}
  { "success": false, "error": "Missing API credentials" }
  { "success": false, "error": "Invalid API credentials" }
  ```
</ResponseField>

<ResponseField name="403 Forbidden" type="Wrong environment or inactive key">
  A test key used against production or a live key against the sandbox, or a key
  that is disabled, expired, or calling from a non-allow-listed IP.

  ```json theme={null}
  {
    "success": false,
    "error": "Live API keys cannot be used against the sandbox. Use your test (pk_test_) key."
  }
  ```
</ResponseField>

### Handling failures

```javascript theme={null}
const res = await fetch(url, { method: "POST", headers, body });
const body = await res.json();

if (!body.success) {
  switch (body.code) {
    case "RECORD_NOT_FOUND":
      return askUserToCheckTheirDetails();   // never retry
    case "VALIDATION_ERROR":
      return showFieldError(body.field, body.error);
    case "INSUFFICIENT_BALANCE":
    case "QUOTA_EXCEEDED":
      return alertOps(body.code);
    case "UPSTREAM_ERROR":
      return retryWithBackoff();
  }
}
```

<Note>
  **Driving these paths in the sandbox.** An identifier of all ones
  (`11111111111`) returns the `RECORD_NOT_FOUND` 404; any other well-formed
  value succeeds. Send a short or non-numeric BVN for the 400. Fund and drain a
  test wallet to exercise the 402 paths. The magic values do not exist in
  production.
</Note>

## Canonical paths

The verification groups are **`/kyc/identity/*`**, **`/kyc/verify/*`** and
**`/kyc/nuban/*`**. These are the only supported paths and the only ones that
appear in the [API reference](/api-reference).

<Note>
  **On an older path?** Integrations written before these routes were
  introduced may be calling a legacy prefix named after the underlying provider.
  Those aliases still work and return byte-identical responses, but they are
  deprecated: they are not documented, they will not gain new endpoints, and
  they leak an implementation detail into your codebase.

  Migrate by swapping the prefix — the rest of the path, the request body and
  the response are unchanged. If you are unsure which prefix you are on, or want
  a removal timeline before you plan the work, contact
  [support@hyparrow.com](mailto:support@hyparrow.com).
</Note>
