---
title: "Upload document"
description: "Upload a KYC document for individual and business customers. This is the first step of the server-to-server verification flow."
protocol: rest
method: POST
endpoint: "/v1/customers/kyc/upload-document"
baseUrl: "https://api.gravv.xyz"
group: "KYC"
auth:
  label: "Api-Key"
  name: "Api-Key"
  in: header
---

# Upload document

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

Upload a KYC document for individual and business customers. 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

Individual customers must upload a `SELFIE` and one identification document: `PASSPORT`, `ID_CARD`, or `DRIVERS`.

Business customers upload a single REGISTRATION_CERTIFICATE, which may be a formation document or an ownership document. The `idDocType` value is always `REGISTRATION_CERTIFICATE`.

All `idDocType` values are uppercase.

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

## Document subtypes

Identification documents use `idDocSubType` to indicate which side is uploaded (case-sensitive, uppercase):

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

Omit `idDocSubType` for SELFIE uploads.

## 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.

## Idempotency

The `Idempotency-Key` header is required on this endpoint. If you send the same key again with an identical request body, the API returns the original response instead of uploading the document a second time. If you send the same key with a different request body, then the API rejects the request with a 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="metadata" body="metadata" type="object" required>

  <Expandable title="properties">
    <ParamField name="idDocType" body="metadata.idDocType" type="string" required>
      Type of document being uploaded. Individual customers upload a `SELFIE` plus one identification document: `PASSPORT`, `ID_CARD`, or `DRIVERS`. Business customers upload `REGISTRATION_CERTIFICATE` only.

      Possible values: `PASSPORT`, `ID_CARD`, `DRIVERS`, `SELFIE`, `REGISTRATION_CERTIFICATE`
    </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>

### 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"
}
```

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

```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"
}
```

```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"
}
```

</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 (gateway-level) |
| `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. (gateway-level) |
| `422` | Idempotency-Key reused with a different request payload (gateway-level) |
| `500` | Internal processing error during document upload |

### 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="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",
    "country": "NGA",
    "message": "Document saved. Use /start-s2s to submit for verification."
  },
  "error": null
}
```

```json title="SuccessSelfie"
{
  "data": {
    "status": "success",
    "customer_id": "b84d9a52-2978-43e5-9b86-eac9839e1146",
    "document_type": "SELFIE",
    "country": "NGA",
    "message": "Document saved. Use /start-s2s 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="MissingCustomerId"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "customer_id is required"
  }
}
```

```json title="MissingContent"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "content is required"
  }
}
```

```json title="MissingFilename"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "filename is required"
  }
}
```

```json title="MissingIdDocType"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "id_doc_type is required"
  }
}
```

```json title="MissingCountry"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "country is required"
  }
}
```

```json title="MissingNumber"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "number is required"
  }
}
```

```json title="MissingValidUntil"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "valid_until is required"
  }
}
```

```json title="MissingIdDocSubType"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "id_doc_sub_type is required"
  }
}
```

```json title="InvalidCustomerIdFormat"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "The customer ID provided is not valid"
  }
}
```

```json title="InvalidDocumentType"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "invalid document type for customer type"
  }
}
```

```json title="InvalidDocumentSubType"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "invalid document sub type"
  }
}
```

```json title="DocumentSizeExceeded"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "document exceeds maximum allowed size of 10 MB"
  }
}
```

```json title="InvalidBase64"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "document content is not valid base64"
  }
}
```

```json title="UnsupportedFileType"
{
  "data": null,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "unsupported file type \"application/octet-stream\"; allowed types are JPEG, PNG, and PDF"
  }
}
```

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

</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": "CUSTOMER_NOT_FOUND",
    "message": "Customer not found. Please verify the customer exists in our system."
  }
}
```

</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"
  }
}
```

</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"
  }
}
```

</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"
  }
}
```

</ResponseExample>

### 500 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": "KYC_PROCESSING_ERROR",
    "message": "Document processing failed. Please verify your documents and try again."
  }
}
```

</ResponseExample>

## Authorization

- **ApiKeyAuth**
