> ## Documentation Index
> Fetch the complete documentation index at: https://docs.range.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Triage a manual case

> Create a manual case, enter review, add a comment, optionally link an investigation, and verify dismissal and the audit trail.

Create and review a synthetic manual case, then verify its dismissal and final audit trail.

## Prerequisites

* A workspace API key configured with the [shared quickstart](https://docs.range.org/introduction/quickstart.md), using Bearer authentication through `RANGE_API_KEY`.
* Python with `httpx` installed: `python -m pip install httpx`.
* A workspace where you can create synthetic manual cases. These requests use `/v2` endpoints and a name-only counterparty subject, so no existing counterparty ID is needed.

This procedure covers `source_type: manual` only. Do not apply its permission assumptions to screening cases. Use the [Platform test inputs](https://docs.range.org/resources/test-inputs.md#platform-api) and the IDs returned by your own requests. Recorded response IDs below belong to a completed verification run.

Run this setup once:

```python theme={null}
import os
import uuid
import httpx

run_id = uuid.uuid4().hex
title = f"docs-test-{run_id}-case"
api = httpx.Client(
    base_url="https://api.range.org",
    headers={"Authorization": f"Bearer {os.environ['RANGE_API_KEY']}",
             "User-Agent": "curl/8.4.0"},
    timeout=30,
)

def call(method, path, **kwargs):
    response = api.request(method, path, **kwargs)
    response.raise_for_status()
    return response.json() if response.content else None
```

## 1. Create a manual case

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/post-cases.md).

```python theme={null}
case = call("POST", "/v2/cases", json={
    "source_type": "manual", "title": title, "severity": "low",
    "subject": {"type": "counterparty", "name": title},
})
case_id = case["id"]
assert case["status"] == "open"
```

Recorded response (HTTP 201), abbreviated where fields are omitted:

```json theme={null}
{
  "id": "52f32bff-01f7-4ab2-91a6-f2a5273c30b7",
  "title": "docs-test-b5684b40251e420fb73b8583d43b7d24-case",
  "source_type": "manual",
  "subject": {
    "type": "counterparty",
    "name": "docs-test-b5684b40251e420fb73b8583d43b7d24-case"
  },
  "severity": "low",
  "status": "open",
  "resolution": null,
  "viewed": false,
  "created_at": "2026-10-20T22:31:48.387Z",
  "updated_at": "2026-10-20T22:31:48.387Z"
}
```

The POST response supplies `case_id`; no list call is needed to discover the new case.

## 2. Enter review

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/patch-cases-by-id.md).

```python theme={null}
case = call("PATCH", f"/v2/cases/{case_id}", json={"status": "in_review"})
assert case["status"] == "in_review"
```

Recorded response (HTTP 200), abbreviated where fields are omitted:

```json theme={null}
{
  "id": "52f32bff-01f7-4ab2-91a6-f2a5273c30b7",
  "status": "in_review",
  "resolution": null,
  "title": "docs-test-b5684b40251e420fb73b8583d43b7d24-case",
  "viewed": false
}
```

## 3. Record your review comment

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/post-cases-by-id-comments.md).

```python theme={null}
comment = call("POST", f"/v2/cases/{case_id}/comments",
               json={"body": f"docs-test-{run_id}-comment"})
assert comment["body"] == f"docs-test-{run_id}-comment"
```

Recorded response (HTTP 201), abbreviated where fields are omitted:

```json theme={null}
{
  "id": "42a00fbb-2f9c-4d78-8c9c-af1e54930c7b",
  "author": "system",
  "body": "docs-test-b5684b40251e420fb73b8583d43b7d24-comment",
  "created_at": "2026-10-20T22:31:48.627Z"
}
```

## 4. Optionally save an investigation

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/post-investigations.md).

```python theme={null}
investigation = call("POST", "/v2/investigations",
                     json={"title": f"docs-test-{run_id}-investigation"})
investigation_id = investigation["id"]
```

Recorded response (HTTP 201), abbreviated where fields are omitted:

```json theme={null}
{
  "id": "c8740654-04f8-477e-be7d-ecb133d9892b",
  "title": "docs-test-b5684b40251e420fb73b8583d43b7d24-investigation",
  "status": "open",
  "data": {}
}
```

For tracing, follow the [investigation playbook](https://docs.range.org/ai/investigation-playbook.md). This step saves an empty investigation for the synthetic linking example.

## 5. Link the optional investigation

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/post-cases-by-id-investigations.md).

```python theme={null}
call("POST", f"/v2/cases/{case_id}/investigations",
     json={"investigation_id": investigation_id})
```

Recorded response: HTTP 204, no response body.

## 6. Verify the optional link

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/get-cases-by-id-investigations.md).

```python theme={null}
linked = call("GET", f"/v2/cases/{case_id}/investigations")
assert any(row["id"] == investigation_id for row in linked["items"])
```

Recorded response (HTTP 200), abbreviated where fields are omitted:

```json theme={null}
{
  "items": [
    {
      "id": "c8740654-04f8-477e-be7d-ecb133d9892b",
      "title": "docs-test-b5684b40251e420fb73b8583d43b7d24-investigation",
      "status": "open"
    }
  ]
}
```

Skip steps 4 through 6 together if no investigation is needed.

## 7. Dismiss the case

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/patch-cases-bulk.md).

```python theme={null}
result = call("PATCH", "/v2/cases/bulk",
              json={"case_ids": [case_id], "resolution": "dismissed"})
assert result == {"updated": 1, "skipped": 0}
```

Recorded response (HTTP 200), abbreviated where fields are omitted:

```json theme={null}
{
  "updated": 1,
  "skipped": 0
}
```

Check both counters. For the single manual case created here, success is exactly `updated: 1` and `skipped: 0`.

## 8. Verify the final case state

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/get-cases-by-id.md).

```python theme={null}
case = call("GET", f"/v2/cases/{case_id}")
assert case["status"] == "resolved"
assert case["resolution"] == "dismissed"
```

Recorded response (HTTP 200), abbreviated where fields are omitted:

```json theme={null}
{
  "id": "52f32bff-01f7-4ab2-91a6-f2a5273c30b7",
  "status": "resolved",
  "resolution": "dismissed",
  "viewed": false,
  "updated_at": "2026-10-20T22:31:48.973Z"
}
```

The final state is `status: resolved` and `resolution: dismissed`. In the API-key verification run, two consecutive detail GETs returned identical responses with `viewed: false` and an unchanged `updated_at`. The handler records a view only when a session viewer email is present; `PatchCaseDto.read` requires a session. This procedure does not set `read`.

## 9. Read the timeline last

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/get-cases-by-id-timeline.md).

```python theme={null}
timeline = call("GET", f"/v2/cases/{case_id}/timeline")
assert any(event["action"] == "status_changed"
           and event["details"].get("new_status") == "resolved"
           and event["details"].get("resolution") == "dismissed"
           for event in timeline["items"])
print(timeline)
```

Recorded response (HTTP 200), abbreviated where fields are omitted:

```json theme={null}
{
  "items": [
    {
      "id": "9fc2707a-68c8-4245-a181-eee7d61b1022",
      "action": "created",
      "actor": "system",
      "details": {
        "severity": "low",
        "source_type": "manual"
      },
      "created_at": "2026-10-20T22:31:48.387Z"
    },
    {
      "id": "a8ef5667-7db2-4828-934e-a6617100ea38",
      "action": "status_changed",
      "actor": "system",
      "details": {
        "new_status": "in_review",
        "old_status": "open",
        "resolution": null
      },
      "created_at": "2026-10-20T22:31:48.555Z"
    },
    {
      "id": "cc316d6b-b8d3-4745-8c4f-fb3ffcc0fb3f",
      "action": "commented",
      "actor": "system",
      "details": {
        "comment_id": "42a00fbb-2f9c-4d78-8c9c-af1e54930c7b"
      },
      "created_at": "2026-10-20T22:31:48.629Z"
    },
    {
      "id": "fcb2495b-0cd8-473d-a2d5-32d040b2501f",
      "action": "investigation_linked",
      "actor": "system",
      "details": {
        "investigation_id": "c8740654-04f8-477e-be7d-ecb133d9892b"
      },
      "created_at": "2026-10-20T22:31:48.851Z"
    },
    {
      "id": "f4fc5b72-2521-4ea8-b883-4e1f62d523ad",
      "action": "status_changed",
      "actor": "system",
      "details": {
        "via": "bulk_dismiss",
        "new_status": "resolved",
        "old_status": null,
        "resolution": "dismissed"
      },
      "created_at": "2026-10-20T22:31:48.972Z"
    }
  ]
}
```

The resolved status-change entry confirms dismissal is included in the audit trail.

## 10. Clean up the optional investigation

If you ran steps 4 through 6, delete only the investigation you created in step 4 with `DELETE /v2/investigations/{id}`, using the returned `investigation_id`. Skip this deletion if you skipped those steps.

[Endpoint reference](https://docs.range.org/platform-api/reference/v2/delete-investigations-by-id.md).

```python theme={null}
call("DELETE", f"/v2/investigations/{investigation_id}")
```

Recorded response: HTTP 204, no response body.

The case remains as a dismissed audit record with `status: resolved` and `resolution: dismissed`. This API-key procedure ends with dismissal and does not delete the case. After cleanup, close the client:

```python theme={null}
api.close()
```

## Complete runnable script

Save as `triage_manual_case.py`, set `RANGE_API_KEY`, and run `python triage_manual_case.py`. Add `--with-investigation` to exercise the optional branch. The script keeps the case as a dismissed audit record and deletes only the investigation it created. Its failure cleanup dismisses its own case before closing the client.

```python theme={null}
import os
import uuid
import httpx

run_id = uuid.uuid4().hex
title = f"docs-test-{run_id}-case"
api = httpx.Client(
    base_url="https://api.range.org",
    headers={"Authorization": f"Bearer {os.environ['RANGE_API_KEY']}",
             "User-Agent": "curl/8.4.0"},
    timeout=30,
)

def call(method, path, **kwargs):
    response = api.request(method, path, **kwargs)
    response.raise_for_status()
    return response.json() if response.content else None

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--with-investigation", action="store_true")
args = parser.parse_args()
case_id = investigation_id = None
dismissed = False
try:
    case = call("POST", "/v2/cases", json={
        "source_type": "manual", "title": title, "severity": "low",
        "subject": {"type": "counterparty", "name": title},
    })
    case_id = case["id"]
    assert case["status"] == "open"
    case = call("PATCH", f"/v2/cases/{case_id}", json={"status": "in_review"})
    assert case["status"] == "in_review"
    comment = call("POST", f"/v2/cases/{case_id}/comments",
                   json={"body": f"docs-test-{run_id}-comment"})
    assert comment["body"] == f"docs-test-{run_id}-comment"
    if args.with_investigation:
        investigation = call("POST", "/v2/investigations",
                             json={"title": f"docs-test-{run_id}-investigation"})
        investigation_id = investigation["id"]
        call("POST", f"/v2/cases/{case_id}/investigations",
             json={"investigation_id": investigation_id})
        linked = call("GET", f"/v2/cases/{case_id}/investigations")
        assert any(row["id"] == investigation_id for row in linked["items"])
    result = call("PATCH", "/v2/cases/bulk",
                  json={"case_ids": [case_id], "resolution": "dismissed"})
    assert result == {"updated": 1, "skipped": 0}
    dismissed = True
    case = call("GET", f"/v2/cases/{case_id}")
    assert case["status"] == "resolved"
    assert case["resolution"] == "dismissed"
    timeline = call("GET", f"/v2/cases/{case_id}/timeline")
    assert any(event["action"] == "status_changed"
               and event["details"].get("new_status") == "resolved"
               and event["details"].get("resolution") == "dismissed"
               for event in timeline["items"])
    print(timeline)
finally:
    try:
        if case_id and not dismissed:
            cleanup = call("PATCH", "/v2/cases/bulk",
                           json={"case_ids": [case_id], "resolution": "dismissed"})
            final = call("GET", f"/v2/cases/{case_id}")
            assert final["status"] == "resolved" and final["resolution"] == "dismissed"
    finally:
        try:
            if investigation_id:
                call("DELETE", f"/v2/investigations/{investigation_id}")
        finally:
            api.close()
```

Cleanup reference: [delete an investigation](https://docs.range.org/platform-api/reference/v2/delete-investigations-by-id.md). For request failures, use [Errors and rate limits](https://docs.range.org/resources/errors-and-rate-limits.md).


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