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

# Webhooks

> Signed terminal-event delivery, Standard Webhooks verification, and the delivery ledger.

Register an HTTPS endpoint and Originalis POSTs a signed event when an
action reaches a terminal state — no polling, no open connection.

| Event                                    | Fires when                                     |
| ---------------------------------------- | ---------------------------------------------- |
| `deal.analysis.completed` / `.failed`    | A deal analysis finishes (or terminally fails) |
| `founder.analysis.completed` / `.failed` | A founder assessment finishes                  |
| `research.completed` / `.failed`         | A research run finishes                        |

## Register

```bash theme={null}
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://ops.examplefund.com/hooks/originalis"}' \
  "https://api.originalis.ai/api/v1/webhooks"
```

The response carries the signing secret (`whsec_...`) **once** — store it
immediately. Endpoints must be HTTPS on a public address.

## Payloads are thin — fetch the resource for truth

```json theme={null}
{
  "event": "deal.analysis.completed",
  "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
  "created_at": "2026-09-07T15:04:05Z",
  "data": {
    "entity_id": "8a9b0c1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d",
    "result_url": "/api/v1/deals/8a9b0c1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d"
  }
}
```

## Verify signatures

Deliveries follow the [Standard Webhooks](https://www.standardwebhooks.com)
scheme — off-the-shelf verifier libraries work. Each delivery carries
`webhook-id`, `webhook-timestamp`, and `webhook-signature`
(`v1,base64(HMAC-SHA256(secret, "{id}.{timestamp}.{body}"))`):

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

def verify(secret: str, headers: dict, body: bytes) -> bool:
    key = base64.b64decode(secret.removeprefix("whsec_"))
    message = (
        f"{headers['webhook-id']}.{headers['webhook-timestamp']}."
        + body.decode()
    )
    expected = "v1," + base64.b64encode(
        hmac.new(key, message.encode(), hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(expected, headers["webhook-signature"])
```

## Delivery semantics

At-least-once. Failed deliveries retry with exponential backoff — up to
11 attempts over roughly 30 minutes. A `4xx` from your receiver stops
retries immediately (except `408` and `429`, which retry like a `5xx`).

* Deduplicate on `webhook-id` — it is stable across retries.
* Answer with a `2xx` within 10 seconds; do slow work after
  acknowledging.
* Inspect recent deliveries at
  `GET /api/v1/webhooks/{webhook_id}/deliveries` — status, attempts,
  and your receiver's last response code.

<Warning>
  Known gap (documented, not silent): a run failed by the background
  stale-timeout sweep may not produce a webhook — the poll endpoints remain
  the source of truth.
</Warning>
