---
title: "KYC"
description: "How to verify a customer's identity, through a hosted page or the API, and how to read the result."
---
Know your customer (KYC) verification confirms a customer's identity before you create an account or activate some features. This page shows how to submit documents, start verification and read the result.

You can submit documents for verification in two ways, through a hosted page or through the API. Whichever way you use, Gravv sends the outcome as a `customer.kyc.status.completed` or `customer.kyc.status.failed` webhook and records it on the customer. [Get a customer](/api-reference/customers/get-v1-customers-customer-id) returns `kyc`, and the `status` of its `basic` entry becomes `completed` or `failed`.

<Note>
Gravv bills verification to your business, at your pricing. Gravv bills a hosted start once. Starting again after a failure or an expired link gives a new link at no extra fee. Gravv bills every [server-to-server start](#initiate-verification) again, even while a review is pending, so do not call it in a loop.

Sandbox KYC does not complete today. The `basic` entry stays `pending`, and Gravv sends no outcome webhook.
</Note>

## Required documents

Individual customers must provide a selfie and one government-issued identity document, with at least its front side:

- Passport
- National ID card
- Driver's license

Business customers must provide four company documents: a certificate of incorporation, a shareholder registry, a proof of address, and a tax document. **Verify the people first.** Before you start verification for a business, every associated person linked to it must have `basic` KYC `completed`. Verify each person with their own customer id. Otherwise the start returns 400 `INVALID_REQUEST: associated person "Tunde Bakare" must complete basic KYC before business verification can proceed`.

**A money example.** Adire Logistics wants to pay a 12,000.00 USD freight invoice. You verify its director Tunde first, upload the four company documents, then verify the business. With `basic` KYC `completed`, the business can open an account and, on the standard limits, move up to 50,000.00 USD a day.

### Businesses with automatic verification

Gravv can set up your business so that KYC completes from the uploaded documents alone. For such a business, the hosted start returns 400 `INVALID_REQUEST: this tenant uses document-based auto verification; upload required documents instead`. Instead, [upload the documents](#upload-documents) and call the server-to-server start, which returns `{"status": "completed", "message": "KYC auto-approved for this tenant."}`.

## Submit through the hosted page

Use the [Start KYC verification](/api-reference/kyc/post-v1-customers-kyc-start) endpoint to let a customer upload their identity documents on a hosted page. The endpoint needs only the customer's id:

```bash title="Basic request"
curl --request POST \
     --url https://api.gravv.xyz/v1/customers/kyc/start \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 2b7c9e10-4d5f-4a6b-8c7d-0e1f2a3b4c01' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "customer_id": "c239895a-0436-4287-a37a-c2664f68d187"
}
'
```

The response includes the verification status and a `web_url` link. Share this link with the customer so they can upload their ID documents and complete a liveness check. The link is valid for one hour. If it expires, or the review fails, call the endpoint again with a **new** `Idempotency-Key`. You get a fresh link at no extra fee. The same key replays the old link. For a customer whose KYC is already `completed`, the response has `review_status` `completed` and no link.

A pending response looks like this:

```json title="Basic response"
{
  "data": {
    "review_status": "pending",
    "status": "success",
    "web_url": "https://<verification-host>/websdk/p/…"
  },
  "error": null
}
```

## Submit through the API

To submit documents through the API, Base64-encode each document, upload it with the [Upload document](/api-reference/kyc/post-v1-customers-kyc-upload-document) endpoint, then call the [Initiate server-to-server verification](/api-reference/kyc/post-v1-customers-kyc-start-s2s) endpoint.

### Upload documents

Upload each required document with the [Upload document](/api-reference/kyc/post-v1-customers-kyc-upload-document) endpoint. Encode the document in Base64 and send the raw Base64 string, not a `data:` URI, as the `content` value. Upload each side of a two-sided document as its own request. The `Idempotency-Key` header is required.

```bash title="Upload passport request"
curl --request POST \
     --url https://api.gravv.xyz/v1/customers/kyc/upload-document \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 2b7c9e10-4d5f-4a6b-8c7d-0e1f2a3b4c02' \
     --header 'content-type: application/json' \
     --data '
{
  "customer_id": "aabe0a33-6716-42e2-bbca-7abf1a8bd91c",
  "metadata": {
    "idDocType": "PASSPORT",
    "idDocSubType": "FRONT_SIDE",
    "country": "NGA",
    "firstName": "John",
    "lastName": "Doe",
    "number": "A12345678",
    "issuedDate": "2020-01-15",
    "validUntil": "2030-01-15"
  },
  "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAY...",
  "filename": "passport_front.jpg"
}
'
```

A successful upload returns a confirmation:

```json title="Upload passport response"
{
  "data": {
    "status": "success",
    "customer_id": "aabe0a33-6716-42e2-bbca-7abf1a8bd91c",
    "document_type": "PASSPORT",
    "document_sub_type": "FRONT_SIDE",
    "document_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
    "country": "NGA",
    "message": "Document saved. Use /start-kyc to submit for verification."
  },
  "error": null
}
```

The message names `/start-kyc`, but for documents you upload yourself the next call is [Initiate server-to-server verification](#initiate-verification). Uploading the same type and sub-type again replaces the earlier document.

The `metadata` object describes the document. `idDocType` and `idDocSubType` values are uppercase, and `country` is a 2-letter or 3-letter uppercase ISO code.

| Field          | Required for                       | Description                                                                                                                                                                                                     |
| :------------- | :--------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `idDocType`    | All documents                      | Document type. Individual customers: `PASSPORT`, `ID_CARD`, `DRIVERS`, `SELFIE`, `PROOF_OF_ADDRESS`, or `TAX_DECLARATION`. Business customers: `COMPANY_DOC`.                                                 |
| `idDocSubType` | Identity and company documents     | Identity documents: `FRONT_SIDE` or `BACK_SIDE`. `COMPANY_DOC`: `INCORPORATION_CERT`, `SHAREHOLDER_REGISTRY`, `PROOF_OF_ADDRESS`, `TAX_DOCUMENT`, or `OTHER`. Omit for `SELFIE`, `PROOF_OF_ADDRESS` and `TAX_DECLARATION`. |
| `country`      | All documents except `SELFIE`      | Issuing country as a 2-letter or 3-letter uppercase ISO code, such as `NG` or `NGA`.                                                                                                                            |
| `number`       | `PASSPORT`, `ID_CARD`, `DRIVERS`   | Document identification number.                                                                                                                                                                                 |
| `validUntil`   | `PASSPORT`, `ID_CARD`, `DRIVERS`   | Expiration date in `YYYY-MM-DD` format.                                                                                                                                                                         |
| `issuedDate`   | Optional                           | Issue date in `YYYY-MM-DD` format.                                                                                                                                                                              |
| `firstName`    | Optional                           | First name as shown on the document.                                                                                                                                                                            |
| `lastName`     | Optional                           | Last name as shown on the document.                                                                                                                                                                             |
| `middleName`   | Optional                           | Middle name as shown on the document.                                                                                                                                                                           |
| `dob`          | Optional                           | Date of birth in `YYYY-MM-DD` format.                                                                                                                                                                           |
| `placeOfBirth` | Optional                           | Place of birth as shown on the document.                                                                                                                                                                        |

Uploads follow these rules:

- The file must be a JPEG, PNG or PDF. Gravv reads the type from the decoded `content` and does not check the `filename` extension.
- The maximum document size is 10 MB. In sandbox, keep each file under 500 KB. Sandbox rejects requests of about 600 KB or more with a 413.
- Gravv does not deduplicate a request body over 1 MiB under its `Idempotency-Key`. A file over about 770 KB makes a body that large, so a retried upload of it runs again instead of returning the first response. After a timeout, read the [documents list](/api-reference/kyc/get-v1-customers-customer-id-kyc-documents) before you upload again.
- Gravv does not check dates on upload. The review can reject a document with a wrong date, so send dates as `YYYY-MM-DD`.

A business customer uploads four company documents. Each uses `idDocType` `COMPANY_DOC`, with one of the sub-types `INCORPORATION_CERT`, `SHAREHOLDER_REGISTRY`, `PROOF_OF_ADDRESS`, and `TAX_DOCUMENT` (or `OTHER`). Gravv does not accept `REGISTRATION_CERTIFICATE`. This request uploads the certificate of incorporation:

```bash title="Upload certificate of incorporation request"
curl --request POST \
     --url https://api.gravv.xyz/v1/customers/kyc/upload-document \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 2b7c9e10-4d5f-4a6b-8c7d-0e1f2a3b4c03' \
     --header 'content-type: application/json' \
     --data '
{
  "customer_id": "c84d9a52-2978-43e5-9b86-eac9839e1147",
  "metadata": {
    "idDocType": "COMPANY_DOC",
    "idDocSubType": "INCORPORATION_CERT",
    "country": "NGA"
  },
  "content": "JVBERi0xLjcKJ...",
  "filename": "certificate_of_incorporation.pdf"
}
'
```

Repeat the request with a new `Idempotency-Key` for `SHAREHOLDER_REGISTRY`, `PROOF_OF_ADDRESS` and `TAX_DOCUMENT`.

### Initiate verification

After you upload all required documents, call the [Initiate server-to-server verification](/api-reference/kyc/post-v1-customers-kyc-start-s2s) endpoint with the customer's id:

```bash title="Initiate verification request"
curl --request POST \
     --url https://api.gravv.xyz/v1/customers/kyc/start-s2s \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 2b7c9e10-4d5f-4a6b-8c7d-0e1f2a3b4c04' \
     --header 'content-type: application/json' \
     --data '
{
  "customer_id": "aabe0a33-6716-42e2-bbca-7abf1a8bd91c"
}
'
```

Gravv starts the review and sends the outcome as a webhook. Gravv bills each call. Call it once, and again only after you replace what a rejection named.

For a pending verification, the API returns:

```json title="Initiate verification response"
{
  "data": {
    "status": "pending",
    "message": "KYC verification initiated. Status will be updated via webhook."
  },
  "error": null
}
```

If a required document is missing, the request fails with HTTP 400 and a message that lists only what is missing. Errors on the KYC endpoints are strings that start with a code:

```json title="Missing documents response"
{
  "data": null,
  "error": "INVALID_REQUEST: missing required documents: Identity Document (ID card, passport, or driver's license), Selfie"
}
```

For a business customer, the message lists the missing company documents, for example `INVALID_REQUEST: missing required business documents: Certificate of Incorporation (INCORPORATION_CERT), Tax Document (TAX_DOCUMENT)`. If the customer's KYC is already completed, the request returns HTTP 409 `KYC_ALREADY_COMPLETED: KYC verification has already been completed for this customer.`

Verification through the API records no liveness check. A customer verified this way completes a liveness step later if they apply for a card.

## Check verification status

The final decision is the customer's `basic` KYC entry, or the `customer.kyc.status.*` webhook. To see where the review stands in the meantime, call the [Get applicant verification status](/api-reference/kyc/get-v1-customers-customer-id-kyc-status) endpoint.

While the review is in progress, the value of the `reviewStatus` field is `pending` and there's no `reviewResult` field:

```json title="Pending status response"
{
  "data": {
    "reviewStatus": "pending",
    "priority": 0
  },
  "error": null
}
```

Once the review finishes, `reviewStatus` is `completed` and `reviewResult` holds the decision:

```json title="Approved status response"
{
  "data": {
    "reviewStatus": "completed",
    "reviewResult": {
      "reviewAnswer": "approved"
    },
    "reviewDate": "2026-06-19 21:59:00+0000",
    "priority": 0
  },
  "error": null
}
```

A rejected review lists the reason codes in `rejectLabels`, and `reviewRejectType` shows whether the customer can resubmit:

```json title="Rejected status response"
{
  "data": {
    "reviewStatus": "completed",
    "reviewResult": {
      "reviewAnswer": "rejected",
      "rejectLabels": [
        "BAD_SELFIE",
        "SCREENSHOTS",
        "UNSATISFACTORY_PHOTOS"
      ],
      "reviewRejectType": "RETRY"
    },
    "reviewDate": "2026-06-19 20:44:19+0000",
    "priority": 0
  },
  "error": null
}
```

The status `data` object contains:

- `reviewStatus`: `pending` while the review is in progress, or `completed` once a decision is returned.
- `reviewResult`: the decision, present once the review is completed.
  - `reviewAnswer`: `approved` or `rejected`.
  - `rejectLabels`: reason codes for a rejection, present only when `reviewAnswer` is `rejected`.
  - `reviewRejectType`: the rejection category, such as `RETRY`, present only when `reviewAnswer` is `rejected`.
- `reviewDate`: the decision timestamp, present once the review is completed.
- `priority`: the applicant's review priority.

## Get uploaded documents

To see which documents a customer has uploaded, call the [Get KYC document data](/api-reference/kyc/get-v1-customers-customer-id-kyc-documents) endpoint. It returns the metadata for each uploaded document, newest first:

```json title="Uploaded documents response"
{
  "data": {
    "customer_id": "aabe0a33-6716-42e2-bbca-7abf1a8bd91c",
    "documents": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "IDType": "PASSPORT",
        "IDSubType": "FRONT_SIDE",
        "IDNumber": "A12345678",
        "IssuingCountry": "NGA",
        "IssuedDate": "2020-01-01",
        "ExpiryDate": "2030-01-01",
        "has_front_image": true,
        "has_back_image": false,
        "OriginalFilename": "passport_front.jpg"
      }
    ],
    "count": 1
  },
  "error": null
}
```

<Warning>
`IDNumber` is the full document number, in plain text. Do not log it, and show it only to people allowed to see identity data.
</Warning>

- For a customer with no uploaded documents, the call returns 404 `no KYC document data found for customer`, not an empty list. Treat it as "no documents".
- A two-sided ID is two rows, one `FRONT_SIDE` and one `BACK_SIDE`.
- To get a document's image or PDF, see [Download KYC documents](/getting-started/download-kyc-documents).
