> ## 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.

# Webhook payloads & delivery

> Payload shape, retry behavior, and handling guidance

## Payload shape

Every webhook is an HTTP `POST` with `Content-Type: application/json`. The body shape depends on the event family — workflow run events carry `data.workflow_run`; case events carry `data.case`.

### Workflow run events

`workflow_run.completed` and `workflow_run.failed` carry the run that fired them:

```json theme={null}
{
  "event": "workflow_run.completed",
  "timestamp": "2026-04-22T10:30:45Z",
  "data": {
    "workflow_run": {
      "id": 12345,
      "workflow_id": 67890,
      "status": "completed",
      "input": { "...": "the input the run was started with" },
      "output": { "...": "the workflow's final output" },
      "created_at": "2026-04-22T10:25:00Z",
      "updated_at": "2026-04-22T10:30:45Z"
    }
  }
}
```

| Field                           | Description                                                                                   |
| ------------------------------- | --------------------------------------------------------------------------------------------- |
| `event`                         | One of `workflow_run.completed` or `workflow_run.failed`.                                     |
| `timestamp`                     | When the event was dispatched, in ISO 8601 UTC.                                               |
| `data.workflow_run.id`          | Unique ID of the workflow run. Use this for deduplication.                                    |
| `data.workflow_run.workflow_id` | The workflow that produced this run. Use to filter if you only care about specific workflows. |
| `data.workflow_run.status`      | `completed` or `failed`. Mirrors `event` but is convenient to switch on.                      |
| `data.workflow_run.input`       | The input payload the run was started with.                                                   |
| `data.workflow_run.output`      | The workflow's final output. For failed runs this may include error details.                  |
| `data.workflow_run.created_at`  | When the run started (ISO 8601 UTC).                                                          |
| `data.workflow_run.updated_at`  | When the run reached its terminal state (ISO 8601 UTC).                                       |

### Case events

`case.closed` and `case.error` carry the [case](/integrations/cases-api) whose status changed:

```json theme={null}
{
  "event": "case.closed",
  "timestamp": "2026-04-22T10:30:45Z",
  "data": {
    "case": {
      "id": 12345,
      "subject": "Acme Corp — renewal",
      "status": "closed",
      "workflow_id": 67890,
      "attributes": { "...": "the case's current attribute values" },
      "created_at": "2026-04-22T10:25:00Z",
      "updated_at": "2026-04-22T10:30:45Z"
    }
  }
}
```

| Field                   | Description                                                                          |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `event`                 | One of `case.closed` or `case.error`.                                                |
| `timestamp`             | When the event was dispatched, in ISO 8601 UTC.                                      |
| `data.case.id`          | Unique ID of the case. Use this for deduplication.                                   |
| `data.case.subject`     | The case's human-readable subject. Never contains sensitive data.                    |
| `data.case.status`      | `closed` or `error`. Mirrors `event` but is convenient to switch on.                 |
| `data.case.workflow_id` | The workflow that owns the case. Use to filter if you only care about specific ones. |
| `data.case.attributes`  | The case's current attribute values, as a flat `{ key: value }` object.              |
| `data.case.created_at`  | When the case was created (ISO 8601 UTC).                                            |
| `data.case.updated_at`  | When the case last changed (ISO 8601 UTC).                                           |

<Note>
  For workflows with restricted data handling, any case attribute marked sensitive is delivered as `"[redacted]"`;
  the `subject` is always sent in the clear. See [PHI & HIPAA](/enterprise/data-protection/phi-hipaa).
</Note>

## Authentication

Champ applies the authentication configured on the destination's REST API connection to every delivery:

* **Bearer** — `Authorization: Bearer <token>`
* **Basic** — HTTP Basic credentials
* **Custom headers** — applied as-is to every request

Configure this when you create the REST API connection used as the subscription's **Connection**. See [Add a REST API integration](/integrations/rest-api).

<Warning>
  Champ does not currently sign webhook payloads — there is no HMAC or `X-Signature` header. Use bearer-token or basic auth on the destination connection to verify that a request actually came from Champ, and restrict your handler to that credential.
</Warning>

## Retries

Failed deliveries retry automatically:

| Parameter       | Value                                    |
| --------------- | ---------------------------------------- |
| Max attempts    | 10                                       |
| Backoff         | Polynomially increasing between attempts |
| Success         | Any HTTP status code in `200`–`299`      |
| Failure         | Any non-2xx response, timeout, or error  |
| Connect timeout | 10 seconds                               |
| Read timeout    | 30 seconds                               |

After 10 failed attempts, the event is marked `failed` and no further attempts are made. You can see which events failed and inspect the last response on the Webhooks page; there is no manual retry button today.

## Handling events

### Respond quickly

Return a 2xx response as soon as possible. Don't do heavy work inside the request handler — enqueue the payload for background processing and acknowledge immediately. A slow handler risks hitting the 30-second read timeout and being retried as a failure.

### Be idempotent

Because Champ retries until it gets a 2xx, the same payload can arrive more than once. Deduplicate on the resource id plus `event` — `data.workflow_run.id` for run events, `data.case.id` for case events (a run could fire both `completed` and `failed` in rare recovery cases — treat them as distinct keys).

### Switch on event type

```javascript theme={null}
app.post("/webhooks/champ", async (req, res) => {
  const { event, data } = req.body;

  // Acknowledge fast; do the real work asynchronously.
  res.sendStatus(200);

  // Run events carry data.workflow_run; case events carry data.case.
  const resource = data.workflow_run ?? data.case;
  await queue.enqueue({ event, resource });
});
```

### Filter by workflow

Subscriptions are tenant-wide. If you only care about specific workflows, filter by the payload's `workflow_id` (`data.workflow_run.workflow_id` or `data.case.workflow_id`) inside your handler rather than trying to scope the subscription itself.
