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

# Issue codes

> Every code that can appear in issues[], what triggers it, and what details it carries.

Every finding in `issues[]` carries a `code`. Codes are a **stable contract**: once
published, a code is never renamed or repurposed. Branch your logic on `code`, never on
`message`.

## Types

| Type | Effect on the decision |
| - | - |
| `REJECT` | The transaction is `REJECTED` |
| `WARN` | None. Recorded for your review |
| `INFO` | None. Explains something about how the analysis ran |

Some codes are marked **policy-driven** below: their type is not fixed, it comes from the
`REJECT` / `WARN` / `IGNORE` action you configured for that policy. With `IGNORE`, nothing
is emitted at all.

<Info>
  Most codes carry `details: null`. `details` is populated only when it tells you something
  you cannot get from elsewhere in the result — a similarity score you cannot recompute,
  or a historical value that does not appear anywhere else. Thresholds and expected values
  are omitted because they are in the config you sent; extracted values are omitted because
  they are in `result_data`.
</Info>

## Request and session

These abort the analysis. `decision` is `UNDETERMINED`.

| Code | Type | Meaning | `details` |
| - | - | - | - |
| `REQUEST_SCHEMA_INVALID` | `REJECT` | The analysis request did not match the expected schema | `{ field }` |
| `TRANSACTION_STATUS_NOT_COMPLETED` | `REJECT` | The transaction was analysed before capture finished | `{ status }` |
| `TRANSACTION_TYPE_UNKNOWN` | `REJECT` | Unrecognised transaction type | `{ transaction_type }` |
| `ORGANIZATION_ACCOUNT_MISMATCH` | `REJECT` | The customer does not belong to the transaction's account | — |
| `SESSION_MISSING` | `REJECT` | The transaction has no capture session | — |
| `LIVENESS_BLOCK_MISSING` | `REJECT` | The type requires liveness but none was provided | — |
| `DOCUMENT_BLOCK_MISSING` | `REJECT` | The type requires a document but none was provided | — |
| `DOCUMENT_FRONT_IMAGE_MISSING` | `REJECT` | No usable front image of the document | — |
| `LIVENESS_IMAGE_MISSING` | `REJECT` | The liveness session returned no reference image | — |
| `TIMESTAMP_ORDER_INVALID` | `REJECT` | Transaction timestamps are inconsistent | — |

These generally mean something went wrong between the SDK and the API rather than anything
about the user. Retry with a new transaction.

## Configuration — blocking

Your `config` could not be applied. Analysis does not run; `decision` is `UNDETERMINED`.

| Code | Type | Meaning | `details` |
| - | - | - | - |
| `CONFIG_TRANSACTION_TYPE_MISMATCH` | `REJECT` | The configured type does not match the transaction | — |
| `CONFIG_REQUIRED_POLICY_MISSING` | `REJECT` | A policy this transaction type requires is absent | `{ policy }` |
| `CONFIG_VALUE_OUT_OF_RANGE` | `REJECT` | A score, TTL or age is outside its valid range | `{ field, value, valid_range }` |
| `CONFIG_AGE_RANGE_INVERTED` | `REJECT` | `expected_min_age` is greater than `expected_max_age` | — |
| `CONFIG_FILTER_MODE_INVALID` | `REJECT` | Unsupported document eligibility filter mode | — |
| `CONFIG_POLICY_ACTION_UNKNOWN` | `REJECT` | A policy action outside `REJECT` / `WARN` / `IGNORE` | `{ policy }` |

These are integration bugs, not user failures. Fix the config and create a new transaction.

## Configuration — advisory

The config is valid but probably not what you meant. Analysis proceeds normally.

| Code | Type | Meaning |
| - | - | - |
| `CONFIG_AGE_RANGE_TOO_NARROW` | `WARN` | The age range is narrower than the estimator's precision and will reject most people |
| `CONFIG_TARGET_DOCUMENTS_EMPTY` | `WARN` | `filter_mode: ALLOW` with an empty list — nothing can be accepted |
| `CONFIG_GPS_POLICY_WITHOUT_PERMISSION` | `WARN` | A GPS policy is active but location permission was not granted |

## Engine

`decision` is `UNDETERMINED`. No `details` — the diagnostics are internal.

| Code | Type | Meaning |
| - | - | - |
| `ENGINE_PROVIDER_ERROR` | `REJECT` | An external provider failed and the analysis could not complete |
| `ENGINE_TIMEOUT` | `REJECT` | The analysis exceeded its time budget |

Retriable. Create a new transaction rather than treating the user as rejected.

## Customer

| Code | Type | Meaning |
| - | - | - |
| `CUSTOMER_BLOCKED` | `REJECT` | The customer's status is `BLOCKED` |
| `CUSTOMER_SUSPICIOUS` | `WARN` | The customer is flagged as suspicious |

## Device

All policy-driven via [`device_policy`](/configuration/device-policy). No `details` —
the signals behind them are in `result_data.metadata`.

| Code | Type | Meaning |
| - | - | - |
| `COMPROMISED_DEVICE` | policy-driven | Rooted, jailbroken, or running under a debugger |
| `DEVELOPER_MODE_ENABLED` | policy-driven | Developer mode is on |
| `EMULATOR_DETECTED` | policy-driven | Captured on an emulator or simulator |
| `VPN_OR_PROXY_DETECTED` | policy-driven | Connection routed through VPN, proxy or Tor |
| `MOCK_LOCATION_DETECTED` | policy-driven | The reported GPS location is simulated |
| `GEO_IP_MISMATCH` | policy-driven | GPS country differs from IP country |

### Location unavailable

| Code | Type | Meaning |
| - | - | - |
| `GEOLOCATION_NOT_REQUESTED` | `INFO` | The SDK never asked for location permission |
| `GEOLOCATION_DENIED` | `INFO` | The user denied location permission |

### Signal missing with policy active

Emitted only when the corresponding policy is not `IGNORE`. Never change the decision —
they tell you a rule you configured could not run.

| Code | Type |
| - | - |
| `COMPROMISED_DEVICE_NOT_EVALUABLE` | `WARN` |
| `DEVELOPER_MODE_NOT_EVALUABLE` | `WARN` |
| `EMULATOR_NOT_EVALUABLE` | `WARN` |
| `VPN_OR_PROXY_NOT_EVALUABLE` | `WARN` |
| `MOCK_LOCATION_NOT_EVALUABLE` | `WARN` |
| `GEO_IP_MISMATCH_NOT_EVALUABLE` | `WARN` |

## Document — capture and legibility

| Code | Type | Meaning | `details` |
| - | - | - | - |
| `DOCUMENT_NOT_READABLE` | `REJECT` | No data could be extracted | — |
| `DOCUMENT_IMAGE_UNUSABLE` | `REJECT` | Blurred, glared, cropped or badly framed | `{ side, reason }` |
| `DOCUMENT_BACK_IMAGE_MISSING` | `REJECT` | This document type requires its back side | — |
| `MRZ_NOT_FOUND` | `REJECT` | No MRZ on a document that should have one | — |
| `BARCODE_UNREADABLE` | `REJECT` | The barcode is present but could not be decoded | — |
| `NO_FACE_DETECTED_IN_DOCUMENT` | `REJECT` | No face on the front of the document | — |
| `MULTIPLE_FACES_IN_DOCUMENT` | `REJECT` | More than two faces on the front | `{ face_count }` |

Most of these are recoverable by the user — better lighting, a flatter surface, no glare.
Worth surfacing as "try again" rather than a hard failure.

## Document — authenticity

No `details` beyond one case: naming the check that caught the forgery would tell a
fraudster what to fix.

| Code | Type | Meaning | `details` |
| - | - | - | - |
| `DOCUMENT_DATA_MANIPULATED` | `REJECT` | Signs of digital manipulation | — |
| `DOCUMENT_PHYSICALLY_TAMPERED` | `REJECT` | Signs of physical tampering | — |
| `DOCUMENT_IS_COPY` | `REJECT` | A photocopy or a photograph of a screen | — |
| `DOCUMENT_INTEGRITY_FAILED` | `REJECT` | Failed its integrity checks | — |
| `MRZ_CHECKSUM_INVALID` | `REJECT` | One or more MRZ check digits are invalid | — |
| `DOCUMENT_DATA_MISMATCH` | `REJECT` | Printed data, MRZ and barcode disagree | `{ field }` |

## Document — eligibility

| Code | Type | Meaning | `details` |
| - | - | - | - |
| `DOCUMENT_TYPE_NOT_SUPPORTED` | `REJECT` | chmod does not support this type and country | — |
| `DOCUMENT_NOT_ELIGIBLE` | `REJECT` | Supported, but not permitted by your config | — |
| `EXPIRED_DOCUMENT` | `REJECT` | Expired, and `allow_expired` is `false` | `{ evaluated_at }` |
| `DOCUMENT_NOT_YET_VALID` | `REJECT` | The issue date is in the future | `{ evaluated_at }` |
| `DOCUMENT_TYPE_MISMATCH` | `WARN` | Type detected at capture differs from the extracted one | `{ detected }` |
| `DOCUMENT_COUNTRY_MISMATCH` | `WARN` | Country detected at capture differs from the extracted one | `{ detected }` |

`DOCUMENT_TYPE_NOT_SUPPORTED` and `DOCUMENT_NOT_ELIGIBLE` are worth distinguishing in your
UI: the first means "we cannot read this", the second means "you asked us not to accept it".

## Document — customer history

The customer's history is not in the result, so all three carry it in `details`.

| Code | Type | Meaning | `details` |
| - | - | - | - |
| `DATE_OF_BIRTH_HISTORY_MISMATCH` | `REJECT` | Date of birth differs from the one on record | `{ on_record, reference_transaction_id }` |
| `CONFLICTING_DOCUMENT_NUMBER_HISTORY` | `REJECT` | Same type and country, different number | `{ on_record, reference_transaction_id }` |
| `NAME_HISTORY_MISMATCH` | `WARN` | Name differs from the one on record | `{ field, on_record, score, threshold, reference_transaction_id }` |

## Data matching

All `REJECT`. Only the fuzzy comparisons carry `details` — you cannot recompute a
similarity score, but you already know the exact values you sent.

| Code | Meaning | `details` |
| - | - | - |
| `NAME_MISMATCH` | Given names below the similarity threshold | `{ score, threshold }` |
| `SURNAME_MISMATCH` | Surnames below the similarity threshold | `{ score, threshold }` |
| `GENDER_MISMATCH` | Gender differs from the expected value | — |
| `EXPECTED_DOCUMENT_TYPE_MISMATCH` | Document type differs from the expected value | — |
| `DOCUMENT_ISSUING_COUNTRY_MISMATCH` | Issuing country differs from the expected value | — |
| `DOCUMENT_NUMBER_MISMATCH` | Document number differs from the expected value | — |
| `DATE_OF_BIRTH_MISMATCH` | Date of birth differs from the expected value | — |

## Liveness

All `REJECT`. Scores and facial attributes are already in `result_data.liveness.data`.

| Code | Meaning | `details` |
| - | - | - |
| `LIVENESS_FAILED` | The liveness check did not complete | — |
| `LIVENESS_LOW_CONFIDENCE` | Below `liveness_min_score` | — |
| `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 | `{ face_count }` |
| `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 | `{ reason }` |
| `LIVENESS_GENDER_MISMATCH_EXPECTED` | Detected gender differs from `expected_gender` | — |
| `LIVENESS_AGE_OUT_OF_RANGE` | Estimated age falls outside the accepted range | — |

Sunglasses, closed eyes, occlusion and poor quality are all things the user can fix.
Offering a retry converts most of them.

## Face comparison

| Code | Type | Meaning | `details` |
| - | - | - | - |
| `DOCUMENT_VS_LIVENESS_FACE_MISMATCH` | `REJECT` | Document face does not match the liveness capture | — |
| `LIVENESS_VS_IDENTITY_REF_FACE_MISMATCH` | `REJECT` | Liveness capture does not match the enrolled face | — |
| `DOCUMENT_VS_IDENTITY_REF_FACE_MISMATCH` | `REJECT` | Document face does not match the enrolled face | — |
| `IDENTITY_REFERENCE_IMAGE_UNUSABLE` | `REJECT` | The enrolled face image cannot be used | — |
| `DOCUMENT_FACE_IMAGE_UNUSABLE` | `REJECT` | The face on the document cannot be used | — |
| `FACE_COMPARISON_SKIPPED_NO_REFERENCE` | `INFO` | First transaction — no enrolled face to compare against | `{ skipped_types }` |
| `FACE_COMPARISON_DISABLED` | `INFO` | `require_face_comparison` is `false` | — |

`FACE_COMPARISON_SKIPPED_NO_REFERENCE` is normal and expected on a customer's first
transaction. It is `INFO`, not a rejection.

## Not yet enforced

These codes are defined and their policies are accepted by the API, but the analysis
engine does not evaluate them yet, so they are **not currently emitted**. Configure them
if you want the intent recorded; do not rely on them as active protection.

| Code | Type | Meaning | `details` |
| - | - | - | - |
| `REPEATED_FACE_ANOTHER_CUSTOMER` | policy-driven | The face is already registered to a different customer | `{ other_customer_id, similarity }` |
| `INTERNAL_FACE_BLACKLIST_MATCH` | policy-driven | The face matches your organization's blocklist | — |
| `GLOBAL_FACE_BLACKLIST_MATCH` | policy-driven | The face matches the platform blocklist | — |

Blocklist matches deliberately carry no `details`: exposing why someone was listed is
exactly the information that makes a blocklist easy to evade.

## Handling codes in your app

Group codes by what the user can do about them, not by which check produced them:

```typescript lines theme={null}
const RETRYABLE = new Set([
  // The capture was bad, not the person. Let them try again.
  "DOCUMENT_IMAGE_UNUSABLE",
  "DOCUMENT_NOT_READABLE",
  "DOCUMENT_BACK_IMAGE_MISSING",
  "LIVENESS_IMAGE_UNUSABLE",
  "LIVENESS_NO_FACE_DETECTED",
  "LIVENESS_SUNGLASSES_DETECTED",
  "LIVENESS_EYES_CLOSED",
  "LIVENESS_FACE_OCCLUDED",
  "LIVENESS_POOR_IMAGE_QUALITY",
]);

const FRAUD = new Set([
  // Do not offer a retry. Route to manual review.
  "DOCUMENT_DATA_MANIPULATED",
  "DOCUMENT_PHYSICALLY_TAMPERED",
  "DOCUMENT_IS_COPY",
  "MRZ_CHECKSUM_INVALID",
  "DOCUMENT_DATA_MISMATCH",
  "DOCUMENT_VS_LIVENESS_FACE_MISMATCH",
  "DATE_OF_BIRTH_HISTORY_MISMATCH",
  "CONFLICTING_DOCUMENT_NUMBER_HISTORY",
]);

function triage(issues: Issue[]) {
  const blocking = issues.filter((i) => i.type === "REJECT").map((i) => i.code);

  if (blocking.some((c) => FRAUD.has(c)))          return "manual_review";
  if (blocking.every((c) => RETRYABLE.has(c)))     return "offer_retry";
  return "reject";
}
```

<Warning>
  Offering an unlimited retry on a fraud signal turns your flow into a testing ground:
  an attacker learns which forgery passes by trying until one does. Cap retries and route
  the codes above to a human.
</Warning>


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