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

# Manage cases via REST API

> Open, read, and update cases for a direct case workflow programmatically

A **direct case workflow** can have cases created from outside Champ with a single HTTP request — from your own backend, a form handler, or another tool. Each case you create kicks off the workflow's automation against the data you submit.

The flow is:

1. Create an **API token** in the dashboard.
2. `POST` a case to your workflow's cases endpoint with its `attributes`.
3. Fetch the case to read its current status and attributes.
4. Optionally, update the case's status — for example to close it from your own system.

<Note>
  This endpoint only works for workflows whose case source is **direct**. Ticketing and email workflows take in cases through their own channels and can't be written to with this API.
</Note>

## Create an API token

1. Go to [**Tokens**](https://dash.champ.ai/app/tokens) in the dashboard.
2. Click **Create Token**, choose the **API** scope, and click **Create**.
3. Copy the token immediately — it's shown **once** and cannot be retrieved again. API tokens look like `api_xxxxxxxx…`.

The token is scoped to your organization. Treat it as a secret: store it in an environment variable or secrets manager, and delete it from the Tokens page if it's ever exposed.

## Find your workflow ID

Open the direct case workflow in the dashboard. The numeric ID is the last segment of its URL (`https://dash.champ.ai/app/workflows/<WORKFLOW_ID>`). You'll use it in the request path below.

## Create a case

Send a `POST` request to the workflow's cases endpoint:

```bash theme={null}
curl -X POST https://api.champ.ai/api/external/v1/workflows/{WORKFLOW_ID}/cases \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Refund request", "attributes": {"amount": 42, "reason": "late delivery"}}'
```

<ParamField path="WORKFLOW_ID" type="integer" required>
  Path parameter. The numeric ID of a **direct case** workflow.
</ParamField>

<ParamField path="Authorization" type="string" required>
  Request header. `Bearer <YOUR_API_TOKEN>` — an API-scoped token from the Tokens page.
</ParamField>

<ParamField path="subject" type="string">
  Request body. A human-readable title for the case. If you omit it or leave it blank, Champ stamps a timestamped
  default like `Refund Intake via API - 2026-05-28 17:04:12 UTC`.
</ParamField>

<ParamField path="attributes" type="object">
  Request body. The case's field values, validated against the workflow's attribute schema. Any attribute that is **required** in the workflow must be supplied here. Omit it (or pass `{}`) for workflows with no required attributes.
</ParamField>

<ParamField path="mode" type="string" default="live">
  Request body. `"live"` (default) or `"test"`. Test cases run the workflow end to end but don't count as real work — they're excluded from webhooks, billing, metrics, and digests, and their runs execute in test mode. Point your staging environment here.
</ParamField>

<Note>
  The body accepts only `subject`, `attributes`, and `mode`. Any other top-level key is rejected with a `422` before the case is created.
</Note>

The case is created immediately and the workflow's automation starts asynchronously. The response is the new case:

```json theme={null}
{
  "case": {
    "id": 987,
    "workflow_id": 123,
    "subject": "Refund request",
    "status": "Open",
    "outcome": null,
    "mode": "live",
    "attributes": { "amount": 42, "reason": "late delivery" },
    "created_at": "2026-05-28T17:04:12Z",
    "updated_at": "2026-05-28T17:04:12Z"
  }
}
```

| Field         | Description                                                                 |
| ------------- | --------------------------------------------------------------------------- |
| `id`          | Unique case ID. Use it to fetch the case.                                   |
| `workflow_id` | The workflow this case belongs to.                                          |
| `subject`     | The case title.                                                             |
| `status`      | Case status. New cases start as `Open`; the workflow updates it as it runs. |
| `outcome`     | Disposition of a `Closed` case (e.g. `Resolved`); `null` otherwise.         |
| `mode`        | `"live"` or `"test"`. See [Test cases](#test-cases).                        |
| `attributes`  | The case's current field values.                                            |

## Fetch a case

Read a case's current status and attributes by ID:

```bash theme={null}
curl https://api.champ.ai/api/external/v1/workflows/{WORKFLOW_ID}/cases/{CASE_ID} \
  -H "Authorization: Bearer <YOUR_API_TOKEN>"
```

The response is the same `case` shape as above. As the workflow runs, the `status` and `attributes` reflect the latest state.

<Tip>
  Cases normally evolve through the workflow's own automation. To watch a case progress, poll this endpoint or [set up a webhook subscription](/webhooks/subscriptions).
</Tip>

## Update a case's status

Move a case to a new status yourself — for example to close it when it's resolved on your side, or to reopen it:

```bash theme={null}
curl -X PATCH https://api.champ.ai/api/external/v1/workflows/{WORKFLOW_ID}/cases/{CASE_ID} \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"status": "Closed"}'
```

<ParamField path="status" type="string" required>
  Request body. The status to move the case to. Must be one of the statuses declared on the workflow (see [Case statuses](#case-statuses)). `Error` is engine-managed and can't be set through the API.
</ParamField>

<Warning>
  This is more than writing a field: the update is recorded on the case's timeline and **starts the workflow run
  attached to the new status**, which counts toward your usage like any other run. Use it when you intend the case to
  actually move, not to sync statuses.
</Warning>

The response is the updated `case` in the same shape as above.

<Note>
  The body accepts only `status`. Any other top-level key is rejected with a `422`.
</Note>

## Test cases

Create a case with `"mode": "test"` to exercise a workflow without creating real work:

```bash theme={null}
curl -X POST https://api.champ.ai/api/external/v1/workflows/{WORKFLOW_ID}/cases \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Staging smoke test", "attributes": {"amount": 1}, "mode": "test"}'
```

Test cases behave like live ones — the workflow runs end to end, statuses transition, attributes update — but they are excluded from webhooks, billing, metrics, and reporting digests, and their runs are marked test mode. This makes them safe to fire from a staging environment or CI check.

<Note>
  Test cases still execute the workflow's real steps. If your workflow calls external systems or sends messages, design it to branch on the case's `mode` where those actions shouldn't run for test traffic.
</Note>

## Case statuses

Every case carries a `status`. A new case always starts as **`Open`**, and the workflow's automation moves it
through the rest as it runs. You can also move it yourself with the [update endpoint](#update-a-cases-status).

Each workflow ships with three **default statuses** that are always present:

| Status   | Meaning                                                                  |
| -------- | ------------------------------------------------------------------------ |
| `Open`   | The case is live and the workflow is working it. Every case starts here. |
| `Closed` | The case is resolved and done. The workflow has finished with it.        |
| `Error`  | The workflow hit a problem it couldn't resolve and parked the case.      |

A workflow can also define **custom statuses** on top of these defaults (for example `Awaiting customer` or
`Pending review`) to model the intermediate stages of its own process. A case's `status` is always one of the
statuses declared on its workflow, so the exact set you'll see depends on how that workflow is configured — open the
workflow in the dashboard to view its full status list.

<Note>
  A case can only ever be in a status its workflow has defined. `Error` is set by the engine when a run fails and
  can't be set through the API — recover an errored case by moving it to another status.
</Note>

## Validation errors

If `attributes` doesn't match the workflow's schema — a missing required field, a wrong type — the request returns `422` with a `validation_errors` array. Each entry points to the offending field with a [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901):

```json theme={null}
{
  "error": "attributes_validation_failed",
  "validation_errors": [
    { "path": "/amount", "error": "value at `/amount` is not a number" }
  ]
}
```

## Errors

| Status | When                                                                                                                                                                                                      |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing or invalid `Authorization` header.                                                                                                                                                                |
| `404`  | Workflow or case not found, or it belongs to another organization.                                                                                                                                        |
| `422`  | The workflow isn't a direct case workflow, an unsupported body key was sent, `attributes` wasn't an object, `attributes` failed validation, or a status update sent a status the workflow doesn't define. |

Errors return a JSON body with an `error` message.
