> For the complete documentation index, see [llms.txt](https://developer.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://developer.fraudcheck.co.za/identity/basic-identity-verification.md).

# Identity Verification - Basic

Identity verification for South African ID holders using multiple identity data sources and optional live verification. Requests are automatically routed across available verification sources to improve uptime and verification success rates.

A single request returns one standardised response with clear verification results and decision codes. All routing, failover, retry logic, and response handling are managed server-side.

The service also includes SA ID checksum validation, improved name matching, configurable matching thresholds, and optional live verification for difficult-to-verify identities.

## When to use this

Use this service to:

* Verify a South African ID number against name and surname details
* Improve verification success rates through multi-source verification routing
* Support onboarding and KYC flows that require high availability
* Verify identities using both on-file and optional live verification
* Reduce failed verifications caused by name formatting and spelling variations
* Avoid implementing retry, failover, or source-routing logic

## How it works

Submit an SA ID number, first name, and surname in a single POST request. The service validates the ID number, then automatically routes the request across multiple verification sources. The verification first checks against two on-file identity data sources and, if enabled and no record is found, escalates to a live Department of Home Affairs (DHA) verification.

Responses are returned synchronously in a single standardised response, including the matched record, verification outcomes, and final decision code..

| Property              | Value                                                         |
| --------------------- | ------------------------------------------------------------- |
| Response type         | `Synchronous`                                                 |
| Typical response time | < 5s (may increase if additional verification paths are used) |
| Result retrieval      | Returned inline                                               |
| Consent required      | `Yes`                                                         |
| Region                | South Africa                                                  |

***

## Authentication

All requests require a Bearer token in the `Authorization` header.

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

***

## Endpoint

```http
POST /idv/kyc/verify
```

## Request

### Headers

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

### Body parameters

| Field                          | Type    | Required | Description                                                                                                                                                   |
| ------------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IdentityNo`                   | string  | Yes      | 13-digit South African ID number                                                                                                                              |
| `FirstName`                    | string  | Yes      | Consumer's first name                                                                                                                                         |
| `LastName`                     | string  | Yes      | Consumer's surname                                                                                                                                            |
| `SecondName`                   | string  | No       | Second or middle name                                                                                                                                         |
| `CostCenter`                   | integer | No       | Client-side cost centre reference, recorded against this transaction for billing/audit purposes                                                               |
| `ConsentObtainedByDataSubject` | boolean | No       | Confirms POPIA consent. Recommended `true`.                                                                                                                   |
| lenientThreshold               | number  | No       | Given name match threshold between 0.0 and 1.0. Defaults to 0.95. See [Matching behaviour](#matching-behaviour) for how this interacts with surname matching. |
| includeLiveDHA                 | boolean | No       | Enables live DHA verification if no record is found from the two on-file data sources                                                                         |

### Example request

```json
{
  "IdentityNo": "*************",
  "FirstName": "TSH**** SHE**",
  "LastName": "Smith",
  "CostCenter": 1001,
  "ConsentObtainedByDataSubject": true,
  "lenientThreshold": 0.75,
  "includeLiveDHA": false
}
```

***

## Matching behaviour

`lenientThreshold` controls how strictly the submitted **given name** is matched against the official record (via Jaro-Winkler similarity), and also governs **swap detection** (see below). It does **not** relax surname matching in the same way:

* **Given name** matched against `threshold` (defaults to 0.95, or whatever `lenientThreshold` you supply). A lower value tolerates spelling variance, missing/extra middle names, and given-name ordering.
* **Surname** always requires **at least 95% similarity**, regardless of how low `lenientThreshold` is set. This tolerates a single-character typo (e.g. `"Siyba"` for `"Siyoba"`) while still rejecting a genuinely different surname, even under a very lenient threshold.
* **Swap detection** if the submitted given name matches the record's surname *and* the submitted surname matches the record's given name (both directions clearing `lenientThreshold`), the request is treated as the same identity with the two fields entered in reverse order, and is **accepted**.
* **Maiden-name detection** in certain surname-mismatch cases, the service detects a pattern consistent with a marriage-related surname change and notes this in the message. The outcome is still `REJECT` resubmit using the applicant's correct surname to obtain a positive match.

***

## Response

The service returns one standardised response, regardless of which verification source was used.

The response is grouped into three sections:

* `request` the details submitted in the request
* `record` the identity record returned from the verification source
* `outcome` the field-level match results and final verification outcome

### Common fields

| Field            | Type    | Description                                                                                                    |
| ---------------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `success`        | boolean | `true` if either source verified the identity                                                                  |
| `transaction_id` | string  | Unique request identifier. A `failover` suffix indicates the secondary source responded.                       |
| `code`           | string  | Final decision code: `ACCEPT`, `REJECT`, `FAIL`, or `REVIEW` (see [Decision code logic](#decision-code-logic)) |
| `message`        | string  | Human-readable explanation of the result, written for the integrator                                           |
| `request`        | object  | Details submitted in the request                                                                               |
| `record`         | object  | Identity record returned by the verification source                                                            |
| `outcome`        | object  | Field-level verification results                                                                               |

### Outcome object fields

| Field                         | Type    | Description                                                                                                                |
| ----------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
| `id_found`                    | boolean | `false` only when no verification source has any record for the submitted ID number                                        |
| `id_matched`                  | boolean | The submitted ID number matches the record returned by the verification source                                             |
| `first_name_matched`          | boolean | The submitted given name cleared `lenientThreshold` against the record                                                     |
| `first_name_similarity_score` | number  | Jaro-Winkler similarity score for the given name comparison                                                                |
| `first_name_threshold`        | number  | The threshold actually applied (your `lenientThreshold`, or the 0.95 default)                                              |
| `last_name_matched`           | boolean | The submitted surname matches the record (always requires ≥ 95% similarity: see [Matching behaviour](#matching-behaviour)) |
| `last_name_similarity_score`  | number  | Jaro-Winkler similarity score for the surname comparison                                                                   |
| `last_name_threshold`         | number  | The threshold actually applied to surname matching (always ≥ 0.95, regardless of `lenientThreshold`)                       |
| `names_swapped`               | boolean | `true` when the given name and surname appear to be entered in reverse order relative to the record                        |
| `dob_matched`                 | boolean | Date of birth parsed from the ID number matches the record's date of birth                                                 |
| `id_valid`                    | string  | Result of the SA ID checksum validation                                                                                    |

### Decision code logic

| Scenario                                                                  | code   | Meaning                                                                                         |
| ------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------- |
| ID number, given name, and surname all matched                            | ACCEPT | Identity verification passed                                                                    |
| Given name and surname matched, but appear swapped relative to the record | ACCEPT | Treated as the same identity with the fields reversed                                           |
| ID checksum fails                                                         | REJECT | ID number is structurally invalid                                                               |
| ID found, but the submitted surname or given name does not match          | REJECT | Identity details do not match the record (message may note a likely maiden-name/surname change) |
| No record found from any available verification source                    | FAIL   | Verification could not be completed                                                             |
| Live DHA verification was attempted but the connection was unavailable    | REVIEW | DHA attempt                                                                                     |

### Example: accepted (clean match)

```json
{
  "success": true,
  "transaction_id": "d41d8cd98f00b204e9800998ecf8427e",
  "code": "ACCEPT",
  "message": "The submitted ID number, surname, and given name all match the verified Home Affairs record for this identity.",
  "request": {
    "id_number": "*************",
    "first_name": "John",
    "last_name": "Smith",
    "date_of_birth": "1986-01-01"
  },
  "record": {
    "id_number": "*************",
    "first_names": "JOHN",
    "surname": "SMITH",
    "date_of_birth": "1986-01-01",
    "deceased_status": "Alive",
    "deceased_date": null,
    "id_book_issued_date": "",
    "id_card_issued": null,
    "id_card_date": null
  },
  "outcome": {
    "id_found": true,
    "id_matched": true,
    "first_name_matched": true,
    "first_name_similarity_score": 1.0,
    "first_name_threshold": 0.95,
    "last_name_matched": true,
    "last_name_similarity_score": 1.0,
    "last_name_threshold": 0.95,
    "names_swapped": false,
    "dob_matched": true,
    "id_valid": "Identity passed checksum"
  }
}
```

### Example: accepted (given name and surname swapped)

Submitted with `FirstName: "Smith"`, `LastName: "John"` reversed relative to the record. Both cross-direction checks clear `lenientThreshold`, so this is accepted as the same identity rather than rejected or flagged for review.

```json
{
  "success": true,
  "transaction_id": "d41d8cd98f00b204e9800998ecf8427e",
  "code": "ACCEPT",
  "message": "The ID number belongs to a real, verified identity at Home Affairs. The submitted given name and surname appear to be swapped compared to the official record - accepted as the same person. Official record: given name(s) [JOHN], surname [SMITH].",
  "request": {
    "id_number": "8601015800086",
    "first_name": "Smith",
    "last_name": "John",
    "date_of_birth": "1986-01-01"
  },
  "record": {
    "id_number": "8601015800086",
    "first_names": "JOHN",
    "surname": "SMITH",
    "date_of_birth": "1986-01-01",
    "deceased_status": "Alive",
    "deceased_date": null,
    "id_book_issued_date": "",
    "id_card_issued": null,
    "id_card_date": null
  },
  "outcome": {
    "id_found": true,
    "id_matched": true,
    "first_name_matched": false,
    "first_name_similarity_score": 0.0,
    "first_name_threshold": 0.75,
    "last_name_matched": false,
    "last_name_similarity_score": 0.0,
    "last_name_threshold": 0.95,
    "names_swapped": true,
    "dob_matched": true,
    "id_valid": "Identity passed checksum"
  }
}
```

### Example: accepted (surname typo tolerated)

Submitted surname `"Siyba"` against a record surname of `"SIYOBA"` — a single missing letter. This clears the 95% surname similarity floor even though `lenientThreshold` was set very low (`0.5`) for the given name.

```json
{
  "success": true,
  "transaction_id": "d41d8cd98f00b204e9800998ecf8427e",
  "code": "ACCEPT",
  "message": "The submitted ID number, surname, and given name all match the verified Home Affairs record for this identity.",
  "request": {
    "id_number": "*********",
    "first_name": "Tshepo",
    "last_name": "Siyba",
    "date_of_birth": "1995-09-03"
  },
  "record": {
    "id_number": "********",
    "first_names": "TSHEPO",
    "surname": "SIYOBA",
    "date_of_birth": "1995-09-03",
    "deceased_status": "Unknown",
    "deceased_date": null,
    "id_book_issued_date": "",
    "id_card_issued": false,
    "id_card_date": null
  },
  "outcome": {
    "id_found": true,
    "id_matched": true,
    "first_name_matched": true,
    "first_name_similarity_score": 1.0,
    "first_name_threshold": 0.5,
    "last_name_matched": true,
    "last_name_similarity_score": 0.9611,
    "last_name_threshold": 0.95,
    "names_swapped": false,
    "dob_matched": true,
    "id_valid": "Identity passed checksum"
  }
}
```

### Example: rejected (ID checksum invalid)

```json
{
  "success": false,
  "transaction_id": "d41d8cd98f00b204e9800998ecf8427e",
  "code": "REJECT",
  "message": "ID number failed checksum validation",
  "request": {
    "id_number": "*************",
    "first_name": "John",
    "last_name": "Smith",
    "date_of_birth": ""
  },
  "record": {
    "id_number": "",
    "first_names": "",
    "surname": "",
    "date_of_birth": "",
    "deceased_status": "Unknown",
    "deceased_date": null,
    "id_book_issued_date": "",
    "id_card_issued": null,
    "id_card_date": null
  },
  "outcome": {
    "id_found": false,
    "id_matched": false,
    "first_name_matched": false,
    "first_name_similarity_score": 0.0,
    "first_name_threshold": 0.95,
    "last_name_matched": false,
    "last_name_similarity_score": 0.0,
    "last_name_threshold": 0.95,
    "names_swapped": false,
    "dob_matched": false,
    "id_valid": "Identity failed checksum"
  }
}
```

### Example - rejected (surname mismatch, likely maiden name)

Using the request from [Example request](#example-request) above — given name matches the record exactly, but the submitted surname is a genuine mismatch, not a typo or swap. Since the ID number's encoded sex is Female, the message notes this may indicate a marriage-related surname change:

```json
{
  "success": true,
  "transaction_id": "example-txn-id",
  "code": "REJECT",
  "message": "The ID number belongs to a real, verified identity at Home Affairs, and the submitted given name matches that record, but the submitted surname does not ([Smith] vs [MALULEKE]). This may indicate a surname change due to marriage.",
  "request": {
    "id_number": "*********",
    "first_name": "TSHI**** SHEILA",
    "last_name": "Smith",
    "date_of_birth": "2000-04-30"
  },
  "record": {
    "id_number": "**********",
    "first_names": "TSHI**** SHEILA",
    "surname": "MALULEKE",
    "date_of_birth": "2000-04-30",
    "deceased_status": "Alive",
    "deceased_date": null,
    "id_book_issued_date": "",
    "id_card_issued": false,
    "id_card_date": null
  },
  "outcome": {
    "id_found": true,
    "id_matched": true,
    "first_name_matched": true,
    "first_name_similarity_score": 1.0,
    "first_name_threshold": 0.75,
    "last_name_matched": false,
    "last_name_similarity_score": 0.4417,
    "last_name_threshold": 0.95,
    "names_swapped": false,
    "dob_matched": true,
    "id_valid": "Identity passed checksum"
  }
}
```

`success: true` here reflects that the ID number itself was found and verified at Home Affairs — `code: REJECT` is the actual verification outcome. This distinction matters for integrators: check `code`, not just `success`, to determine whether the identity was accepted.

## Errors

| HTTP | Code           | Meaning                                                             | Action                                                                                          |
| ---- | -------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| 422  | invalid\_input | Required field missing, consent not confirmed, or invalid ID format | Provide `IdentityNo`, `FirstName`, `LastName`, and set `ConsentObtainedByDataSubject` to `true` |
| 401  | unauthorized   | Token expired or invalid                                            | Refresh the access token                                                                        |
| 403  | forbidden      | Account does not have access to this service                        | Contact your account manager                                                                    |
| 500  | server\_error  | Unexpected error                                                    | Retry or contact support if the issue persists                                                  |

{% hint style="info" %}
A negative verification result is returned as HTTP `200` with a decision code such as `REJECT`, `FAIL`, or `REVIEW`. This means the API call was processed successfully, but the identity could not be accepted. Check `code`, not `success`, to determine the outcome — `success: true` can still accompany a `REJECT` when the ID number itself was found and verified, but the submitted name details didn't match.
{% endhint %}

## Code examples

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

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

curl -X POST "$BASE_URL/idv/kyc/verify" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "IdentityNo": "************",
    "FirstName": "John",
    "LastName": "Smith",
    "CostCenter": 1001,
    "ConsentObtainedByDataSubject": true,
    "lenientThreshold": 0.95,
    "includeLiveDHA": true
  }'
```

{% endtab %}

{% tab title="Python" %}

```python
import requests

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

response = requests.post(
    f"{BASE_URL}/idv/kyc/verify",
    headers={
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
    },
    json={
        "IdentityNo": "************",
        "FirstName": "John",
        "LastName": "Smith",
        "CostCenter": 1001,
        "ConsentObtainedByDataSubject": True,
        "lenientThreshold": 0.95,
        "includeLiveDHA": True,
    },
)

result = response.json()

print("Decision:", result.get("code"))
print("Message:", result.get("message"))

if result.get("success"):
    print("Matched Record:", result.get("record"))
    print("Verification Outcome:", result.get("outcome"))
```

{% endtab %}

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

```javascript
const BASE_URL = "https://consumer-service-api.fraudcheckonline.co.za/consumer-service";

const response = await fetch(`${BASE_URL}/idv/kyc/verify`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    IdentityNo: "************",
    FirstName: "John",
    LastName: "Smith",
    CostCenter: 1001,
    ConsentObtainedByDataSubject: true,
    lenientThreshold: 0.95,
    includeLiveDHA: true,
  }),
});

const result = await response.json();

console.log("Decision:", result.code);
console.log("Message:", result.message);

if (result.success) {
  console.log("Matched Record:", result.record);
  console.log("Verification Outcome:", result.outcome);
}
```

{% endtab %}
{% endtabs %}

***

## Compliance & consent

Explicit consent must be obtained from the data subject before submitting a verification request.

`ConsentObtainedByDataSubject` must be set to `true` for all requests. Requests without confirmed consent will be rejected.

Proof of consent should be retained in line with POPIA and internal compliance requirements.

***

## FAQ

<details>

<summary>Do I need to implement retry logic?</summary>

No. Failover between sources is handled server-side in a single API call.

</details>

<details>

<summary>When is live DHA verification use</summary>

Live DHA verification is only attempted when:

* `includeLiveDHA` is set to `true`
* no record is found from the two on-file verification sources

</details>

<details>

<summary>How long does verification take?</summary>

Most requests complete in under 5 seconds. Response times may increase when additional verification paths or live DHA verification are used.

</details>

<details>

<summary>What do the decision codes mean?</summary>

| Code   | Meaning                                                                                    |
| ------ | ------------------------------------------------------------------------------------------ |
| ACCEPT | Identity verification passed, including cases where a given name/surname swap was detected |
| REJECT | Identity details did not match, or the ID number failed checksum validation                |
| FAIL   | No identity record could be found from any available verification source                   |
| REVIEW | Live DHA verification was attempted but the connection was unavailable                     |

</details>

<details>

<summary>Why did I get ACCEPT when the given name and surname look mismatched?</summary>

If the submitted given name matches the record's surname, and the submitted surname matches the record's given name, the service treats this as the same identity with the two fields entered in reverse order and returns `ACCEPT`. Check `outcome.names_swapped` to detect this case programmatically: the `message` field will also explain it.

</details>

<details>

<summary>Can lenientThreshold cause an incorrect surname to be accepted?</summary>

No. `lenientThreshold` only relaxes given-name matching. Surname matching always requires at least 95% similarity, regardless of how low `lenientThreshold` is set this tolerates a single-character typo while still rejecting a genuinely different surname.

</details>

<details>

<summary>Why does the response mention marriage/maiden names?</summary>

When the given name matches the record but the surname is a genuine mismatch, and the ID number's encoded sex is Female, the `REJECT` message notes this may be due to a surname change from marriage. This is informational only: resubmit with the correct (e.g. maiden) surname for a positive match.

</details>

***

## Changelog

| Date       | Version | Change                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-07-02 | v2.1    | Given name/surname swaps now resolve to `ACCEPT` instead of `REVIEW`; surname matching enforces a 95% similarity floor independent of `lenientThreshold`; added maiden-name detection for likely marriage-related surname mismatches; added `outcome.names_swapped`; added `CostCenter` request field; all response messages rewritten for the integrator audience. `REVIEW` is now reserved solely for live DHA connectivity failures. |
| 2026-05-19 | v2.0    | Added enhanced KYC verification route with checksum validation, adaptive name matching, configurable thresholds, grouped responses, deterministic decision codes, and optional live DHA verification                                                                                                                                                                                                                                    |
| 2026-04-11 | v1.0    | Initial release.                                                                                                                                                                                                                                                                                                                                                                                                                        |
