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

# Webhooks

> Receive signed events instead of polling.

Register an HTTPS endpoint and we'll POST you events as they happen:

| Event                    | When                                                     |
| ------------------------ | -------------------------------------------------------- |
| `extraction.completed`   | Structured results are ready                             |
| `extraction.failed`      | Processing failed — the charge is refunded automatically |
| `underwriting.completed` | An underwriting run finished                             |
| `underwriting.failed`    | An underwriting run failed                               |

```bash theme={null}
curl -X POST https://api.trycactus.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $CACTUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/cactus/webhooks", "events": ["extraction.completed", "extraction.failed"]}'
```

The response includes the signing `secret` (prefix `whsec_`) **once** —
store it securely.

## Verifying signatures

Every delivery includes a `Cactus-Signature` header:

```
Cactus-Signature: t=1720000000,v1=5257a869e7...
```

`v1` is an HMAC-SHA256 digest of `"{t}.{raw request body}"` using your
endpoint secret. Verify every delivery:

```python theme={null}
import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, provided = int(parts["t"]), parts["v1"]
    if abs(time.time() - ts) > tolerance:
        return False  # too old — possible replay
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, provided)
```

Reject deliveries older than five minutes, and always compute the digest
over the **raw** request body — parsing and re-serializing JSON will change
the bytes.

## Delivery and retries

Respond with any `2xx` within 10 seconds to acknowledge. Anything else is
retried with exponential backoff for up to 24 hours. Deliveries can arrive
more than once — use the event `id` to deduplicate.
