---
title: Webhooks
description: Subscribe to execution and adverse-action events, verify the HMAC signature, handle retries idempotently, and rotate secrets without downtime.
canonical_url: "https://cascades.crscreditapi.com/docs/concepts/webhooks"
doc_version: "1.0.0"
last_updated: "2026-10-05T19:24:37Z"
source: docs/concepts/webhooks.md
---

# Webhooks

Cascades runs asynchronously, so the reliable way to learn that a run finished, failed, or produced an adverse-action notice is to be told. Subscribe an HTTPS endpoint once and every matching event is delivered as a signed `POST`.

## Which events exist?

| Event | Fires when |
|---|---|
| [`execution.completed`](/docs/api/webhooks/execution.completed) | An execution reached `completed`. Once per execution, including batch rows |
| [`execution.failed`](/docs/api/webhooks/execution.failed) | An execution reached `failed`; the payload adds `first_error_message`. Not for cancellations |
| [`adverse_action.triggered`](/docs/api/webhooks/adverse_action.triggered) | Adverse-action handling began |
| [`adverse_action.completed`](/docs/api/webhooks/adverse_action.completed) | Adverse-action obligations were fulfilled |
| [`adverse_action.failed`](/docs/api/webhooks/adverse_action.failed) | A notice email failed to deliver |
| [`adverse_action.retry_scheduled`](/docs/api/webhooks/adverse_action.retry_scheduled) | A post-adverse notice retry was scheduled |

Events are emitted for **API-triggered** executions only (runs started from the Cascades UI have no API client and produce no webhooks). Test-mode runs emit them too; `execution.*` payloads carry `execution_mode: "test"` (adverse-action payloads do not — look the execution up by `workflow_execution_id` and read `input_context.execution_mode`).

## How do I subscribe?

```bash
curl -X POST "https://cascades.crscreditapi.com/api/v1/webhook_subscriptions" \
  -H "Authorization: Bearer {{API_KEY}}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.partner.example/cascades",
    "event_filter": ["execution.completed", "execution.failed"]
  }'
```

The `201` response includes `signing_secret` **exactly once**; store it. An empty `event_filter` subscribes to every event; unknown event names are `422 validation_failed`. The URL must be `http` or `https`, public (no private, loopback, link-local, metadata or `localhost` addresses) and free of userinfo. Needs `webhooks:write`.

`PATCH /webhook_subscriptions/{id}` changes the URL, filter or `active` flag; `DELETE` removes it; `GET` never re-exposes the secret.

## What does a delivery look like?

```http
POST /cascades HTTP/1.1
Host: hooks.partner.example
Content-Type: application/json
User-Agent: Cascades-Webhook/1.0
X-Cascades-Event: execution.completed
X-Cascades-Delivery: 9081
X-Cascades-Signature: t=1715191234,v1=4f1b2…

{
  "id": "6d1f1a5e-4a2b-4c7d-9e8f-0a1b2c3d4e5f",
  "event": "execution.completed",
  "created_at": "2026-09-09T14:03:21Z",
  "data": {
    "workflow_execution_id": 1842,
    "workflow_id": 27,
    "batch_execution_id": null,
    "external_reference": "rv-app-20491",
    "execution_mode": "test",
    "status": "completed",
    "review_status": "approved",
    "completed_at": "2026-09-09T14:03:21Z"
  }
}
```

`id` is the envelope id (the same across retries of one event); `X-Cascades-Delivery` is the delivery row id — your idempotency key. Branch on `X-Cascades-Event`.

## How do I verify the signature?

`X-Cascades-Signature` is `t=<unix timestamp>,v1=<hex HMAC-SHA256>` where the HMAC is computed over `"<t>.<raw request body>"` with your `signing_secret`. Verify against the **raw** bytes — any reformatting changes the digest — compare in constant time, and reject timestamps outside your tolerance (60 seconds is a reasonable cap). Drop-in verifiers for Ruby, Python and Node are in [Verify webhook signatures](/docs/guides/verify-webhooks).

### Rotating the secret

`POST /webhook_subscriptions/{id}/rotate_secret` mints a new secret and returns it once. For the next **24 hours** every delivery is signed with both: `v1=` with the new secret and `v0=` with the old one. A verifier that accepts a match on *any* `v?=` value handles rotation with no deploy coordination; after the grace window only `v1=` ships.

## What if my endpoint is down?

Any non-2xx response, connection error or timeout (5 s connect, 10 s read) is a failure. Cascades retries with backoff:

```text
attempt 1 → +1m → 2 → +5m → 3 → +30m → 4 → +2h → 5 → +8h → 6 → +24h → dead
```

After six attempts the delivery is `dead`. `GET /webhook_deliveries` shows every delivery with `status` (`pending`, `retrying`, `succeeded`, `failed`, `dead`), `attempts`, `last_response_status` and `next_retry_at` — filter by subscription, event type or status. `POST /webhook_deliveries/{id}/retry` re-enqueues a `failed` or `dead` row immediately; an already-succeeded one is `409 delivery_not_retryable`. The delivery payload itself is not exposed through the API; re-read the execution instead.

Your endpoint should return `2xx` as soon as the event is durably stored, do the real work on a queue, be idempotent on `X-Cascades-Delivery`, and answer within 10 seconds.

## Related

- [Webhook events reference](/docs/api/webhooks) — payload schema per event
- [Verify webhook signatures](/docs/guides/verify-webhooks)
- [Executions](/docs/concepts/executions) and [Adverse action](/docs/concepts/adverse-action)

## Sitemap

See the full [sitemap](https://cascades.crscreditapi.com/sitemap.md) for all pages.
