KYC
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 returns kyc, and the status of its basic entry becomes completed or failed.
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 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.
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 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 endpoint to let a customer upload their identity documents on a hosted page. The endpoint needs only the customer's id:
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:
{
"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 endpoint, then call the Initiate server-to-server verification endpoint.
Upload documents
Upload each required document with the 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.
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:
{
"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. 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
contentand does not check thefilenameextension. - 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 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:
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 endpoint with the customer's id:
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:
{
"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:
{
"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 endpoint.
While the review is in progress, the value of the reviewStatus field is pending and there's no reviewResult field:
{
"data": {
"reviewStatus": "pending",
"priority": 0
},
"error": null
}
Once the review finishes, reviewStatus is completed and reviewResult holds the decision:
{
"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:
{
"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:pendingwhile the review is in progress, orcompletedonce a decision is returned.reviewResult: the decision, present once the review is completed.reviewAnswer:approvedorrejected.rejectLabels: reason codes for a rejection, present only whenreviewAnswerisrejected.reviewRejectType: the rejection category, such asRETRY, present only whenreviewAnswerisrejected.
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 endpoint. It returns the metadata for each uploaded document, newest first:
{
"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
}
IDNumber is the full document number, in plain text. Do not log it, and show it only to people allowed to see identity data.
- 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_SIDEand oneBACK_SIDE. - To get a document's image or PDF, see Download KYC documents.