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

# How verification works

> The lifecycle of a transaction, who does what, and when the decision arrives.

A transaction is the unit of work in chmod. It is created by your backend, carried out
by the SDK on the user's device, analysed by chmod, and read back by your backend.

## Customers and transactions

A **customer** is a person. You create one the first time you verify someone and reuse
the same `customer_id` for every later transaction.

This matters more than it looks. On the first transaction there is no enrolled face to
compare against, so chmod records one. On every transaction after that, the new selfie
is compared against that enrolled face, and the extracted document data is checked
against what the customer's earlier documents said. A customer whose date of birth
suddenly changes is a signal you only get if you reuse the id.

<Warning>
  Creating a fresh customer for every verification throws that history away. Store
  `customer_id` against your own user record and reuse it.
</Warning>

A **transaction** is one verification attempt. It carries its own configuration, so two
transactions for the same customer can enforce entirely different rules.

## Transaction lifecycle

```mermaid theme={null}
stateDiagram-v2
    [*] --> CREATED: POST /kyc/transaction
    CREATED --> INITIATED: SDK opens the session
    INITIATED --> COMPLETED: user finishes capture
    COMPLETED --> PROCESSING: analysis starts
    PROCESSING --> FINISHED: analysis succeeded
    PROCESSING --> FAILED: analysis could not run
    CREATED --> EXPIRED: TTL elapsed
    INITIATED --> EXPIRED: TTL elapsed
    FINISHED --> [*]
    FAILED --> [*]
    EXPIRED --> [*]
```

| Status | Meaning |
| - | - |
| `CREATED` | Your backend opened it. The `sdk_token` is valid and nothing has happened yet. |
| `INITIATED` | The SDK started a session on the device. |
| `COMPLETED` | The user finished capture. Everything needed for analysis has been uploaded. |
| `PROCESSING` | Analysis is running. |
| `FINISHED` | Analysis finished. `result_data.decision` holds the verdict. |
| `FAILED` | Analysis could not run. `decision` is `UNDETERMINED`. |
| `EXPIRED` | The TTL elapsed before the user finished. |

A transaction that reaches `FINISHED` or `FAILED` is terminal. To try again, create a
new transaction for the same customer.

## The decision

Once analysis completes, `result_data.decision` is one of three values:

<CardGroup cols={3}>
  <Card title="APPROVED" icon="circle-check">
    Every applicable check ran and none of them rejected.
  </Card>

  <Card title="REJECTED" icon="circle-xmark">
    At least one check rejected. Every reason is in `issues[]`.
  </Card>

  <Card title="UNDETERMINED" icon="circle-question">
    The analysis could not run — a malformed request, an invalid configuration or a
    provider failure. Not a risk verdict.
  </Card>
</CardGroup>

`UNDETERMINED` means exactly one thing: nothing could be concluded. It is not a grey
zone between approved and rejected. If the analysis ran at all, the answer is `APPROVED`
or `REJECTED`.

## What gets checked

The transaction type decides which families of checks apply.

| Check | `DOCUMENT_AND_BIOMETRIC` | `DOCUMENT_ONLY` | `BIOMETRIC_ONLY` |
| - | :-: | :-: | :-: |
| Customer status | Yes | Yes | Yes |
| Device integrity | Yes | Yes | Yes |
| Document extraction and legibility | Yes | Yes | — |
| Document authenticity and tampering | Yes | Yes | — |
| Document eligibility and expiry | Yes | Yes | — |
| Data matching against expected values | Yes | Yes | — |
| Consistency with the customer's history | Yes | Yes | — |
| Liveness result and score | Yes | — | Yes |
| Facial attributes, age and gender | Yes | — | Yes |
| Document face vs. liveness face | Yes | — | — |
| Liveness face vs. enrolled face | Yes <sup>1</sup> | — | Yes <sup>1</sup> |
| Document face vs. enrolled face | Yes <sup>1</sup> | Yes <sup>1</sup> | — |

<sup>1</sup> Only when the customer already has an enrolled face. On a first transaction
the comparison is skipped — it is not a rejection — and an informational
`FACE_COMPARISON_SKIPPED_NO_REFERENCE` issue is emitted.

All applicable checks always run to completion. chmod does not stop at the first
rejection, so `issues[]` lists every problem at once rather than making you resubmit to
discover the next one.

## Getting the result

Two ways, and they are not exclusive:

<CardGroup cols={2}>
  <Card title="Webhook" icon="webhook" href="/results/webhooks">
    chmod calls the URL in your transaction config as soon as the decision exists.
    Signed, so you can verify it came from us.
  </Card>

  <Card title="Polling" icon="arrows-rotate" href="/api-reference/transactions/get-transaction">
    Fetch the transaction whenever you want. Useful as a fallback and for reconciliation.
  </Card>
</CardGroup>

Use the webhook as the trigger and the fetch as the source of truth: the webhook tells
you a decision is ready, and the `GET` gives you the full result to act on.


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