# Face Liveness & Match

> Passive liveness detection and face comparison against the BVN photo in one call, via NIBSS N-Face.

Runs passive liveness detection on a selfie and, when you pass a BVN, compares the face with the photo on the BVN record, through NIBSS N-Face. One call, two scores.

`POST /v1/verify/face`

- **Availability:** Sandbox and live
- **Indicative price:** ₦150.00 per completed check
- **Consent:** The live selfie is the consent (`in_person_selfie`).
- **MCP tool:** `face_match` (image by https URL)

## Request

**Body parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `image` | string | Yes | Selfie as base64 JPEG/PNG, or an https URL when image_type is url. At least 20 characters. |
| `image_type` | string | No | One of: `base64`, `url`. Defaults to `base64`. |
| `bvn` | string | No | Compare the selfie against the photo on this BVN. Omit for liveness only. Format: 11 digits. |
| `liveness_threshold` | number | No | Between 0 and 1. Defaults to `0.6`. |
| `face_match_threshold` | number | No | Between 0 and 1. Defaults to `0.9`. |
| `order` | string | No | Liveness first, comparison first, or both in parallel. One of: `pl_fc_serial`, `fc_pl_serial`, `fc_pl_parallel`. Defaults to `pl_fc_serial`. |
| `os_type` | string | No | One of: `ios`, `android`, `desktop`, `unknown`. Defaults to `unknown`. |
| `reason` | string | Yes | Purpose of the check. One of the 38 NIMC purpose codes. See the [purpose code list](https://speedmap-delta.vercel.app/docs/compliance#purpose-codes). |

- Omit `bvn` for liveness only (method `face_liveness`); include it for liveness and match (`face_liveness_match`).
- With `image_type: "url"`, `image` must be a public https URL.
- `match` is `true` when liveness passes and, if a BVN was supplied, the face comparison passes too.
- To capture the selfie with an active liveness check inside your iOS, Android, React Native or Flutter app, see [Mobile apps](https://speedmap-delta.vercel.app/docs/mobile-sdks).

Send an `Idempotency-Key` header so a retried request is never billed twice. See [Idempotency](https://speedmap-delta.vercel.app/docs/idempotency).

_cURL_

```bash
curl -X POST http://localhost:4000/v1/verify/face \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "image": "<base64 selfie>",
  "bvn": "22222222221",
  "reason": "financialProducts"
}'
```

_Node.js_

```javascript
const res = await fetch("http://localhost:4000/v1/verify/face", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SPEEDMAP_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    "image": "<base64 selfie>",
    "bvn": "22222222221",
    "reason": "financialProducts"
  }),
});

const verification = await res.json();
if (!res.ok) throw new Error(verification.error.message);
console.log(verification.status, verification.match);
```

_Python_

```python
import os, uuid, requests

res = requests.post(
    "http://localhost:4000/v1/verify/face",
    headers={
        "Authorization": f"Bearer {os.environ['SPEEDMAP_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "image": "<base64 selfie>",
        "bvn": "22222222221",
        "reason": "financialProducts"
    },
    timeout=30,
)
res.raise_for_status()
verification = res.json()
print(verification["status"], verification["match"])
```

## Response

A completed check returns `200` with a [verification object](https://speedmap-delta.vercel.app/docs/verifications). `status` is `verified` or `not_found`. If the provider has not answered in time you get `202` with `status: "pending"`; [retrieve it later](https://speedmap-delta.vercel.app/docs/verifications#pending-verifications) or wait for the `verification.completed` [webhook](https://speedmap-delta.vercel.app/docs/webhooks).

**200 OK · assertion**

```json
{
  "reference_id": "spm_8171f3a87cbe221341f7185f",
  "object": "verification",
  "product": "face",
  "method": "face_liveness_match",
  "status": "verified",
  "match": true,
  "field_matches": null,
  "field_confidence": null,
  "mode": "sandbox",
  "reason": "financialProducts",
  "subject": "*******2221",
  "summary": {
    "liveness_passed": true,
    "liveness_confidence": 0.885,
    "liveness_quality": 0.885,
    "face_match_passed": true,
    "face_match_confidence": 0.988,
    "face_match_quality": 0.902,
    "session_code": "11b004ce-9f2f-43df-858e-79eacf50af07",
    "thresholds": {
      "liveness": 0.6,
      "face_match": 0.9
    }
  },
  "consent": {
    "mechanism": "in_person_selfie",
    "consent_id": null,
    "timestamp": "2026-10-19T09:41:07.512Z"
  },
  "source": "api",
  "error": null,
  "billing": {
    "amount_kobo": 0,
    "currency": "NGN"
  },
  "latency_ms": 1180,
  "created_at": "2026-10-19T09:41:07.512Z",
  "updated_at": "2026-10-19T09:41:07.874Z"
}
```

**summary**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `liveness_passed` | boolean | No | Liveness confidence reached `liveness_threshold`. |
| `liveness_confidence` | number | No | Liveness confidence, 0 to 1. |
| `liveness_quality` | number | No | Image quality score for liveness, 0 to 1. |
| `face_match_passed` | boolean \| null | No | Face comparison reached `face_match_threshold`. `null` without a BVN. |
| `face_match_confidence` | number \| null | No | Similarity to the BVN photo, 0 to 1. |
| `face_match_quality` | number \| null | No | Image quality score for the comparison. |
| `session_code` | string | No | N-Face session identifier, also returned as `provider_reference`. |
| `thresholds` | object | No | The `liveness` and `face_match` thresholds that were applied. |

Full-tier accounts calling the REST API also receive `provider`, `provider_reference` and `data`, the normalized record. MCP, webhooks, lists and bulk results never include it. See [Response shaping](https://speedmap-delta.vercel.app/docs/response-shaping).

**Full tier only**

```json
{
  "provider": "sandbox",
  "provider_reference": "SBX6CB753DB2C",
  "data": {
    "liveness_passed": true,
    "liveness_confidence": 0.885,
    "liveness_quality": 0.885,
    "face_match_passed": true,
    "face_match_confidence": 0.988,
    "face_match_quality": 0.902,
    "session_code": "11b004ce-9f2f-43df-858e-79eacf50af07",
    "thresholds": {
      "liveness": 0.6,
      "face_match": 0.9
    }
  }
}
```

## Test values

Send these bodies with a sandbox key. More outcome triggers are on the [Sandbox](https://speedmap-delta.vercel.app/docs/sandbox) page.

**Live and matching**

```json
{
  "image": "data-LIVE-selfie-0001",
  "bvn": "22222222221",
  "reason": "financialProducts"
}
```

**Spoof (fails liveness)**

```json
{
  "image": "data-SPOOF-photo-of-photo",
  "reason": "financialProducts"
}
```

**Different person**

```json
{
  "image": "data-MISMATCH-selfie",
  "bvn": "22222222221",
  "reason": "financialProducts"
}
```

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | The body failed validation. `details` lists each field. |
| 401 | `unauthorized` | Missing or invalid API key. |
| 402 | `insufficient_balance` | Live only: wallet balance is below the check price. |
| 403 | `not_entitled` | Your workspace is not enabled for this product or method. |
| 409 | `idempotency_key_reused` | The `Idempotency-Key` was used with a different body. |
| 422 | `provider_rejected_request` | The upstream registry rejected the request. |
| 503 | `verification_unavailable` | Temporary failure. Retry, with the same or a new `Idempotency-Key`. |

All errors share one shape. See [Errors](https://speedmap-delta.vercel.app/docs/errors) for the full list.

---

Source: https://speedmap-delta.vercel.app/docs/products/face
