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

# Biometric policy

> Liveness and face-comparison thresholds, expected age range and gender.

`biometric_policy` governs the liveness check and the face comparisons that follow it.

Applies to `DOCUMENT_AND_BIOMETRIC` and `BIOMETRIC_ONLY`. Ignored for `DOCUMENT_ONLY`.

```json lines theme={null}
{
  "biometric_policy": {
    "liveness_min_score": 0.85,
    "face_comparison_min_score": 0.85,
    "require_face_comparison": true,
    "expected_gender": null,
    "expected_min_age": 18,
    "expected_max_age": 120,
    "on_another_customer_repeated_face": "REJECT"
  }
}
```

## Thresholds

<ParamField body="liveness_min_score" type="number" default="0.85">
  Minimum confidence that the person in front of the camera is physically present rather
  than a photo, a video or a mask. Range `0.1`–`1.0`.

  Below the threshold, `LIVENESS_LOW_CONFIDENCE` is emitted and the transaction is
  rejected.
</ParamField>

<ParamField body="face_comparison_min_score" type="number" default="0.85">
  Minimum confidence for two faces to be considered the same person. Range `0.1`–`1.0`.
  Applies to every comparison performed.
</ParamField>

<ParamField body="require_face_comparison" type="boolean" default="true">
  Whether face comparison runs at all. Setting it to `false` skips every comparison and
  emits an informational `FACE_COMPARISON_DISABLED`.
</ParamField>

<Warning>
  Raising a threshold makes the check stricter for genuine users too. At `0.95` you will
  reject people whose document photo is ten years old, who have grown a beard, or who are
  wearing glasses in one image and not the other. `0.85` is the calibrated default;
  move it in small steps and watch your rejection rate.
</Warning>

## Which faces get compared

Up to three comparisons run, depending on the transaction type and whether the customer
already has an enrolled face:

| Comparison | What it proves | `DOCUMENT_AND_BIOMETRIC` | `DOCUMENT_ONLY` | `BIOMETRIC_ONLY` |
| - | - | :-: | :-: | :-: |
| `DOCUMENT_VS_LIVENESS` | The person holding the document is the person on it | Yes | — | — |
| `LIVENESS_VS_IDENTITY_REF` | The person is the same one you enrolled before | Yes <sup>1</sup> | — | Yes <sup>1</sup> |
| `DOCUMENT_VS_IDENTITY_REF` | The document belongs to the person you enrolled | Yes <sup>1</sup> | Yes <sup>1</sup> | — |

<sup>1</sup> Only when the customer has an enrolled face — that is, when this is not their
first transaction.

On a first transaction there is nothing to compare against. chmod does **not** reject:
it skips the comparison and emits `FACE_COMPARISON_SKIPPED_NO_REFERENCE` as `INFO`,
listing which comparisons were skipped. The face captured in that first successful
transaction becomes the enrolled reference for every transaction after it.

<Info>
  This is the strongest argument for reusing `customer_id`. A new customer per
  verification means every transaction is a first transaction, and
  `LIVENESS_VS_IDENTITY_REF` — the check that catches one person onboarding under several
  identities — never runs.
</Info>

Each comparison and its score comes back in the result:

```json lines theme={null}
{
  "face_comparison": {
    "status": "PASSED",
    "data": {
      "comparison": [
        { "type": "DOCUMENT_VS_LIVENESS",      "confidence_score": 0.97, "matched": true },
        { "type": "LIVENESS_VS_IDENTITY_REF",  "confidence_score": 0.94, "matched": true }
      ]
    }
  }
}
```

A failed comparison emits the matching issue — `DOCUMENT_VS_LIVENESS_FACE_MISMATCH`,
`LIVENESS_VS_IDENTITY_REF_FACE_MISMATCH` or `DOCUMENT_VS_IDENTITY_REF_FACE_MISMATCH`.
None of them carry `details`, because the score is already in `comparison[]` and the
threshold is in the config you sent.

## Expected attributes

chmod estimates age and gender from the liveness capture. These fields turn the estimate
into a rule.

<ParamField body="expected_gender" type="string | null" default="null">
  `MALE`, `FEMALE`, or `null` to accept any. Rejects with
  `LIVENESS_GENDER_MISMATCH_EXPECTED`.
</ParamField>

<ParamField body="expected_min_age" type="integer | null" default="null">
  Minimum accepted age, `0`–`200`. Rejects with `LIVENESS_AGE_OUT_OF_RANGE`.
</ParamField>

<ParamField body="expected_max_age" type="integer | null" default="null">
  Maximum accepted age, `0`–`200`.
</ParamField>

<Warning>
  Age is **estimated from a photograph**, and the estimator returns a range rather than a
  number. It is not a substitute for the date of birth printed on the document — for a
  real age check, use `data_matching.date_of_birth` in
  [Document policy](/configuration/document-policy).

  A range narrower than ten years rejects most genuine subjects. chmod flags that with
  `CONFIG_AGE_RANGE_TOO_NARROW`. Setting `expected_min_age` greater than
  `expected_max_age` is rejected outright as `CONFIG_AGE_RANGE_INVERTED`.
</Warning>

Gender estimation carries the same caveat: it reads presentation, not identity. Treat it
as a fraud signal, not a fact about the person.

## Duplicate faces

<ParamField body="on_another_customer_repeated_face" type="string" default="REJECT">
  What to do when the captured face already belongs to a **different** customer in your
  account. Emits `REPEATED_FACE_ANOTHER_CUSTOMER`, carrying the other customer's id and
  the similarity score.

  Takes `REJECT`, `WARN` or `IGNORE`.
</ParamField>

<Note>
  This check is accepted and validated by the API but is **not enforced yet** — the
  analysis engine does not currently evaluate it, so no
  `REPEATED_FACE_ANOTHER_CUSTOMER` issue is emitted regardless of what you configure.
  Set it now if you want the intent recorded; do not rely on it as active protection.
</Note>

## Image quality

Before any threshold is applied, the liveness capture has to be usable. These checks need
no configuration and always run:

| Issue | Fires when |
| - | - |
| `LIVENESS_FAILED` | The liveness check did not complete |
| `LIVENESS_IMAGE_UNUSABLE` | The reference image cannot be analysed |
| `LIVENESS_NO_FACE_DETECTED` | No face in the image |
| `LIVENESS_MULTIPLE_FACES_DETECTED` | More than one face in the image |
| `LIVENESS_SUNGLASSES_DETECTED` | The subject is wearing sunglasses |
| `LIVENESS_FACE_OCCLUDED` | The face is partially covered |
| `LIVENESS_EYES_CLOSED` | The subject's eyes are closed |
| `LIVENESS_POOR_IMAGE_QUALITY` | Too dark or out of focus |

All are `REJECT`. The facial attributes behind them — including confidence for each —
come back in `result_data.liveness.data.face_attributes`, so you can tell a genuine
failure from a user who simply needs to take their sunglasses off and retry.


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