---
title: Webhook events
description: Every event Cascades delivers, the X-Cascades-Signature scheme, and the retry schedule.
canonical_url: "https://cascades.crscreditapi.com/docs/api/webhooks"
doc_version: "1.0.0"
last_updated: "2026-10-09T18:04:24Z"
---

# Webhook events

Every delivery is an HTTPS POST signed with `X-Cascades-Signature: t=<unix>,v1=<hmac-sha256 of "<t>.<raw body>">`. Retries: 1m, 5m, 30m, 2h, 8h, 24h, then `dead`.

Payload schemas are the `x-webhooks` entries of the OpenAPI document: https://cascades.crscreditapi.com/openapi.json · https://cascades.crscreditapi.com/openapi.yaml.

| Event | Summary |
|---|---|
| [`execution.completed`](https://cascades.crscreditapi.com/docs/api/webhooks/execution.completed.md) | An execution reached `completed` |
| [`execution.failed`](https://cascades.crscreditapi.com/docs/api/webhooks/execution.failed.md) | An execution reached `failed` |
| [`adverse_action.triggered`](https://cascades.crscreditapi.com/docs/api/webhooks/adverse_action.triggered.md) | An adverse-action notice was sent (or the pre-adverse waiting period started) |
| [`adverse_action.completed`](https://cascades.crscreditapi.com/docs/api/webhooks/adverse_action.completed.md) | The adverse-action flow finished |
| [`adverse_action.failed`](https://cascades.crscreditapi.com/docs/api/webhooks/adverse_action.failed.md) | A notice could not be delivered |
| [`adverse_action.retry_scheduled`](https://cascades.crscreditapi.com/docs/api/webhooks/adverse_action.retry_scheduled.md) | A post-adverse notice delivery failed and a retry was scheduled |

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

## Ruby

```ruby
require "openssl"

def verify_cascades_signature(header, raw_body, secret, tolerance: 60)
  parts = header.to_s.split(",").map { |p| p.split("=", 2) }.to_h
  ts = parts["t"].to_i
  return false if ts == 0
  return false if (Time.now.to_i - ts).abs > tolerance

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{ts}.#{raw_body}")
  parts.any? do |k, v|
    k.start_with?("v") && OpenSSL.secure_compare(expected, v)
  end
end
```

## Python

```python
import hmac
import hashlib
import time

def verify_cascades_signature(header, raw_body, secret, tolerance=60):
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts = int(parts.get("t", 0))
    if ts == 0 or abs(int(time.time()) - ts) > tolerance:
        return False

    signed = f"{ts}.{raw_body}".encode()
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return any(
        k.startswith("v") and hmac.compare_digest(expected, v)
        for k, v in parts.items()
    )
```

## Node.js

```javascript
const crypto = require("crypto");

function verifyCascadesSignature(header, rawBody, secret, tolerance = 60) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const ts = parseInt(parts.t, 10);
  if (!ts) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > tolerance) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");
  const expBuf = Buffer.from(expected, "hex");
  return Object.entries(parts).some(([k, v]) => {
    if (!k.startsWith("v")) return false;
    const sigBuf = Buffer.from(v, "hex");
    return sigBuf.length === expBuf.length && crypto.timingSafeEqual(expBuf, sigBuf);
  });
}
```

## Sitemap

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