> ## Documentation Index
> Fetch the complete documentation index at: https://docs.proofage.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Uploading media

> Upload the selfie and document images of a verification and submit it.

When you [capture in your own UI](/integration/web/custom-ui), you upload each image to `POST /v1/verifications/{id}/media` and then submit. [Consent](/integration/web/consent) must be recorded first.

## Request fields

Uploads are `multipart/form-data`.

| Field | Required | Values |
| - | - | - |
| `file` | Always | The image. |
| `type` | Always | `selfie` or `document`. |
| `side` | When `type=document` | `front` or `back`. |
| `document` | When `type=document` | `id`, `driver_license`, `passport` or `residence_permit`. |

### Documents and their sides

| Document | `document` | Sides |
| - | - | - |
| National ID card | `id` | `front` and `back` |
| Driving licence | `driver_license` | `front` and `back` |
| Passport | `passport` | `front` only |
| Residence permit | `residence_permit` | `front` and `back` |

## File requirements

| Constraint | Value |
| - | - |
| Formats | JPEG, PNG, GIF, BMP or WebP. HEIC, which iPhones save by default, is refused: convert it to JPEG first. |
| Maximum size | 10 MB |
| Documents | At least 200 × 200 pixels |

## Signing

A `multipart/form-data` request, such as a media upload, is not signed over its raw body. Its canonical string has three lines:

```
METHOD + path [+ "?" + query]
form fields, sorted by name and encoded as RFC 3986 (spaces as %20)
SHA-256 of each file in hex, sorted and joined with commas
```

For a selfie upload with the single field `type=selfie`:

```
POST/v1/verifications/550e8400-e29b-41d4-a716-446655440000/media
type=selfie
9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
```

Nested fields are sorted at every level. The signature is the hex HMAC-SHA256 of that string with the secret key, as for any other request.

## When an image is refused

Each image is checked as it arrives. An unusable one is refused with `422` and a `code` that says what to fix, such as `FACE_TOO_BLURRY`; nothing is stored, and the person retakes it. Every code, with what to ask the person, is on [Errors](/integration/error-handling#consent-upload-and-submit).

A new selfie on a facial age estimation that has already asked for a document is refused with `422 MAX_ATTEMPTS_REACHED` when no attempts are left. Otherwise running out of attempts is decided after submit: the verification is `declined`, and its `reason` is the last attempt's reason, not a separate code.

## Ready to submit

Before `POST /v1/verifications/{id}/submit`, the required images must be uploaded:

| Workspace type | Images needed before submit |
| - | - |
| Identity (KYC) | A selfie, the document front, and the document back unless the document is a passport. |
| Age verification by ID document | The same as Identity (KYC). |
| Facial age estimation | A selfie. If the estimate cannot confirm the age, the verification moves to `documents_required` and also needs the document front, and the back unless it is a passport. |

If one is missing, submit answers `422 MISSING_REQUIRED_MEDIA`.

## Uploading an image again

* A new `selfie` replaces the previous selfie of the attempt.
* A new document image replaces the previous one of the same side, but only while the attempt is still missing an image. Once every required image is in, further document uploads are refused with a `422` validation error: submit instead.
* After `resubmission_requested`, the first upload moves the verification back to `started`, even if that image is then refused.

## Examples

### A selfie

```bash theme={null}
curl -X POST https://api.proofage.net/v1/verifications/550e8400-e29b-41d4-a716-446655440000/media \
  -H "Accept: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-HMAC-Signature: YOUR_HMAC_SIGNATURE" \
  -F "type=selfie" \
  -F "file=@selfie.jpg"
```

`200 OK` with an empty body.

### The front and back of an ID card

```bash theme={null}
curl -X POST https://api.proofage.net/v1/verifications/550e8400-e29b-41d4-a716-446655440000/media \
  -H "Accept: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-HMAC-Signature: YOUR_HMAC_SIGNATURE" \
  -F "type=document" \
  -F "side=front" \
  -F "document=id" \
  -F "file=@id_front.jpg"

curl -X POST https://api.proofage.net/v1/verifications/550e8400-e29b-41d4-a716-446655440000/media \
  -H "Accept: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-HMAC-Signature: YOUR_HMAC_SIGNATURE" \
  -F "type=document" \
  -F "side=back" \
  -F "document=id" \
  -F "file=@id_back.jpg"
```

### A refused image

```json 422 theme={null}
{
  "code": "FACE_TOO_BLURRY",
  "message": "Face validation failed. Please upload a clear, well-lit selfie with your face fully visible."
}
```

### Consent missing

```json 403 theme={null}
{
  "error": {
    "code": "CONSENT_REQUIRED",
    "message": "Consent must be accepted before proceeding with verification."
  }
}
```

## Next steps

* [Consent](/integration/web/consent): recorded before the first upload.
* [Capture in your own UI](/integration/web/custom-ui): the whole sequence of requests.
* [API authentication](/getting-started/api-authentication): signing, including file uploads.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.