---
title: "Card KYC"
description: "Verify a customer who will get a card, with a liveness check, through a hosted page or the API."
---
Card KYC verifies a customer who will get a card. The hosted page captures a liveness check in the same step, and card activation needs that check. For general customer verification, use [KYC](/getting-started/kyc) instead. See [Which KYC flow to use](/platform/cards/card-kyc/overview#which-kyc-flow-to-use).

<Warning>
Never call [Start KYC verification](/api-reference/card-kyc/post-v1-risk-start-kyc) for a customer whose KYC status is anything other than `not_started`. For a customer who is `pending`, `failed` or already `completed`, the call starts the review again and sets their KYC to `pending`. A `completed` customer loses that status until the new review finishes. Check the customer's `kyc` `basic` status with [Get a customer](/api-reference/customers/get-v1-customers-customer-id) first.
</Warning>

## Required documents

For individual customers, upload a selfie and one of the following government-issued IDs:

* Passport
* National ID card
* Driver's license

For business customers, upload one company document.

You can submit documents through a hosted page or through the API.

Gravv records the outcome on the customer. [Get a customer](/api-reference/customers/get-v1-customers-customer-id) returns `kyc`, and the `status` of the `basic` entry becomes `completed` or `failed`. Gravv also reports the outcome with a `customer.kyc.status.completed` or `customer.kyc.status.failed` webhook. Gravv sends no `pending` event for card KYC today, so read the customer to see that a review has started.

## Submit through a hosted page

Use the [Start KYC verification](/api-reference/card-kyc/post-v1-risk-start-kyc) endpoint to send the customer to a hosted page, where they upload their ID documents and complete a liveness check. Send the customer's `customer_id`:

```bash title="basic KYC request"
curl --request POST \
     --url https://api.gravv.xyz/v1/risk/start-kyc \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 5a1e7c2d-3b4f-4d6e-8a9b-0c1d2e3f4a01' \
     --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. The link is valid for one hour:

```json title="basic KYC response"
{
  "data": {
    "applicant_id": "66f0c2a1b3c4d5e6f7a8b9c0",
    "external_user_id": "c239895a-0436-4287-a37a-c2664f68d187",
    "kyc_level": "basic",
    "review_status": "pending",
    "status": "success",
    "web_url": "https://<verification-host>/websdk/p/…"
  },
  "error": null
}
```

Every error from this endpoint is HTTP 400 with a message in `error`.

## Submit through the API

To submit IDs through the API, encode them in Base64 and send them with the [Upload document](/api-reference/card-kyc/post-v1-risk-upload-document) and [Initiate server-to-server verification](/api-reference/card-kyc/post-v1-risk-start-kyc-s2s) endpoints. Verification through the API records no liveness check, so card activation then returns a liveness link for the cardholder.

### Upload documents

First, upload each required document with the [Upload document](/api-reference/card-kyc/post-v1-risk-upload-document) endpoint. Encode each document in Base64 and send it as the value of the `content` field. Upload the back of a two-sided ID in a second request with `idDocSubType` `BACK_SIDE`.

```bash title="upload passport request"
curl --request POST \
     --url https://api.gravv.xyz/v1/risk/upload-document \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 5a1e7c2d-3b4f-4d6e-8a9b-0c1d2e3f4a02' \
     --header 'content-type: application/json' \
     --data '
{
  "customer_id": "aabe0a33-6716-42e2-bbca-7abf1a8bd91c",
  "metadata": {
    "idDocType": "passport",
    "idDocSubType": "FRONT_SIDE",
    "country": "USA",
    "firstName": "John",
    "lastName": "Doe",
    "number": "P123456789",
    "issuedDate": "2020-01-15",
    "validUntil": "2030-01-15",
    "dob": "1990-05-20",
    "placeOfBirth": "New York, NY"
  },
  "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAY...",
  "filename": "passport_front.jpg"
}
'
```

A successful upload returns `status` `success`:

```json title="upload passport response"
{
  "data": {
    "country": "USA",
    "customer_id": "aabe0a33-6716-42e2-bbca-7abf1a8bd91c",
    "document_sub_type": "FRONT_SIDE",
    "document_type": "PASSPORT",
    "message": "Document saved. Use /initiate-kyc to submit for verification.",
    "status": "success"
  },
  "error": null
}
```

The message names `/initiate-kyc`, but the next call is [Initiate server-to-server verification](#initiate-verification).

A request that fails validation also returns HTTP 200, with `data.status` `error`. Treat an upload as saved only when `data.status` is `success`:

```json title="upload validation error response"
{
  "data": {
    "error_code": "INVALID_REQUEST",
    "message": "The provided data format is invalid. Please check your request and try again.",
    "status": "error"
  },
  "error": null
}
```

Each document upload takes these metadata fields:

| Field          | Required for             | Description                                                                                                  |
| :------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------- |
| `idDocType`    | All documents            | Document type, in lower case: `passport`, `id_card`, `drivers_license`, `selfie`, or for a business `company_doc` |
| `country`      | All documents            | Issuing country as a 3-letter ISO code, such as `USA`, `GBR`, `NGA`. Required for a selfie too                |
| `idDocSubType` | ID documents             | Document side: `FRONT_SIDE` or `BACK_SIDE`                                                                   |
| `number`       | ID documents (optional)  | Document identification number                                                                               |
| `issuedDate`   | ID documents (optional)  | Issue date in YYYY-MM-DD format                                                                              |
| `validUntil`   | ID documents (optional)  | Expiration date in YYYY-MM-DD format                                                                         |
| `dob`          | Optional                 | Date of birth in YYYY-MM-DD format                                                                           |
| `firstName`    | Optional                 | First name on document                                                                                       |
| `lastName`     | Optional                 | Last name on document                                                                                        |
| `middleName`   | Optional                 | Middle name on document                                                                                      |
| `placeOfBirth` | Optional                 | Place of birth as shown on document                                                                          |

Each upload must also meet these rules:

- `filename` must end in `.jpg`, `.jpeg`, `.png` or `.pdf`, and the content must be a JPEG, PNG or PDF. A selfie must be an image.
- Keep each file under 5 MB. In sandbox, keep each file under 500 KB. Sandbox refuses requests from about 600 KB with a 413.
- Gravv does not deduplicate a request body over 1 MiB under its `Idempotency-Key`. A retried upload of a file over about 770 KB therefore runs again instead of returning the first response. The retry replaces the earlier file of the same type and side.

### Initiate verification

After you upload all required documents, call the [Initiate server-to-server verification](/api-reference/card-kyc/post-v1-risk-start-kyc-s2s) endpoint:

```bash title="initiate verification request"
curl --request POST \
     --url https://api.gravv.xyz/v1/risk/start-kyc-s2s \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 5a1e7c2d-3b4f-4d6e-8a9b-0c1d2e3f4a03' \
     --header 'content-type: application/json' \
     --data '
{
  "customer_id": "aabe0a33-6716-42e2-bbca-7abf1a8bd91c"
}
'
```

The endpoint returns HTTP 200 with one of these outcomes. Branch on `data.status` and `data.code`:

* **Submitted.** Gravv sent all documents and started verification:

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

Wait for the outcome on the customer record or in a webhook.

* **Missing documents.** You have not uploaded all the required documents yet:

```json title="missing documents response"
{
  "data": {
    "code": "MISSING_DOCUMENTS",
    "details": {
      "missing_documents": [
        "SELFIE"
      ]
    },
    "message": "Required documents have not been uploaded for this customer"
  },
  "error": null
}
```

Upload the documents listed in `details.missing_documents` and call the endpoint again with a new `Idempotency-Key`. A customer whose KYC is already completed gets `data.code` `KYC_ALREADY_COMPLETED` instead.

* **Partial success.** Gravv could not send some documents. The response includes a `web_url` where the customer can finish verification:

```json title="partial success response"
{
  "data": {
    "applicant_id": "507f1f77bcf86cd799439011",
    "failed_documents": [
      {
        "doc_type": "SELFIE",
        "reason": "Upload failed"
      }
    ],
    "message": "Upload failed",
    "status": "partial_success",
    "web_url": "https://<verification-host>/websdk/p/…"
  },
  "error": null
}
```

Other errors are HTTP 400 with a message in `error`.
