---
title: "Upload document"
description: "Upload a customer document for KYC or KYB verification. This is the first step of the server-to-server verification flow."
protocol: rest
method: POST
endpoint: "/v1/risk/upload-document"
baseUrl: "https://api.gravv.xyz"
group: "Risk (deprecated)"
deprecated: true
auth:
  label: "Api-Key"
  name: "Api-Key"
  in: header
---

# Upload document

<span class="api-deprecated-badge">Deprecated</span>

<Endpoint method="POST" path="/v1/risk/upload-document" />

Upload a customer document for KYC or KYB verification. This is the first step of the server-to-server verification flow.

Documents are uploaded as base64-encoded strings along with metadata about the document type, 
customer information, and document details. The document is validated and stored in preparation 
for verification.

## Supported document types

The `metadata.idDocType` field accepts the following values (lowercase):

- **Individual customers:** passport, id_card, drivers_license, selfie,
  residence_permit, utility_bill, bank_card,
  vehicle_registration_certificate, other

- **Business customers:** registration_certificate, company_doc,
  formation_document, ownership_document, proof_of_address,
  proof_of_address_document


Selfie documents must be an image (JPEG or PNG); PDF selfies are rejected.

## Document subtypes

The supported document subtypes are as follows (case-sensitive, uppercase):

- **FRONT_SIDE:** Front side of document
- **BACK_SIDE:** Back side of document (if applicable)

## File format

The `filename` extension and the decoded `content` MIME type must agree and must be one of: `image/jpeg`, `image/png`, `application/pdf`.

## Size limits

The maximum decoded document size is 10 MB. The base64-encoded payload sent in `content` may be up to roughly 13.3 MB. Larger payloads are rejected at the ingress layer with a 413 before reaching the service.

## Idempotency

The `Idempotency-Key` header is required on this endpoint. Replays of the same key with the same payload return the original response; replays with a different payload return 422.

After uploading all required documents, call the Initiate KYC S2S
endpoint to start the verification process.

## Headers

<ParamField name="Idempotency-Key" header="Idempotency-Key" type="string" required>
  Unique key to dedupe retries. Required on this endpoint; 1–255 ASCII printable characters. Missing or malformed keys return 400.
</ParamField>

## Request Body

<ParamField name="customer_id" body="customer_id" type="string" required>
  ID of the customer this document belongs to
</ParamField>

<ParamField name="applicant_id" body="applicant_id" type="string">
  Optional upstream applicant identifier. When omitted (server-to-server flow) the service resolves the applicant from `customer_id`.
</ParamField>

<ParamField name="metadata" body="metadata" type="object" required>

  <Expandable title="properties">
    <ParamField name="idDocType" body="metadata.idDocType" type="string" required>
      Type of document being uploaded. Individual customer documents include passport, id_card, drivers_license, selfie, residence_permit, utility_bill, bank_card, vehicle_registration_certificate, and other. Business customer documents include registration_certificate, company_doc, formation_document, ownership_document, proof_of_address, and proof_of_address_document.

      Possible values: `passport`, `id_card`, `drivers_license`, `selfie`, `residence_permit`, `utility_bill`, `bank_card`, `vehicle_registration_certificate`, `other`, `registration_certificate`, `company_doc`, `formation_document`, `ownership_document`, `proof_of_address`, `proof_of_address_document`
    </ParamField>

    <ParamField name="idDocSubType" body="metadata.idDocSubType" type="string">
      Subtype indicating which side of the document. Must be uppercase; lowercase values are passed through to the upstream verifier and may be silently rejected.

      Possible values: `FRONT_SIDE`, `BACK_SIDE`
    </ParamField>

    <ParamField name="country" body="metadata.country" type="string" required>
      Document issuing country (ISO 3166-1 alpha-3 code, uppercase)
    </ParamField>

    <ParamField name="firstName" body="metadata.firstName" type="string">
      First name as shown on document
    </ParamField>

    <ParamField name="middleName" body="metadata.middleName" type="string">
      Middle name as shown on document (optional)
    </ParamField>

    <ParamField name="lastName" body="metadata.lastName" type="string">
      Last name as shown on document
    </ParamField>

    <ParamField name="issuedDate" body="metadata.issuedDate" type="string">
      Document issue date (YYYY-MM-DD)
    </ParamField>

    <ParamField name="validUntil" body="metadata.validUntil" type="string">
      Document expiration date (YYYY-MM-DD)
    </ParamField>

    <ParamField name="number" body="metadata.number" type="string">
      Document identification number
    </ParamField>

    <ParamField name="dob" body="metadata.dob" type="string">
      Date of birth (YYYY-MM-DD)
    </ParamField>

    <ParamField name="placeOfBirth" body="metadata.placeOfBirth" type="string">
      Place of birth as shown on document
    </ParamField>

  </Expandable>
</ParamField>

<ParamField name="content" body="content" type="string" required>
  Base64-encoded document content. The decoded bytes must be a JPEG, PNG, or PDF; the service sniffs the leading bytes and rejects other MIME types. Maximum decoded size is 10 MB.
</ParamField>

<ParamField name="filename" body="filename" type="string" required>
  Original filename of the document. Extension must be one of `.jpg`, `.jpeg`, `.png`, `.pdf` (case-insensitive); the file extension is validated before the content MIME sniff.
</ParamField>

<ParamField name="return_doc_warnings" body="return_doc_warnings" type="boolean" default="false">
  Whether to return document quality warnings in the response
</ParamField>

### Request Examples

<RequestExample>

```json title="PassportUpload"
{
  "customer_id": "b84d9a52-2978-43e5-9b86-eac9839e1146",
  "metadata": {
    "idDocType": "passport",
    "idDocSubType": "FRONT_SIDE",
    "country": "USA",
    "firstName": "John",
    "middleName": "Michael",
    "lastName": "Doe",
    "issuedDate": "2020-01-15",
    "validUntil": "2030-01-15",
    "number": "A12345678",
    "dob": "1990-05-20",
    "placeOfBirth": "New York, NY"
  },
  "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
  "filename": "passport_front.jpg",
  "return_doc_warnings": true
}
```

```json title="SelfieUpload"
{
  "customer_id": "b84d9a52-2978-43e5-9b86-eac9839e1146",
  "metadata": {
    "idDocType": "selfie",
    "idDocSubType": "FRONT_SIDE",
    "country": "NGA",
    "firstName": "John",
    "lastName": "Doe",
    "dob": "1990-05-20"
  },
  "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
  "filename": "selfie.jpg",
  "return_doc_warnings": false
}
```

```json title="BusinessRegistrationUpload"
{
  "customer_id": "c84d9a52-2978-43e5-9b86-eac9839e1147",
  "metadata": {
    "idDocType": "registration_certificate",
    "idDocSubType": "FRONT_SIDE",
    "country": "NGA",
    "firstName": "Acme",
    "lastName": "Corporation",
    "number": "RC123456",
    "issuedDate": "2020-01-15"
  },
  "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
  "filename": "registration_certificate.pdf",
  "return_doc_warnings": true
}
```

```json title="IDCardFrontSide"
{
  "customer_id": "b84d9a52-2978-43e5-9b86-eac9839e1146",
  "metadata": {
    "idDocType": "id_card",
    "idDocSubType": "FRONT_SIDE",
    "country": "NGA",
    "firstName": "Jane",
    "lastName": "Smith",
    "number": "ID987654321",
    "issuedDate": "2019-06-10",
    "validUntil": "2029-06-10",
    "dob": "1992-03-15",
    "placeOfBirth": "Lagos"
  },
  "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
  "filename": "id_card_front.jpg",
  "return_doc_warnings": true
}
```

</RequestExample>

## Responses

| Status | Description |
| --- | --- |
| `200` | Document uploaded successfully |
| `400` | Invalid request |
| `404` | Customer not found |
| `409` | Concurrent in-flight request with the same Idempotency-Key |
| `413` | Payload too large. The decoded document exceeds the 10 MB service limit, or the request body exceeded the ingress 15 MB limit. The ingress-layer 413 is returned as HTML by nginx and does not match the schema below. |
| `422` | Idempotency-Key reused with a different request payload |

### 200 response

<ResponseField name="data" type="object" required>

  <Expandable title="properties">
    <ResponseField name="status" type="string" required>
      Always `"success"` on a successful upload

      Possible values: `success`
    </ResponseField>

    <ResponseField name="customer_id" type="string" required>
      ID of the customer the document was saved for
    </ResponseField>

    <ResponseField name="document_type" type="string" required>
      Normalised document type derived from `metadata.idDocType`
    </ResponseField>

    <ResponseField name="document_sub_type" type="string" required>
      Document sub-type as supplied in `metadata.idDocSubType`
    </ResponseField>

    <ResponseField name="country" type="string" required>
      ISO 3166-1 alpha-3 country code as supplied in `metadata.country`
    </ResponseField>

    <ResponseField name="message" type="string" required>
      Human-readable confirmation message
    </ResponseField>

  </Expandable>
</ResponseField>

<ResponseField name="error" type="any" required>
  Error information if the request failed
</ResponseField>

<ResponseExample>

```json title="SuccessPassport"
{
  "data": {
    "status": "success",
    "customer_id": "b84d9a52-2978-43e5-9b86-eac9839e1146",
    "document_type": "passport",
    "document_sub_type": "FRONT_SIDE",
    "country": "NGA",
    "message": "Document saved. Use /initiate-kyc to submit for verification."
  },
  "error": null
}
```

```json title="SuccessSelfie"
{
  "data": {
    "status": "success",
    "customer_id": "b84d9a52-2978-43e5-9b86-eac9839e1146",
    "document_type": "selfie",
    "document_sub_type": "FRONT_SIDE",
    "country": "NGA",
    "message": "Document saved. Use /initiate-kyc to submit for verification."
  },
  "error": null
}
```

</ResponseExample>

### 400 response

<ResponseField name="data" type="any" required>
  Always null for error responses
</ResponseField>

<ResponseField name="error" type="string" required>
  Human-readable error message
</ResponseField>

<ResponseField name="error" type="any">

  <Expandable title="properties">
    <ResponseField name="code" type="any">
    </ResponseField>

    <ResponseField name="message" type="any">
    </ResponseField>

  </Expandable>
</ResponseField>

<ResponseExample>

```json title="InvalidDocumentType"
{
  "data": null,
  "error": {
    "code": "INVALID_ARGUMENT",
    "message": "selfie is not a valid document type for business customers",
    "details": {}
  }
}
```

```json title="MissingRequiredField"
{
  "data": null,
  "error": {
    "code": "INVALID_ARGUMENT",
    "message": "filename is required",
    "details": {}
  }
}
```

```json title="InvalidBase64"
{
  "data": null,
  "error": {
    "code": "INVALID_ARGUMENT",
    "message": "Invalid base64 encoded content",
    "details": {}
  }
}
```

```json title="MissingIdempotencyKey"
{
  "data": null,
  "error": {
    "code": "INVALID_ARGUMENT",
    "message": "missing idempotency key in request headers",
    "details": {}
  }
}
```

```json title="UnsupportedMime"
{
  "data": null,
  "error": {
    "code": "INVALID_ARGUMENT",
    "message": "unsupported file content type: application/octet-stream. Supported types: JPEG, PNG, PDF",
    "details": {}
  }
}
```

</ResponseExample>

### 404 response

<ResponseField name="data" type="any" required>
  Always null for error responses
</ResponseField>

<ResponseField name="error" type="string" required>
  Human-readable error message
</ResponseField>

<ResponseField name="error" type="any">

  <Expandable title="properties">
    <ResponseField name="code" type="any">
    </ResponseField>

    <ResponseField name="message" type="any">
    </ResponseField>

  </Expandable>
</ResponseField>

<ResponseExample>

```json title="Response"
{
  "data": null,
  "error": {
    "code": "NOT_FOUND",
    "message": "Customer not found",
    "details": {}
  }
}
```

</ResponseExample>

### 409 response

<ResponseField name="data" type="any" required>
  Always null for error responses
</ResponseField>

<ResponseField name="error" type="string" required>
  Human-readable error message
</ResponseField>

<ResponseExample>

```json title="Response"
{
  "data": null,
  "error": {
    "code": "CONFLICT",
    "message": "A request with the same Idempotency-Key is currently being processed",
    "details": {}
  }
}
```

</ResponseExample>

### 413 response

<ResponseField name="data" type="any" required>
  Always null for error responses
</ResponseField>

<ResponseField name="error" type="string" required>
  Human-readable error message
</ResponseField>

<ResponseExample>

```json title="Response"
{
  "data": null,
  "error": {
    "code": "PAYLOAD_TOO_LARGE",
    "message": "file too large: content exceeds maximum allowed size of 10MB",
    "details": {}
  }
}
```

</ResponseExample>

### 422 response

<ResponseField name="data" type="any" required>
  Always null for error responses
</ResponseField>

<ResponseField name="error" type="string" required>
  Human-readable error message
</ResponseField>

<ResponseExample>

```json title="Response"
{
  "data": null,
  "error": {
    "code": "UNPROCESSABLE_ENTITY",
    "message": "Idempotency-Key was previously used with a different request payload",
    "details": {}
  }
}
```

</ResponseExample>

## Authorization

- **ApiKeyAuth**
