> For the complete documentation index, see [llms.txt](https://docs.fraudcheck.co.za/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fraudcheck.co.za/consumer-service/identity/identity-verification-document-with-liveness.md).

# Identity Verification - Document with Liveness

Identity Verification Document with Liveness confirms that a consumer holds a genuine identity document and is the live person pictured on it. Create a session with one API call, let the consumer complete a short capture on their phone, then fetch a single decision.

## What the system does

When someone verifies their identity with you, a few specialised services work together behind the scenes to confirm two things: their ID document is genuine, and the person submitting it is a real, live human who matches the photo on that document.

* **Document reading.** As soon as an ID photo (passport, national ID, driving licence, and so on) is captured, the system reads every field off it, such as name, date of birth, ID number and expiry date, without anyone typing it in.
* **Authenticity checks.** While reading the document, the system checks for signs it is genuine. Does the barcode or chip data match what is printed on the page? Does the security text that should appear on this document type (for example "Department of Home Affairs" on a South African ID) actually show up? Does the ID number check out mathematically? Any mismatch is flagged.
* **Matching against what the applicant claimed.** The data read off the document is cross-checked against the details the person entered, confirming they are not submitting someone else's document.
* **Liveness check.** The person takes a quick selfie capture. The system confirms this is a live person in front of the camera right now, not a photo, video replay, or mask.
* **Face match.** The selfie is compared against the photo on the ID document to confirm it is the same person.
* **Final decision.** Everything is combined into one verdict: `PASS`, `REVIEW` (needs a person to check), or `FAIL`, with plain-language reasons for anything that did not check out.

## When to use this

Use this service to:

* Verify a consumer's identity document and confirm they are physically present, without needing an official registry photograph
* Extract the identity details printed on the document (names, ID number, date of birth, nationality)
* Onboard consumers remotely on their own mobile device

## How it works

1. **Log in** to get a bearer token. See [Authentication](/consumer-service/authentication.md).
2. **Create a verification session** with `POST /doc-auth/verify`. The response contains a `capture_url` and a `transaction_id`.
3. **Let the consumer complete the capture**, in one of two ways:
   * **Embed the SDK (recommended).** Pass the `transaction_id` to the [`@fraudcheck/capture-flow` SDK](#embedding-capture-in-your-own-app) and the consumer completes the document and liveness capture inside your app.
   * **Send the link (no code).** Share `capture_url` with the consumer yourself, or include `CellNo` in the request and Fraudcheck texts the link to them. They complete the capture on Fraudcheck's hosted page.
4. **Retrieve the result** with `GET /doc-auth/results/{transaction_id}`. It returns HTTP `404` until the consumer has completed the capture, then a `decision` of `PASS`, `REVIEW`, or `FAIL`.

| Property         | Value                                                                                                                                                                                                       |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Response type    | `Synchronous` (session creation), then poll for the result                                                                                                                                                  |
| Result retrieval | `GET /doc-auth/results/{transaction_id}` once the consumer has completed it                                                                                                                                 |
| Link validity    | 24 hours from session creation                                                                                                                                                                              |
| Consent required | `Yes`                                                                                                                                                                                                       |
| Coverage         | Multiple countries. See [supported countries and documents](/consumer-service/identity/identity-verification-document-with-liveness/identity-verification-document-with-liveness-without-sdk.md#countries). |

{% hint style="info" %}
Capturing the document images in your own system instead? See [Document with Liveness - Without the SDK](/consumer-service/identity/identity-verification-document-with-liveness/identity-verification-document-with-liveness-without-sdk.md) for direct upload and the helper endpoints (countries, documents, quality check, crop).
{% endhint %}

***

## Embedding capture in your own app

By default, hand the consumer `capture_url` (directly, or by SMS via `CellNo`) and let them complete the document upload and liveness check on Fraudcheck's hosted capture page. If you'd rather embed the user journey inside your own app instead of redirecting to that hosted page, use the `@fraudcheck/capture-flow` React SDK.

### Install

```bash
npm install @fraudcheck/capture-flow
```

### Usage

```jsx
import { CaptureFlow } from '@fraudcheck/capture-flow'
import '@fraudcheck/capture-flow/style.css'

function VerifyPage() {
  return (
    <CaptureFlow
      transactionId={transactionId}
      showCompletionScreen={false} // render your own "done" UI instead of ours
      onComplete={({ transactionId, results }) => {
        // Session finished on the client. Treat this as a signal to fetch the
        // result, not as the final result - see the hint below.
      }}
      onError={(err) => {
        // Session bootstrap failed, or the final status update failed after retries
      }}
      onStepChange={({ step, index, total }) => {
        // 'welcome' | 'fields' | 'document' | 'liveness'
      }}
    />
  )
}
```

`transactionId` is the `transaction_id` returned by `POST /doc-auth/verify` - the SDK never creates or reads a transaction ID itself, so you still create the session exactly as described below. All callbacks are optional and fire alongside the SDK's own UI, never instead of it. `showCompletionScreen` (default `true`) is the one exception - set it `false` to suppress just the final "done" screen and show your own instead, driven by `onComplete`. Every other screen in the flow is unaffected.

{% hint style="info" %}
`onComplete` only means the capture session finished on the client. The backend is the source of truth: call `GET /doc-auth/results/{transaction_id}` for the authoritative `decision` and `checks` rather than building UI around the `results` passed to `onComplete`.
{% endhint %}

***

## Authentication

```http
Authorization: Bearer {your_access_token}
```

***

## Endpoints

```http
POST /doc-auth/verify
GET  /doc-auth/results/{transaction_id}
```

## Request

### Headers

| Header          | Required | Value              |
| --------------- | -------- | ------------------ |
| `Authorization` | Yes      | `Bearer {token}`   |
| `Content-Type`  | Yes      | `application/json` |

### Body parameters

| Field                          | Type    | Required | Description                                                                                                                                                                                                         |
| ------------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IdentityNo`                   | string  | No       | Consumer's identity number. If omitted, the consumer is asked for it during capture.                                                                                                                                |
| `FirstName`                    | string  | No       | Consumer's first name. If omitted, the consumer is asked for it during capture.                                                                                                                                     |
| `LastName`                     | string  | No       | Consumer's surname. If omitted, the consumer is asked for it during capture.                                                                                                                                        |
| `ConsentObtainedByDataSubject` | boolean | Yes      | Must be `true`. Confirms POPIA consent.                                                                                                                                                                             |
| `CellNo`                       | string  | No       | Consumer's mobile number. When provided, the capture link is also sent to this number by SMS. Use international format (for example `+254712345678`); a local number starting with `0` is treated as South African. |

### Example request

```json
{
  "IdentityNo": "*************",
  "FirstName": "Jane",
  "LastName": "Smith",
  "ConsentObtainedByDataSubject": true
}
```

### Example request (with SMS)

```json
{
  "IdentityNo": "*************",
  "FirstName": "Jane",
  "LastName": "Smith",
  "ConsentObtainedByDataSubject": true,
  "CellNo": "0821234567"
}
```

***

## Response

### Session response (`POST /doc-auth/verify`)

| Field            | Type    | Description                                                                           |
| ---------------- | ------- | ------------------------------------------------------------------------------------- |
| `capture_url`    | string  | Secure link for the consumer to upload their document and complete the liveness check |
| `transaction_id` | string  | Unique request identifier. Use it to retrieve the result.                             |
| `sms_sent`       | boolean | Only returned when `CellNo` was sent. `true` if the capture link was sent by SMS.     |
| `sms_message`    | string  | Only returned when `CellNo` was sent. Confirms delivery, or explains why it failed.   |

### Example response

```json
{
  "capture_url": "https://capture.apps.fraudcheck.co.za/f2e0272fd426d3499b4459ba6eccbd61",
  "transaction_id": "f2e0272fd426d3499b4459ba6eccbd61"
}
```

### Example response (with `CellNo`)

```json
{
  "capture_url": "https://capture.apps.fraudcheck.co.za/f2e0272fd426d3499b4459ba6eccbd61",
  "transaction_id": "f2e0272fd426d3499b4459ba6eccbd61",
  "sms_sent": true,
  "sms_message": "Capture link sent via SMS"
}
```

{% hint style="info" %}
If the SMS cannot be delivered, the session is still created: `sms_sent` is `false`, `sms_message` explains why, and `capture_url` still works. Share the link with the consumer yourself.
{% endhint %}

***

## Retrieving the result

Call this endpoint after the consumer has completed the capture link.

```http
GET /doc-auth/results/{transaction_id}
Authorization: Bearer {your_access_token}
```

### Path parameter

| Parameter        | Type   | Required | Description                                              |
| ---------------- | ------ | -------- | -------------------------------------------------------- |
| `transaction_id` | string | Yes      | The `transaction_id` returned by `POST /doc-auth/verify` |

### Response fields

| Field            | Type   | Description                                                         |
| ---------------- | ------ | ------------------------------------------------------------------- |
| `decision`       | string | Overall outcome: `PASS`, `REVIEW`, or `FAIL`                        |
| `checks`         | object | Individual check results (see below)                                |
| `reasons`        | array  | Why the verification did not pass. Empty when `decision` is `PASS`. |
| `warnings`       | array  | Non-blocking observations about the capture                         |
| `document`       | object | Document `type`, issuing `country`, and `date_of_issue`             |
| `identity`       | object | Identity details extracted from the document (see below)            |
| `transaction_id` | string | The transaction ID requested                                        |
| `created_at`     | string | When the verification result was first recorded (UTC)               |
| `completed_at`   | string | When the verification completed (UTC)                               |
| `schema_version` | string | Version of this response format                                     |

### `checks` object

| Field        | Type   | Description                                               |
| ------------ | ------ | --------------------------------------------------------- |
| `document`   | object | `passed`: the identity document is authentic and readable |
| `liveness`   | object | `passed`: the selfie was taken of a live person           |
| `face_match` | object | `passed`: the selfie matches the portrait on the document |

### `identity` object

| Field              | Type   | Description                               |
| ------------------ | ------ | ----------------------------------------- |
| `full_name`        | string | Full name as printed on the document      |
| `first_names`      | string | Given names                               |
| `surname`          | string | Surname                                   |
| `identity_number`  | string | Identity number                           |
| `date_of_birth`    | string | Date of birth (`YYYY-MM-DD`)              |
| `sex`              | string | `M` or `F`                                |
| `nationality`      | string | Nationality                               |
| `country_of_birth` | string | Country of birth                          |
| `status`           | string | Citizenship status, for example `CITIZEN` |

### Decision codes

| Decision | Meaning                                                                           |
| -------- | --------------------------------------------------------------------------------- |
| `PASS`   | Document authentic, liveness passed, and the selfie matched the document portrait |
| `REVIEW` | The result needs manual review                                                    |
| `FAIL`   | One or more checks did not pass. See `reasons`.                                   |

### Example response (passed)

```json
{
  "checks": {
    "document": {
      "passed": true
    },
    "liveness": {
      "passed": true
    },
    "face_match": {
      "passed": true
    }
  },
  "reasons": [],
  "decision": "PASS",
  "document": {
    "type": "IDENTITY_CARD",
    "country": "ZAF",
    "date_of_issue": "2015-10-20"
  },
  "identity": {
    "sex": "F",
    "status": "CITIZEN",
    "surname": "SMITH",
    "full_name": "JANE MARY SMITH",
    "first_names": "JANE MARY",
    "nationality": "RSA",
    "date_of_birth": "1995-09-03",
    "identity_number": "*************",
    "country_of_birth": "RSA"
  },
  "warnings": [],
  "created_at": "2026-10-21T14:01:14Z",
  "completed_at": "2026-10-21T14:01:14Z",
  "schema_version": "1.0",
  "transaction_id": "f2e0272fd426d3499b4459ba6eccbd61"
}
```

### Example response (face match failed)

```json
{
  "checks": {
    "document": {
      "passed": true
    },
    "liveness": {
      "passed": true
    },
    "face_match": {
      "passed": false
    }
  },
  "reasons": [
    "The live selfie did not sufficiently match the portrait on the identity document."
  ],
  "decision": "FAIL",
  "document": {
    "type": "IDENTITY_CARD",
    "country": "ZAF",
    "date_of_issue": "2015-10-20"
  },
  "identity": {
    "sex": "F",
    "status": "CITIZEN",
    "surname": "SMITH",
    "full_name": "JANE MARY SMITH",
    "first_names": "JANE MARY",
    "nationality": "RSA",
    "date_of_birth": "1995-09-03",
    "identity_number": "*************",
    "country_of_birth": "RSA"
  },
  "warnings": [],
  "created_at": "2026-10-21T13:57:14Z",
  "completed_at": "2026-10-21T13:57:14Z",
  "schema_version": "1.0",
  "transaction_id": "f2e0272fd426d3499b4459ba6eccbd61"
}
```

***

## Errors

| HTTP | Code            | Meaning                                                                                 | Action                                            |
| ---- | --------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------- |
| 422  | `invalid_input` | Required field missing or consent not confirmed                                         | See `missing_fields` or `message`                 |
| 422  | `invalid_phone` | `CellNo` could not be recognised as a valid mobile number                               | Correct the number, or omit it and share the link |
| 401  | `unauthorized`  | Token missing or expired                                                                | Refresh your access token                         |
| 403  | `forbidden`     | Account lacks access to this service                                                    | Contact your account manager                      |
| 404  | `not_found`     | No result yet (the consumer has not completed the capture), or `transaction_id` unknown | Poll again later; confirm the transaction ID      |
| 503  | `unavailable`   | The capture service is temporarily unavailable                                          | Retry; contact support if persistent              |

{% hint style="info" %}
A negative outcome is not an error. A forged document, a failed liveness check, or a mismatched selfie all return HTTP `200` with `decision: FAIL` and the cause in `reasons`.
{% endhint %}

***

## Code examples

{% tabs %}
{% tab title="cURL" %}

```bash
BASE_URL="https://consumer-service-api.fraudcheckonline.co.za/consumer-service"

curl -X POST "$BASE_URL/doc-auth/verify" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "IdentityNo": "*************",
    "FirstName": "Jane",
    "LastName": "Smith",
    "ConsentObtainedByDataSubject": true
  }'

curl -X GET "$BASE_URL/doc-auth/results/$TRANSACTION_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

{% endtab %}

{% tab title="Python" %}

```python
import requests

BASE_URL = "https://consumer-service-api.fraudcheckonline.co.za/consumer-service"
headers = {"Authorization": f"Bearer {access_token}"}

session = requests.post(
    f"{BASE_URL}/doc-auth/verify",
    headers=headers,
    json={
        "IdentityNo": "*************",
        "FirstName": "Jane",
        "LastName": "Smith",
        "ConsentObtainedByDataSubject": True,
    },
).json()

print("Send this link to the consumer:", session["capture_url"])

# Later, once the consumer has completed the capture:
result = requests.get(
    f"{BASE_URL}/doc-auth/results/{session['transaction_id']}",
    headers=headers,
)
if result.status_code == 404:
    print("Not completed yet - try again later")
else:
    final = result.json()
    print("Decision:", final["decision"], final["reasons"])
```

{% endtab %}

{% tab title="JavaScript (Node.js)" %}

```javascript
const BASE_URL = "https://consumer-service-api.fraudcheckonline.co.za/consumer-service";
const headers = { Authorization: `Bearer ${accessToken}` };

const session = await (
  await fetch(`${BASE_URL}/doc-auth/verify`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      IdentityNo: "*************",
      FirstName: "Jane",
      LastName: "Smith",
      ConsentObtainedByDataSubject: true,
    }),
  })
).json();

console.log("Send this link to the consumer:", session.capture_url);

// Later, once the consumer has completed the capture:
const result = await fetch(
  `${BASE_URL}/doc-auth/results/${session.transaction_id}`,
  { headers }
);
if (result.status === 404) {
  console.log("Not completed yet - try again later");
} else {
  const final = await result.json();
  console.log("Decision:", final.decision, final.reasons);
}
```

{% endtab %}
{% endtabs %}

***

## Compliance & consent

* Obtain explicit consent from the data subject before submitting a request.
* Set `ConsentObtainedByDataSubject` to `true` to confirm consent has been captured.
* Retain proof of consent, and any retrieved identity details, in line with POPIA requirements.

***

## FAQ

<details>

<summary>How is this different from Identity Verification - Enhanced with Liveness?</summary>

Enhanced with Liveness matches the selfie against the official DHA photograph. Document with Liveness matches the selfie against the portrait on the identity document the consumer uploads, and authenticates that document.

</details>

<details>

<summary>Why does the results endpoint return 404?</summary>

The consumer has not completed the capture link yet. Poll again later. A result is only available once the document upload and liveness check are finished.

</details>

<details>

<summary>Do I have to send the link to the consumer myself?</summary>

Not if you include `CellNo` in the request; Fraudcheck will text the link to that number. Without `CellNo`, share `capture_url` through your own channel (email, in-app, or a redirect), or embed the whole journey in your app with the [`@fraudcheck/capture-flow` SDK](#embedding-capture-in-your-own-app).

</details>

<details>

<summary>Can I integrate without the SDK?</summary>

Yes. You can send the consumer `capture_url` (or text it with `CellNo`) and they complete the capture on Fraudcheck's hosted page. If you need to capture the document images in your own system, see [Document with Liveness - Without the SDK](/consumer-service/identity/identity-verification-document-with-liveness/identity-verification-document-with-liveness-without-sdk.md). We recommend the SDK or hosted page because they check image quality during capture.

</details>

<details>

<summary>How long is the capture link valid?</summary>

24 hours from when the session is created. After that, create a new session.

</details>

***

## Changelog

| Date       | Version | Change                                                                                                                                                                                                                                                                                                                      |
| ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-10-21 | v1.1    | Split into this SDK page and a [Without the SDK](/consumer-service/identity/identity-verification-document-with-liveness/identity-verification-document-with-liveness-without-sdk.md) page for direct upload and helper endpoints.                                                                                          |
| 2026-10-21 | v1.0    | Initial release. Replaces Identity Document Authentication. Optional SMS delivery of the capture link via `CellNo`, embeddable capture via the `@fraudcheck/capture-flow` React SDK, direct document upload via `POST /doc-auth/upload`, and helper endpoints for countries, documents by country, quality check, and crop. |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.fraudcheck.co.za/consumer-service/identity/identity-verification-document-with-liveness.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
