> 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/reference/case-management.md).

# Case Management

Case Management keeps all the work you do on a person in one place. Register the person once, open a case for a purpose such as onboarding, and add checks and journeys to it as runs. Each run decides itself, and the case gives you one combined answer.

## How it works

| Term        | What it is                                                                                                                                             |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Subject** | The person. Created once, with your own `reference_id`. A subject can have many cases                                                                  |
| **Case**    | A container on one subject for one purpose (`case_type`), such as onboarding or a periodic review                                                      |
| **Run**     | One piece of work on a case: a run of a workflow, identified by its `transaction_id`. A check that runs immediately, or a journey the person completes |

```
Subject (entity_id)
  └── Case (case_id)            status: open → in_review → resolved
        ├── Run (transaction_id)   e.g. Watchlist          → approved
        ├── Run (transaction_id)   e.g. Address Verification → waiting for the person
        └── ...                    add more at any time
```

Each run decides itself, under the rules of its workflow. A clean pass approves it. A failed check rejects it. Anything unsure goes to a reviewer, who decides it.

The case is combined from its runs every time one of them changes:

| If the case's runs are...                  | Case `status` | Case `resolution` |
| ------------------------------------------ | ------------- | ----------------- |
| Any run waiting for the person, or running | `open`        | `null`            |
| Otherwise, any run waiting for a reviewer  | `in_review`   | `null`            |
| Otherwise, any run rejected                | `resolved`    | `declined`        |
| Otherwise (every run approved)             | `resolved`    | `approved`        |
| No runs yet                                | `open`        | `null`            |

**Resolution.** When the runs settle, the case resolves itself (`resolved_by: "auto"`). You can also resolve it yourself at any point, for example to approve after you've reviewed a rejected run, or to withdraw it. Any work still going stops. Your resolution holds until you add new work. Adding a run to a resolved case reopens it.

| Property         | Value                                                                                                     |
| ---------------- | --------------------------------------------------------------------------------------------------------- |
| Base URL         | `https://consumer-service-api.fraudcheckonline.co.za/consumer-service`                                    |
| Authentication   | Bearer token from `POST /auth/login`                                                                      |
| Ids              | `entity_id`, `case_id` and `transaction_id` are UUIDs                                                     |
| Response type    | `Synchronous` for checks that run immediately. A link, then a later decision, for journeys                |
| Result retrieval | `GET /case-management/runs/{transaction_id}/result`, `GET /case-management/cases/{case_id}`, or a webhook |
| Consent required | `Yes`. In the journey, or declared by you with `consent_obtained`                                         |

{% hint style="info" %}
Need to look something up along the way, such as your workflows, a subject by your reference, or a run by your reference? See [Case Management: Reference API](/consumer-service/reference/case-management/reference-api.md).
{% endhint %}

***

{% stepper %}
{% step %}

## 1. Authenticate

```http
POST https://consumer-service-api.fraudcheckonline.co.za/auth/login
Content-Type: application/json
```

```json
{
  "email": "you@yourcompany.com",
  "password": "your-password"
}
```

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}
```

Send the token on every call:

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

Tokens are short-lived. A `401` means request a new one. See [Authentication](/consumer-service/authentication.md).
{% endstep %}

{% step %}

## 2. Create a subject

The subject is the person. Create them once and reuse `entity_id` (or your own `reference_id`) for every case you open on them.

```http
POST /case-management/entities
```

### Headers

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

### Body parameters

| Field          | Type   | Required | Description                                                                                                                    |
| -------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `subject_type` | string | Yes      | `natural_person` for an adult. See [Subject types](/consumer-service/reference/case-management/reference-api.md#subject-types) |
| `first_name`   | string | Yes      | Given name                                                                                                                     |
| `last_name`    | string | Yes      | Surname                                                                                                                        |
| `reference_id` | string | No       | Your own id for this person (max 150 characters, unique on your account). Use it instead of `entity_id` anywhere               |
| `email`        | string | No       | Needed for a journey invite email                                                                                              |
| `cellphone`    | string | No       | Mobile number                                                                                                                  |
| `id_type`      | string | No       | The identity document type, for example `SA_ID`. Send all three `id_` fields, or none                                          |
| `id_country`   | string | No       | Issuing country, for example `ZA`                                                                                              |
| `id_number`    | string | No       | The document number                                                                                                            |

Most checks need the identity document. Send it when you have it.

### Example request

```json
{
  "subject_type": "natural_person",
  "first_name": "Jane",
  "last_name": "Smith",
  "email": "jane.smith@example.com",
  "cellphone": "0820000000",
  "id_type": "SA_ID",
  "id_country": "ZA",
  "id_number": "8601015800086",
  "reference_id": "CUST-1001"
}
```

### Example response

```json
{
  "entity_id": "a3f2c1d0-1111-4aaa-8bbb-000000000001",
  "reference_id": "CUST-1001",
  "legal_name": "Jane Smith"
}
```

### Already registered?

A subject is unique on your account by `reference_id` and by identity document. A second one is refused with `409`, and the response gives you the existing `entity_id`:

```json
{
  "detail": "A subject with reference_id 'CUST-1001' already exists.",
  "entity_id": "a3f2c1d0-1111-4aaa-8bbb-000000000001"
}
```

Reuse that `entity_id`. You don't need to create the subject again.

Creating a subject is the first event in its audit history, which you can read with [Subject audit history](/consumer-service/reference/case-management/reference-api.md#subject-audit-history).
{% endstep %}

{% step %}

## 3. Create a case (with runs)

Open a case on the subject. You can start its first runs in the same call.

```http
POST /case-management/cases
```

### Body parameters

| Field              | Type          | Required    | Description                                                                       |
| ------------------ | ------------- | ----------- | --------------------------------------------------------------------------------- |
| `entity_id`        | string (UUID) | Conditional | From step 2. Send this or `reference_id`                                          |
| `reference_id`     | string        | Conditional | Your reference for the subject. If you send both, they must name the same subject |
| `case_type`        | string        | Yes         | `onboarding`, `periodic_review`, `remediation` or `investigation`                 |
| `case_name`        | string        | No          | A short name, for example `Retail account onboarding`                             |
| `case_description` | string        | No          | Any notes for the case                                                            |
| `runs`             | array         | No          | Up to 20 runs to start now. Each one is a [run request](#run-request)             |

Every run is checked before anything is created. If one run is invalid, the call returns `400` and no case is created.

### Example request

A watchlist screen that runs immediately, and an address journey for the person:

```json
{
  "reference_id": "CUST-1001",
  "case_type": "onboarding",
  "case_name": "Retail account onboarding",
  "runs": [
    {
      "workflow_key": "fraudcheck_watchlist_verify",
      "client_reference": "APP-77-WL",
      "consent_obtained": true
    },
    {
      "workflow_key": "fraudcheck_address_verification",
      "client_reference": "APP-77-ADDR",
      "send_invite_email": false
    }
  ]
}
```

### Example response

```json
{
  "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
  "entity_id": "a3f2c1d0-1111-4aaa-8bbb-000000000001",
  "reference_id": "CUST-1001",
  "case_type": "onboarding",
  "case_name": "Retail account onboarding",
  "status": "open",
  "runs": [
    {
      "transaction_id": "f19d3e57-3333-4eee-8fff-000000000003",
      "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
      "workflow_key": "fraudcheck_watchlist_verify",
      "run_status": "decided",
      "onboarding_token": null,
      "onboarding_url": null,
      "email_delivery": { "sent": false, "status": "not_applicable" },
      "result": {
        "transaction_id": "f19d3e57-3333-4eee-8fff-000000000003",
        "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
        "entity_id": "a3f2c1d0-1111-4aaa-8bbb-000000000001",
        "reference": "APP-77-WL",
        "workflow": "fraudcheck_watchlist_verify",
        "run_status": "decided",
        "decision": "approved",
        "decided_at": "2026-10-29T10:14:03+02:00",
        "checks": [
          {
            "check": "watchlist_verify",
            "result": "pass",
            "provider_result": "ACCEPT",
            "reason": "ACCEPT",
            "report_url": "https://transactional-report.fraudcheckonline.co.za/api/reports/00000000000000000000000000000000/link"
          }
        ],
        "documents": [],
        "flags": []
      }
    },
    {
      "transaction_id": "0b8a6c21-4444-4abc-8def-000000000004",
      "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
      "workflow_key": "fraudcheck_address_verification",
      "run_status": "waiting_applicant",
      "onboarding_token": "onb_example_token",
      "onboarding_url": "https://journey.apps.fraudcheck.co.za/onboarding/onb_example_token",
      "email_delivery": { "sent": false, "status": "not_requested" },
      "result": null
    }
  ]
}
```

Keep `case_id` and each run's `transaction_id`. The case is `open` here because the address journey is waiting for the person.

### Available workflows

These workflows are pre-configured and ready to use. Each one is a run you add to a case.

| Workflow                                       | `workflow_key`                               | Type             | What the person does                                                              |
| ---------------------------------------------- | -------------------------------------------- | ---------------- | --------------------------------------------------------------------------------- |
| Identity Verification (Basic)                  | `fraudcheck_identity_verification_basic`     | Runs immediately | Nothing                                                                           |
| Watchlist Lite                                 | `fraudcheck_watchlist_lite`                  | Runs immediately | Nothing                                                                           |
| Watchlist                                      | `fraudcheck_watchlist_verify`                | Runs immediately | Nothing. PEPs, sanctions, crime lists and known relatives are screened            |
| Bank Account Verification                      | `fraudcheck_bank_account_verification`       | Runs immediately | Nothing. You send the account in `inputs`                                         |
| Address Verification                           | `fraudcheck_address_verification`            | Journey          | Enters their address. A proof of address is asked for if records don't confirm it |
| Biometric Identity Verification                | `fraudcheck_biometric_identity_verification` | Journey          | Takes a selfie, matched to their official photo                                   |
| Identity Verification (Document with Liveness) | `fraudcheck_document_liveness_verification`  | Journey          | Photographs their ID and takes a selfie                                           |

{% hint style="info" %}
**Need a different workflow?** We can configure any workflow for your process. A workflow can:

* collect information from the person;
* show them text, such as instructions or terms;
* ask them for documents;
* validate and verify those documents and the information they give.

Ask your account manager to set one up. It gets its own `workflow_key`, and you add it to a case as a run in the same way.
{% endhint %}

Your account lists only the workflows enabled for you. See [List workflows](/consumer-service/reference/case-management/reference-api.md#list-workflows).
{% endstep %}

{% step %}

## 4. Add work

Add a run to an existing case at any time.

```http
POST /case-management/cases/{case_id}/runs
```

The body is one [run request](#run-request). The response is the started run, in the same shape as an item of `runs` in step 3.

### Run request

| Field                    | Type    | Required    | Description                                                                                                       |
| ------------------------ | ------- | ----------- | ----------------------------------------------------------------------------------------------------------------- |
| `workflow_key`           | string  | Yes         | The workflow to run. See [Available workflows](#available-workflows)                                              |
| `client_reference`       | string  | No          | Your reference for this run, for example your application number (max 150 characters). Find runs by it later      |
| `consent_obtained`       | boolean | Conditional | Must be `true` for a workflow that runs immediately: you confirm you hold the person's consent                    |
| `inputs`                 | object  | Conditional | Data a workflow that runs immediately needs, such as bank details. Not accepted for a journey                     |
| `send_invite_email`      | boolean | No          | Journeys only. Default `true`: we email the link to the subject. Set `false` to deliver `onboarding_url` yourself |
| `redirect_url`           | string  | No          | Journeys only. Where the person goes after they finish                                                            |
| `redirect_delay_seconds` | integer | No          | Journeys only. Seconds before the redirect, 0–120                                                                 |
| `journey_key`            | string  | No          | Leave it out, unless we've set up more than one journey on the workflow for you                                   |

### Journey or runs immediately

| Workflow type        | You send                                                                     | You get back                            | Next                                                             |
| -------------------- | ---------------------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------- |
| **Runs immediately** | `consent_obtained: true`, and `inputs` if the workflow needs them            | `result` with the decision              | Nothing is asked of the person                                   |
| **Journey**          | Nothing extra. The subject needs an `email` if `send_invite_email` is `true` | `onboarding_url` and `onboarding_token` | The person completes the journey. Read the result later (step 5) |

For a journey, `onboarding_url` is valid for 14 days. `email_delivery.status` is `sent`, `failed` (deliver the link yourself; the run is still valid), `not_requested` or `not_applicable`.

### Example: a journey

```json
{
  "workflow_key": "fraudcheck_biometric_identity_verification",
  "client_reference": "APP-77-BIO",
  "send_invite_email": true,
  "redirect_url": "https://www.example.com/onboarding/done"
}
```

```json
{
  "transaction_id": "5d2e9f10-5555-4a1b-8c2d-000000000005",
  "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
  "workflow_key": "fraudcheck_biometric_identity_verification",
  "run_status": "waiting_applicant",
  "onboarding_token": "onb_example_token_2",
  "onboarding_url": "https://journey.apps.fraudcheck.co.za/onboarding/onb_example_token_2",
  "email_delivery": { "sent": true, "status": "sent" },
  "result": null
}
```

<details>

<summary>Bank Account Verification: the <code>inputs</code></summary>

```json
{
  "workflow_key": "fraudcheck_bank_account_verification",
  "client_reference": "APP-77-BANK",
  "consent_obtained": true,
  "inputs": {
    "bank_name": "FNB",
    "bank_account_number": "62000000000",
    "bank_account_type": "SAVINGS",
    "bank_branch_code": "250655"
  }
}
```

| Input                 | Required | Rule                                                                       |
| --------------------- | -------- | -------------------------------------------------------------------------- |
| `bank_name`           | Yes      | For example `FNB`, `Standard Bank`, `ABSA Bank`, `Nedbank`, `Capitec Bank` |
| `bank_account_number` | Yes      | Digits only                                                                |
| `bank_account_type`   | Yes      | `SAVINGS`, `CURRENT`, `TRANSMISSION`, `BOND` or `SUBSCRIPTION`             |
| `bank_branch_code`    | No       | 6 digits. The bank's universal code is used when omitted                   |

A missing or invalid input is refused with `400` before anything runs. A rejected account's `reason` says what didn't match, for example `Not passed: surname_match`.

</details>

### Adding to a resolved case

You can add a run whatever the case's status. Adding a run to a `resolved` case reopens it: its status is combined from its runs again, including any resolution you set yourself.

### Running a workflow again

A new run of the same workflow on the same case **supersedes** the older one. For example, run Address Verification again after the person moves. The older run is kept, with `superseded_by` set to the new run's `transaction_id`, but it no longer counts towards the case. Cancelled runs don't count either.

{% hint style="info" %}
**Embed a journey in your own app.** Install `@fraudcheck/journey-flow` and pass it `onboarding_token` instead of sending the link. See the package README for usage and events.
{% endhint %}
{% endstep %}

{% step %}

## 5. Results and resolution

### The result of a run

```http
GET /case-management/runs/{transaction_id}/result
```

```json
{
  "transaction_id": "0b8a6c21-4444-4abc-8def-000000000004",
  "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
  "entity_id": "a3f2c1d0-1111-4aaa-8bbb-000000000001",
  "reference": "APP-77-ADDR",
  "workflow": "fraudcheck_address_verification",
  "run_status": "decided",
  "decision": "approved",
  "decided_at": "2026-10-29T12:40:34+02:00",
  "outcome": "ADDRESS_VERIFIED",
  "checks": [
    {
      "check": "address_verification",
      "result": "review",
      "provider_result": "PARTIAL_MATCH",
      "reason": "Street matches but suburb and postal code disagree.",
      "report_url": "https://transactional-report.fraudcheckonline.co.za/api/reports/00000000000000000000000000000000/link"
    }
  ],
  "documents": [],
  "flags": []
}
```

| Field         | Description                                                                                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `run_status`  | Where the run is. See [Run status](#run-status)                                                                                                         |
| `decision`    | `approved` or `rejected`. `null` until the run is decided                                                                                               |
| `decided_at`  | When the decision was made                                                                                                                              |
| `outcome`     | Address Verification only: `ADDRESS_VERIFIED`, `ADDRESS_VERIFIED_BY_DOCUMENT` or `ADDRESS_NOT_VERIFIED`                                                 |
| `checks[]`    | Each check: `result` (`pass`, `review`, `fail`, `error`, `pending`), the provider's `provider_result`, a `reason`, and `report_url` for the full report |
| `documents[]` | Each uploaded document: `verdict` is `PASS` when it was accepted, `attempts` is how many uploads were made, and `reason_codes` say why one wasn't       |
| `flags[]`     | Address matching flags, where there are any                                                                                                             |
| `reference`   | Your `client_reference` for this run                                                                                                                    |

Act on `decision` (and `outcome` for Address Verification). A check can read `review` on an approved run when matching or a document confirmed it.

### Run status

| `run_status`        | Meaning                                                           |
| ------------------- | ----------------------------------------------------------------- |
| `waiting_applicant` | The journey is waiting for the person                             |
| `running`           | Checks are running                                                |
| `in_review`         | Waiting for a reviewer, for example a possible watchlist match    |
| `decided`           | Finished. `decision` is `approved` or `rejected`                  |
| `cancelled`         | Stopped, because the case was withdrawn. Its link no longer works |

### The case

```http
GET /case-management/cases/{case_id}
```

```json
{
  "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
  "case_type": "onboarding",
  "case_name": "Retail account onboarding",
  "case_description": null,
  "status": "resolved",
  "resolution": "approved",
  "resolution_note": null,
  "resolved_by": "auto",
  "resolved_at": "2026-10-29T12:40:35+02:00",
  "created_at": "2026-10-29T10:14:01+02:00",
  "updated_at": "2026-10-29T12:40:35+02:00",
  "subject": {
    "entity_id": "a3f2c1d0-1111-4aaa-8bbb-000000000001",
    "reference_id": "CUST-1001",
    "legal_name": "Jane Smith",
    "subject_type": "natural_person",
    "email": "jane.smith@example.com"
  },
  "runs": [
    {
      "transaction_id": "f19d3e57-3333-4eee-8fff-000000000003",
      "workflow_key": "fraudcheck_watchlist_verify",
      "workflow_name": "Watchlist",
      "run_status": "decided",
      "decision": "approved",
      "decision_reason": null,
      "decided_by": "auto",
      "decided_at": "2026-10-29T10:14:03+02:00",
      "outcome": null,
      "client_reference": "APP-77-WL",
      "superseded_by": null,
      "created_at": "2026-10-29T10:14:01+02:00",
      "completed_at": "2026-10-29T10:14:03+02:00",
      "onboarding_link_status": null,
      "onboarding_link_expires_at": null,
      "onboarding_link_active": false
    },
    {
      "transaction_id": "0b8a6c21-4444-4abc-8def-000000000004",
      "workflow_key": "fraudcheck_address_verification",
      "workflow_name": "Address Verification",
      "run_status": "decided",
      "decision": "approved",
      "decision_reason": null,
      "decided_by": "auto",
      "decided_at": "2026-10-29T12:40:34+02:00",
      "outcome": "ADDRESS_VERIFIED",
      "client_reference": "APP-77-ADDR",
      "superseded_by": null,
      "created_at": "2026-10-29T10:14:01+02:00",
      "completed_at": "2026-10-29T12:40:34+02:00",
      "onboarding_link_status": "active",
      "onboarding_link_expires_at": "2026-11-12T10:14:01+02:00",
      "onboarding_link_active": true
    }
  ]
}
```

| Field                           | Description                                                                                                                                       |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`                        | `open`, `in_review` or `resolved`. See [How it works](#how-it-works)                                                                              |
| `resolution`                    | `approved`, `declined` or `withdrawn`. `null` until resolved                                                                                      |
| `resolved_by`                   | `auto` when combined from the runs. `user:` followed by the user id in your token when you resolved it                                            |
| `resolution_note`               | The note given with the resolution                                                                                                                |
| `runs[]`                        | Every run on the case, oldest first, including superseded and cancelled ones. Use `run_status`, `decision` and `superseded_by` to see which count |
| `runs[].decided_by`             | `auto`, or `user:` followed by the user id of the reviewer who decided the run                                                                    |
| `runs[].onboarding_link_status` | Journeys only: `active`, `used`, `expired` or `revoked`. `onboarding_link_active` is `true` while the link still works                            |

### Resolve the case yourself

```http
POST /case-management/cases/{case_id}/resolve
```

| Field        | Type   | Required    | Description                                                   |
| ------------ | ------ | ----------- | ------------------------------------------------------------- |
| `resolution` | string | Yes         | `approved`, `declined` or `withdrawn`                         |
| `note`       | string | Conditional | Required for `declined` and `withdrawn` (max 4000 characters) |

```json
{
  "resolution": "declined",
  "note": "Customer did not meet our risk appetite."
}
```

```json
{
  "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
  "status": "resolved",
  "resolution": "declined",
  "resolved_by": "user:20002",
  "cancelled_runs": ["0b8a6c21-4444-4abc-8def-000000000004"]
}
```

| Rule                                      | What happens                                                                                                   |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Any point                                 | You can resolve the case whatever its runs are doing. Your resolution overrides the runs                       |
| A run is `waiting_applicant` or `running` | It stops: the run is cancelled, its link stops working, and its `transaction_id` is listed in `cancelled_runs` |
| Audit                                     | The case's audit trail records the resolution, the note, the user who made it and the call's `X-Request-ID`    |
| You add a run later                       | The case reopens and is combined from its runs again                                                           |
| {% endstep %}                             |                                                                                                                |
| {% endstepper %}                          |                                                                                                                |

***

## Webhooks

Instead of polling, Fraudcheck can call your server when something changes. Ask your account manager to set your `https://` callback URL, and to enable case events if you want them.

| Event                   | Sent when                                                                   | `result_url` points to |
| ----------------------- | --------------------------------------------------------------------------- | ---------------------- |
| `run.completed`         | A run is decided (`approved` or `rejected`), automatically or by a reviewer | The run result         |
| `run.document_required` | A journey needs a document from the person, for example a proof of address  | The run result         |
| `case.in_review`        | The case is waiting for a reviewer                                          | The case               |
| `case.resolved`         | The case is resolved: by its runs, by you, or by a withdrawal               | The case               |
| `case.reopened`         | Work was added to a resolved case                                           | The case               |

### Run event

```json
{
  "id": "evt_6f1c0e2a9b4d4e0f8a7c3b2d1e0f9a8b",
  "type": "run.completed",
  "created_at": "2026-10-29T10:40:34+00:00",
  "service_code": "ADDRVER",
  "data": {
    "transaction_id": "0b8a6c21-4444-4abc-8def-000000000004",
    "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
    "entity_id": "a3f2c1d0-1111-4aaa-8bbb-000000000001",
    "reference": "APP-77-ADDR",
    "workflow": "fraudcheck_address_verification",
    "decision": "approved",
    "outcome": "ADDRESS_VERIFIED"
  },
  "result_url": "https://consumer-service-api.fraudcheckonline.co.za/consumer-service/case-management/runs/0b8a6c21-4444-4abc-8def-000000000004/result"
}
```

`reference` is the run's `client_reference`, or the subject's `reference_id` when the run has none. `decision` is `null` on `run.document_required`.

### Case event

```json
{
  "id": "evt_1a2b3c4d5e6f40718293a4b5c6d7e8f9",
  "type": "case.resolved",
  "created_at": "2026-10-29T10:40:35+00:00",
  "service_code": "CASEMGMT",
  "data": {
    "case_id": "c7e1b2a4-2222-4ccc-8ddd-000000000002",
    "entity_id": "a3f2c1d0-1111-4aaa-8bbb-000000000001",
    "reference": "CUST-1001",
    "case_type": "onboarding",
    "status": "resolved",
    "resolution": "approved"
  },
  "result_url": "https://consumer-service-api.fraudcheckonline.co.za/consumer-service/case-management/cases/c7e1b2a4-2222-4ccc-8ddd-000000000002"
}
```

`reference` is the subject's `reference_id`.

### Handle a webhook

1. **Verify the signature.** Each delivery carries `X-Fraudcheck-Signature: t=1761734434,v1=5257a869...`. Compute HMAC-SHA256 with your signing secret over `{t}.{raw request body}` and compare it with `v1`. While a secret is being changed, the header carries one `v1=` per secret: accept any match.
2. **Answer `2xx` quickly.** Any other answer, or no answer, is retried with back-off (after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, then 12 hours).
3. **De-duplicate on `id`.** Delivery is at least once, so the same event can arrive twice.
4. **Fetch the result.** Call `result_url` with your token before you act. A webhook can arrive late or out of order; the result is the record.

***

## Errors

| HTTP      | Meaning                                                                                                                                                                                                                                                                          | Action                                                                         |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| 400       | A request the service can't carry out: the subject type isn't allowed on the workflow, consent wasn't declared, `inputs` are missing or invalid, a journey invite has no email to go to, `entity_id` and `reference_id` name different subjects, or a resolution note is missing | Read `detail`; it names the problem. When creating a case, no case was created |
| 401       | Token missing or expired                                                                                                                                                                                                                                                         | Request a new token                                                            |
| 404       | No such subject, case, run or workflow on your account                                                                                                                                                                                                                           | Check the id, and that the workflow is enabled for you                         |
| 409       | The subject already exists (`reference_id` or identity document)                                                                                                                                                                                                                 | Reuse the `entity_id` in the response                                          |
| 422       | A required field is missing, a value has the wrong type, the JSON is invalid, or an id isn't a UUID                                                                                                                                                                              | Read `detail` for the field                                                    |
| 502 / 504 | The case service didn't respond in time                                                                                                                                                                                                                                          | Retry. Check the case before creating it again                                 |
| 503       | Case Management isn't available                                                                                                                                                                                                                                                  | Retry later                                                                    |

Every response carries an `X-Request-ID` header, for example `X-Request-ID: req_7d1f0c2a9b4e4f3a8c6d5e2b1a0f9e8d`. Keep it with your logs and quote it when you contact support. The subject's audit events record the `request_id` of the call that made them.

{% hint style="info" %}
A negative outcome is not an error. A rejected run returns HTTP `200` with `decision: "rejected"` and the reason in `checks`. A declined case returns `200` with `resolution: "declined"`.
{% endhint %}

***

## Code examples

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

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

# 1. Authenticate
ACCESS_TOKEN=$(curl -s -X POST "https://consumer-service-api.fraudcheckonline.co.za/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"you@yourcompany.com","password":"your-password"}' | jq -r .access_token)
AUTH="Authorization: Bearer $ACCESS_TOKEN"

# 2. Create a subject (a 409 also returns entity_id)
ENTITY_ID=$(curl -s -X POST "$BASE_URL/case-management/entities" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"subject_type":"natural_person","first_name":"Jane","last_name":"Smith",
       "email":"jane.smith@example.com","id_type":"SA_ID","id_country":"ZA",
       "id_number":"8601015800086","reference_id":"CUST-1001"}' | jq -r .entity_id)

# 3. Create a case with a run that runs immediately
CASE=$(curl -s -X POST "$BASE_URL/case-management/cases" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"entity_id\":\"$ENTITY_ID\",\"case_type\":\"onboarding\",
       \"runs\":[{\"workflow_key\":\"fraudcheck_watchlist_verify\",\"client_reference\":\"APP-77-WL\",\"consent_obtained\":true}]}")
CASE_ID=$(echo "$CASE" | jq -r .case_id)
TRANSACTION_ID=$(echo "$CASE" | jq -r '.runs[0].transaction_id')

# 4. Add a journey
curl -s -X POST "$BASE_URL/case-management/cases/$CASE_ID/runs" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"workflow_key":"fraudcheck_address_verification","client_reference":"APP-77-ADDR","send_invite_email":false}' \
  | jq -r .onboarding_url

# 5. Results and resolution
curl -s "$BASE_URL/case-management/runs/$TRANSACTION_ID/result" -H "$AUTH" | jq .decision
curl -s "$BASE_URL/case-management/cases/$CASE_ID" -H "$AUTH" | jq '{status, resolution}'
curl -s -X POST "$BASE_URL/case-management/cases/$CASE_ID/resolve" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"resolution":"withdrawn","note":"Customer cancelled the application."}'
```

{% endtab %}

{% tab title="Python" %}

```python
import requests

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

# 1. Authenticate
login = requests.post(
    "https://consumer-service-api.fraudcheckonline.co.za/auth/login",
    json={"email": "you@yourcompany.com", "password": "your-password"},
)
login.raise_for_status()
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}

# 2. Create a subject (a 409 also returns entity_id)
subject = requests.post(f"{BASE_URL}/case-management/entities", headers=headers, json={
    "subject_type": "natural_person", "first_name": "Jane", "last_name": "Smith",
    "email": "jane.smith@example.com", "id_type": "SA_ID", "id_country": "ZA",
    "id_number": "8601015800086", "reference_id": "CUST-1001",
})
if subject.status_code not in (200, 409):
    subject.raise_for_status()
entity_id = subject.json()["entity_id"]

# 3. Create a case with a run that runs immediately
case = requests.post(f"{BASE_URL}/case-management/cases", headers=headers, json={
    "entity_id": entity_id, "case_type": "onboarding",
    "runs": [{"workflow_key": "fraudcheck_watchlist_verify",
              "client_reference": "APP-77-WL", "consent_obtained": True}],
})
case.raise_for_status()
case_id = case.json()["case_id"]
transaction_id = case.json()["runs"][0]["transaction_id"]

# 4. Add a journey
journey = requests.post(f"{BASE_URL}/case-management/cases/{case_id}/runs", headers=headers, json={
    "workflow_key": "fraudcheck_address_verification",
    "client_reference": "APP-77-ADDR", "send_invite_email": False,
})
journey.raise_for_status()
print(journey.json()["onboarding_url"])

# 5. Results and resolution
result = requests.get(f"{BASE_URL}/case-management/runs/{transaction_id}/result", headers=headers).json()
print(result["decision"])
current = requests.get(f"{BASE_URL}/case-management/cases/{case_id}", headers=headers).json()
print(current["status"], current["resolution"])
requests.post(f"{BASE_URL}/case-management/cases/{case_id}/resolve", headers=headers, json={
    "resolution": "withdrawn", "note": "Customer cancelled the application.",
}).raise_for_status()
```

{% endtab %}

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

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

// 1. Authenticate
const login = await fetch("https://consumer-service-api.fraudcheckonline.co.za/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "you@yourcompany.com", password: "your-password" }),
});
const { access_token } = await login.json();
const headers = { Authorization: `Bearer ${access_token}`, "Content-Type": "application/json" };

// 2. Create a subject (a 409 also returns entity_id)
const subject = await fetch(`${BASE_URL}/case-management/entities`, {
  method: "POST", headers,
  body: JSON.stringify({
    subject_type: "natural_person", first_name: "Jane", last_name: "Smith",
    email: "jane.smith@example.com", id_type: "SA_ID", id_country: "ZA",
    id_number: "8601015800086", reference_id: "CUST-1001",
  }),
});
const { entity_id } = await subject.json();

// 3. Create a case with a run that runs immediately
const created = await (await fetch(`${BASE_URL}/case-management/cases`, {
  method: "POST", headers,
  body: JSON.stringify({
    entity_id, case_type: "onboarding",
    runs: [{ workflow_key: "fraudcheck_watchlist_verify", client_reference: "APP-77-WL", consent_obtained: true }],
  }),
})).json();
const { case_id } = created;
const { transaction_id } = created.runs[0];

// 4. Add a journey
const journey = await (await fetch(`${BASE_URL}/case-management/cases/${case_id}/runs`, {
  method: "POST", headers,
  body: JSON.stringify({
    workflow_key: "fraudcheck_address_verification", client_reference: "APP-77-ADDR", send_invite_email: false,
  }),
})).json();
console.log(journey.onboarding_url);

// 5. Results and resolution
const result = await (await fetch(`${BASE_URL}/case-management/runs/${transaction_id}/result`, { headers })).json();
console.log(result.decision);
const current = await (await fetch(`${BASE_URL}/case-management/cases/${case_id}`, { headers })).json();
console.log(current.status, current.resolution);
await fetch(`${BASE_URL}/case-management/cases/${case_id}/resolve`, {
  method: "POST", headers,
  body: JSON.stringify({ resolution: "withdrawn", note: "Customer cancelled the application." }),
});
```

{% endtab %}
{% endtabs %}

***

## Compliance & consent

* Obtain explicit consent from the person before running any check.
* For a journey, the person gives consent on screen.
* For a workflow that runs immediately, set `consent_obtained` to `true` only once you hold that consent.
* Retain proof of consent and any results in line with POPIA requirements.

***

## FAQ

<details>

<summary>How do I know a case is finished?</summary>

Its `status` is `resolved`, and `resolution` says how. Or listen for the `case.resolved` webhook. A case can reopen if you add work to it later.

</details>

<details>

<summary>Should I open a new case or add a run?</summary>

Add a run when the work is for the same purpose, such as another check during the same onboarding. Open a new case for a new purpose, such as next year's periodic review. Both use the same subject.

</details>

<details>

<summary>Why is my case <code>in_review</code>?</summary>

A run is waiting for a reviewer, for example a possible watchlist match. The reviewer decides the run and the case is combined again. You can also resolve the case yourself.

</details>

<details>

<summary>What happens to the old run when I run a workflow again?</summary>

The new run supersedes it. The old run stays on the case with `superseded_by` set, but only the new run counts.

</details>

<details>

<summary>I no longer have the <code>entity_id</code>, <code>case_id</code> or <code>transaction_id</code>.</summary>

Find the subject by your `reference_id`, a run by your `client_reference`, or the subject's cases with `GET /case-management/cases?entity_id=`. See [Case Management: Reference API](/consumer-service/reference/case-management/reference-api.md).

</details>

<details>

<summary>The webhook says <code>approved</code>. Can I act on that alone?</summary>

Fetch `result_url` first. A webhook can arrive late or twice, and a review or new work can change the answer. The result is the record.

</details>

***

## Changelog

| Date       | Version | Change                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-11-02 | v2.2    | You can resolve a case at any point: work still going stops and is listed in `cancelled_runs`, instead of a `409`. `resolved_by` and `decided_by` name the user (`user:<user_id>`), and the case's audit trail records who resolved it and the request.                                                                                                                                                     |
| 2026-10-30 | v2.1    | New: a subject's audit history (`GET /case-management/entities/{entity_id}/audit`), starting with its creation. Every response carries an `X-Request-ID` header.                                                                                                                                                                                                                                            |
| 2026-10-29 | v2.0    | A case is now a container on a subject. Work is added to it as runs, each with its own `transaction_id`, and the case is combined from its runs. New: add a run, resolve a case, view a case, list runs, and case webhooks. All ids are UUIDs. Subjects take `reference_id` and `id_type`, `id_country` and `id_number`. Removed: `GET /results`, `GET /entities/lookup` and `GET /cases/{case_id}/result`. |
| 2026-10-27 | v1.1    | One guide in four steps. Calls go through the Consumer Service base URL. Helper calls moved to the Reference API page.                                                                                                                                                                                                                                                                                      |
| 2026-10-26 | v1.0    | Initial release.                                                                                                                                                                                                                                                                                                                                                                                            |


---

# 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/reference/case-management.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.
