Resubmit KYC after a rejection
Read why KYC failed, then send the person back to the hosted page or replace the rejected document, without creating a new customer.
This recipe reads why KYC failed, then sends the person back to the hosted page or replaces the rejected document, without creating a new customer.
Who it is for. This recipe is for the remittance app from Verify a person with documents you upload, after Kofi's verification came back failed because his selfie was blurred.
Status. Available. You cannot test it in sandbox today, because sandbox never records a rejection.
Money example
- Kofi is rejected, so he stays on the lowest limits, with a cap of 500.00 USD a day.
- His 800.00 USD rent payout would pause on the daily limit.
- He resubmits a clear selfie and is approved. His limit becomes 50,000.00 USD a day, and the 800.00 USD goes through.
Before you start
- Kofi's
basicrow isfailed. To check, readkyc[type=basic].statusonGET /v1/customers/{customer_id}. - Know whether he was verified the first time on the hosted page or with documents you uploaded.
Find out what is required
- The reason. Read
failure_reasonin thecustomer.kyc.status.failedwebhook, orremarkon thebasicrow. It holds the reviewer's comment, the reasons as sentences, or onlyVerification failed. - The detail. Read
rejectLabelsandreviewRejectTypein the review status thatGET /v1/customers/{customer_id}/kyc/statusreturns.
curl --request GET \
--url https://api.gravv.xyz/v1/customers/9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13/kyc/status \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/customers/9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13/kyc/status", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.get(
"https://api.gravv.xyz/v1/customers/9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13/kyc/status",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"reviewStatus": "completed",
"reviewResult": {
"reviewAnswer": "rejected",
"rejectLabels": ["BAD_SELFIE"],
"reviewRejectType": "RETRY"
},
"reviewDate": "2026-10-13 11:20:40+0000",
"priority": 0
},
"error": null
}
RETRYmeans the person may resubmit. Retake only what the labels name.FINALmeans the reviewer does not expect a resubmission to pass. Contact support rather than trying again.
Steps
If you used the hosted page
Start again with a new key. From failed, the start returns a new link, valid for 1 hour, and sets the row back to pending, with no new fee.
curl --request POST \
--url https://api.gravv.xyz/v1/customers/kyc/start \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: 2c8a4d99-2860-4d9c-bd69-a2abcc0ce82e' \
--header 'Content-Type: application/json' \
--data '
{
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13"
}
'
const response = await fetch("https://api.gravv.xyz/v1/customers/kyc/start", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "2c8a4d99-2860-4d9c-bd69-a2abcc0ce82e",
"Content-Type": "application/json",
},
body: JSON.stringify({
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/customers/kyc/start",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "2c8a4d99-2860-4d9c-bd69-a2abcc0ce82e",
"Content-Type": "application/json",
},
json={
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13",
},
)
print(response.status_code, response.json())
{
"data": {
"status": "success",
"review_status": "pending",
"web_url": "https://<verification-host>/websdk/p/R7u2LwQ8sd"
},
"error": null
}
Send Kofi the link. He retakes only what was rejected.
If you uploaded the documents
- Upload the rejected document again with the same
idDocTypeandidDocSubType, hereSELFIE. The new file replaces the old one.
curl --request POST \
--url https://api.gravv.xyz/v1/customers/kyc/upload-document \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: c5df1c69-b7f1-4abf-a416-2667956a483d' \
--header 'Content-Type: application/json' \
--data '
{
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13",
"filename": "selfie-retake.jpg",
"content": "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYI",
"metadata": {
"idDocType": "SELFIE"
}
}
'
const response = await fetch("https://api.gravv.xyz/v1/customers/kyc/upload-document", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "c5df1c69-b7f1-4abf-a416-2667956a483d",
"Content-Type": "application/json",
},
body: JSON.stringify({
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13",
"filename": "selfie-retake.jpg",
"content": "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYI",
"metadata": {
"idDocType": "SELFIE"
}
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/customers/kyc/upload-document",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "c5df1c69-b7f1-4abf-a416-2667956a483d",
"Content-Type": "application/json",
},
json={
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13",
"filename": "selfie-retake.jpg",
"content": "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYI",
"metadata": {
"idDocType": "SELFIE",
},
},
)
print(response.status_code, response.json())
- Start verification again with a new key. Gravv bills this start again, so first confirm that the upload replaced the right document.
curl --request POST \
--url https://api.gravv.xyz/v1/customers/kyc/start-s2s \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: a47a1d26-8526-4ea3-890b-d8e7b747c770' \
--header 'Content-Type: application/json' \
--data '
{
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13"
}
'
const response = await fetch("https://api.gravv.xyz/v1/customers/kyc/start-s2s", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "a47a1d26-8526-4ea3-890b-d8e7b747c770",
"Content-Type": "application/json",
},
body: JSON.stringify({
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/customers/kyc/start-s2s",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "a47a1d26-8526-4ea3-890b-d8e7b747c770",
"Content-Type": "application/json",
},
json={
"customer_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13",
},
)
print(response.status_code, response.json())
Then wait
Wait for customer.kyc.status.completed or customer.kyc.status.failed, as you did the first time.
If the customer record was wrong
If a name, date of birth or address on the customer was wrong, fix it first with PUT /v1/customers/{customer_id}. Only the fields you send change, and you cannot blank a field. Editing the record does not reset KYC.
What can go wrong
| Response | Why | Fix |
|---|---|---|
| The start returns the old, expired link | You reused the first key, so Gravv replayed the stored response | Use a new key |
409 KYC_ALREADY_COMPLETED: … on start-s2s |
He was approved already, for example by an earlier retry | You don't need to do anything |
| The same rejection again | You sent the same file again, or replaced a different side or type | Read rejectLabels, then replace that exact type and sub-type |
failed again with reviewRejectType FINAL |
The reviewer made a final decision | Contact support. Do not keep resubmitting, because Gravv bills each server-to-server start |
PUT 400 only company emails are allowed when changing an individual's email |
The company-email rule applies to individuals on update too, a known issue | Change only the fields that were wrong. Contact support if the customer needs a free-mail address |
PUT 400 failed to update customer: email already exists |
The email belongs to another customer | Use another email |
Webhooks you will receive
You receive customer.kyc.status.pending on the restart, then customer.kyc.status.completed or customer.kyc.status.failed with failure_reason. See Customer and KYC events.
Testing in sandbox
You cannot test this recipe end to end in sandbox. Sandbox reviews reject test images, but the row stays pending and Gravv sends no failed webhook. Sandbox never produces a failed row. You can test the calls. A start from pending with a new key returns a new link.