> For the complete documentation index, see [llms.txt](https://docs-sdk.crypto-chief.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-sdk.crypto-chief.com/processing/python/guides/webhooks.md).

# Webhooks

Verify and handle Crypto Chief webhooks in Python with typed event payloads.

Outbound webhooks are signed with the **same algorithm** as outgoing requests, so the same API key verifies them. The verification re-canonicalizes the body, so it tolerates key-order drift — but you must pass the **raw** request body, not a re-serialized object.

## FastAPI

Read the raw bytes off the request, then verify and parse in one step. `parse_webhook_event` returns a typed event picked by the event-name prefix:

```python
from fastapi import FastAPI, Request, HTTPException
from cryptochief import (
    parse_webhook_event, WebhookSignatureError, PayInWebhookEvent, PayoutWebhookEvent,
)

app = FastAPI()
API_KEY = "..."

@app.post("/webhook")
async def webhook(request: Request):
    raw = await request.body()  # the EXACT bytes — do not re-encode
    try:
        evt = parse_webhook_event(API_KEY, raw, request.headers.get("Signature"))
    except WebhookSignatureError:
        raise HTTPException(status_code=401, detail="bad signature")

    if isinstance(evt, PayInWebhookEvent) and evt.status == "paid":
        ...  # invoice.paid — fulfill the order for evt.order_id
    elif isinstance(evt, PayoutWebhookEvent):
        ...  # payout.paid / payout.system_fail — reconcile your ledger
    return {"ok": True}
```

## Plain stdlib server

The verification helpers do no I/O, so a plain `http.server` works too:

```python
from http.server import BaseHTTPRequestHandler
from cryptochief import parse_webhook_event, WebhookSignatureError, WEBHOOK_HEADER

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("Content-Length", "0")))
        try:
            evt = parse_webhook_event(API_KEY, raw, self.headers.get(WEBHOOK_HEADER))
        except WebhookSignatureError:
            self.send_response(401); self.end_headers(); return
        print(evt.event)  # e.g. "payout.paid"
        self.send_response(200); self.end_headers()
```

## Manual verification

For any other stack, verify the raw bytes yourself (constant-time, returns a boolean):

```python
from cryptochief import verify_webhook_signature

if not verify_webhook_signature(API_KEY, raw_body, signature_header):
    ...  # reject with 401
```

## Event types

Typed payloads: `PayoutWebhookEvent`, `TransactionWebhookEvent`, `PayInWebhookEvent`, `StaticDepositWebhookEvent`, `SweepWebhookEvent`. Event-name prefixes are `payout.*`, `transaction.*`, `invoice.*` (pay-ins), `static_deposit.*`, and `sweep.*`. `parse_webhook_event` returns the matching dataclass, or the raw `dict` for an unknown prefix. Payout and transaction webhooks fire **only on terminal status**.

`sweep.confirmed` is the moment swept funds are confirmed in your master wallet — the other half of a deposit's life, and what treasury reporting should key off. See [Sweep callbacks](/processing/python/guides/sweep-callbacks.md).

{% hint style="warning" %}
**"No value" is not spelled the same way in every payload.** A static-deposit webhook reports a native-coin transfer as `contract=""`, not `None` — the opposite of the wallet endpoints, where an unset value is always `null` — so test it with `if not evt.contract`. `confirmed_at` and `paid_at` stay `None` until the deposit is confirmed and paid, and `amount_fiat` can come through empty when no rate was available. A sweep webhook goes the other way again: an optional field it has no value for is **omitted**, so a native sweep's `asset_contract` reads `None`.
{% endhint %}

{% hint style="info" %}
`WEBHOOK_SENDER_IPS` lists the addresses webhooks are delivered from — whitelist them at your edge for defence in depth.
{% endhint %}
