
# Face match

```
POST /api/kyc/face/compare
POST /api/kyc/face/upload
GET  /api/kyc/face/checks
GET  /api/kyc/face/checks/:id
```

Compare two photos of a face and learn, synchronously, whether they show the same person. This is the server-to-server counterpart of the dashboard's Face match tab: same engine, same verdicts, same history (API checks appear there labelled "API").

**Authentication:** `Authorization: Bearer sk_…` (secret keys only, for the whole surface). The inputs are biometric data and the verdict is a compliance record, so this never belongs in client-side code.
**Production note:** `sk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production).

## Compare

```
POST /api/kyc/face/compare
```

One call: send both photos inline and get the verdict back. Each photo is **exactly one** of an image field (URL, data URI, or base64) or a `mediaId` from the optional upload endpoint below.

| Field | Required | Description |
|---|---|---|
| `imageA` | one of | The first photo, inline: an `https://` URL we fetch, a `data:` URI, or base64-encoded bytes. JPEG, PNG or WebP; the format is detected from the bytes, never from a label. |
| `imageB` | one of | The second photo, same forms. |
| `mediaAId` | one of | Alternative to `imageA`: a `mediaId` from `POST /face/upload`. |
| `mediaBId` | one of | Alternative to `imageB`. |
| `consent` | yes | Must be `true`: you attest the people shown have consented to the comparison. |
| `sandboxOutcome` | no | Sandbox only, see below. Ignored on live keys. |

```bash
curl -X POST https://trust.myaza.app/api/kyc/face/compare \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "imageA": "https://cdn.example.com/enrolment/selfie-42.jpg",
    "imageB": "data:image/jpeg;base64,/9j/4AAQ…",
    "consent": true
  }'
```

**Size limits.** URLs and uploads accept photos up to 25 MB. Base64 travels inside the JSON body, which is capped at 10 MB, so base64 photos top out around 7 MB of image; send anything bigger by URL or upload.

**URL fetching.** Image URLs must be http(s) and resolve to a public address; anything that points into a private network is refused with `image_url_not_allowed`. Redirects are followed (up to 3), each hop checked the same way.

The response:

```json
{
  "faceCheck": {
    "id": "fc_…",
    "verdict": "match",
    "match": true,
    "confidence": 91,
    "threshold": 80,
    "reviewThreshold": 60,
    "chargedAmount": "0.0300",
    "createdAt": "2026-08-13T12:00:00.000Z"
  }
}
```

### The verdict

`verdict` is three-way, judged against your organisation's thresholds (the pass mark and review floor, managed on the dashboard's Face match tab):

| Verdict | Meaning |
|---|---|
| `match` | Similarity at or above your pass mark. |
| `review` | Similarity between your review floor and pass mark. The score alone cannot decide; a human should compare the photos. |
| `no_match` | Similarity below your review floor. |
| `no_result` | The comparison could not run (for example, no detectable face). Returned with HTTP 502 and **nothing is charged**. |

There is deliberately no per-request threshold: the lines that judge a verdict are your organisation's recorded policy, and every check records the lines that judged it. If part of your flow needs a stricter bar, branch on `confidence` in your own code; the recorded verdict stays consistent.


## Uploading first (optional)

```
POST /api/kyc/face/upload
```

When the bytes are already on your side and you would rather not inline them, or you want to reuse one photo across several checks, upload it once and pass the `mediaId`. Multipart form with a single `file` field (jpeg, png or webp, up to 25 MB); each call returns one `mediaId`.

```bash
curl -X POST https://trust.myaza.app/api/kyc/face/upload \
  -H "Authorization: Bearer sk_live_…" \
  -F "file=@photo-a.jpg"
# → { "mediaId": "med_…" }
```

## Re-read a check

```
GET /api/kyc/face/checks/:id
```

Returns the same `faceCheck` object for any past check belonging to your organisation and environment.

## History

```
GET /api/kyc/face/checks
```

Paginated face-check history for your organisation and the key's environment, newest first. Query parameters: `verdict` (`match` / `review` / `no_match` / `no_result`), `from` / `to` (ISO dates), `page`, `pageSize` (max 100). Rows carry the same fields as the compare response plus `initiatedVia` (`api` or `dashboard`).

## Deleting a check

```
DELETE /api/kyc/face/checks/:id
```

Removes a check from your history. It is a soft delete, with the same body and response shape as the other record deletions; see [Delete records & expire sessions](https://trust.myaza.co/documentation/api-manage-records/markdown#delete-a-face-check).

## Sandbox behaviour

Sandbox keys (`sk_test_`) always return **simulated** verdicts; the real comparison engine runs in production only. With no `sandboxOutcome` the verdict is a simulated `match`; pass `review`, `no_match` or `no_result` to see the others. Simulated scores are placed inside your organisation's own thresholds, so they behave exactly as a live score would. Sandbox checks are free, and sandbox photos are replaced with a placeholder after 3 days.

## Billing

Production checks are billed per comparison at your facial comparison price (see [your pricing](https://trust.myaza.co/documentation/environments/markdown)). A `no_match` still charges, because the check ran and answered. A `no_result` never charges.

## Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `same_photo` | Both sides are the same photo (same media id, or identical inline images). |
| 400 | `media_unreadable` | One of the photos could not be read; send both again. |
| 400 | `invalid_image` | An inline image is neither an https URL, a data URI, nor valid base64. |
| 400 | `unsupported_media_type` | The bytes are not a JPEG, PNG or WebP photo. |
| 400 | `image_too_large` | The photo exceeds 25 MB. |
| 400 | `image_url_not_allowed` | The image URL is not http(s), or resolves to a non-public address. |
| 400 | `image_fetch_failed` | The image URL could not be fetched (unreachable, error status, empty body, or too many redirects). |
| 402 | `insufficient_credits` | Your balance cannot cover the check. |
| 403 | `spot_checks_disabled` | Face checks are not enabled for your organisation. |
| 403 | `secret_key_required` | A publishable key was used; this surface is secret-key only. |
| 429 | `rate_limited` | Hourly face-check limit reached; retry later. |
| 502 | `no_result` | The comparison could not run; nothing charged. |
