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

  1. Kofi is rejected, so he stays on the lowest limits, with a cap of 500.00 USD a day.
  2. His 800.00 USD rent payout would pause on the daily limit.
  3. 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 basic row is failed. To check, read kyc[type=basic].status on GET /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

  1. The reason. Read failure_reason in the customer.kyc.status.failed webhook, or remark on the basic row. It holds the reviewer's comment, the reasons as sentences, or only Verification failed.
  2. The detail. Read rejectLabels and reviewRejectType in the review status that GET /v1/customers/{customer_id}/kyc/status returns.
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
}
  • RETRY means the person may resubmit. Retake only what the labels name.
  • FINAL means 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

  1. Upload the rejected document again with the same idDocType and idDocSubType, here SELFIE. 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())
  1. 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.

Reference

Was this page helpful?