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

# Webhooks

> Verify inbound Spine webhook signatures from the Python SDK.

`client.webhooks` verifies the HMAC signature on an inbound webhook and returns a typed `WebhookEvent`. See [Webhooks](/api-reference/concepts/webhooks) for the conceptual model and signing algorithm.

No network call is made — verification is local crypto against the `whsec_...` secret you saved in the developer portal.

## Verifying a delivery

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
import os
from spine import SpineClient, WebhookSignatureError

client = SpineClient()  # SPINE_API_KEY picked up from env

def handle_spine_webhook(raw_body: bytes, sig_header: str) -> dict:
    try:
        event = client.webhooks.construct_event(
            payload=raw_body,
            sig_header=sig_header,
            secret=os.environ["SPINE_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError:
        return {"ok": False}, 400

    if event.type == "run.completed":
        run = event.data["object"]
        # persist run["run_id"] etc.
    return {"ok": True}, 200
```

Pass the **raw request body bytes**, not the parsed JSON — frameworks may reorder keys during re-serialization and the signature will no longer verify.

| Argument     | Type           | Default  | Description                                       |
| ------------ | -------------- | -------- | ------------------------------------------------- |
| `payload`    | `bytes \| str` | required | Raw request body. `bytes` is preferred.           |
| `sig_header` | `str`          | required | Value of the `Spine-Signature` request header.    |
| `secret`     | `str`          | required | The `whsec_...` secret from the developer portal. |
| `tolerance`  | `int`          | `300`    | Replay window in seconds.                         |

Returns a [`WebhookEvent`](#webhookevent). Raises [`WebhookSignatureError`](/api-reference/sdks/python/errors) on any verification failure.

## FastAPI example

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
from fastapi import FastAPI, Header, HTTPException, Request
from spine import SpineClient, WebhookSignatureError

app = FastAPI()
client = SpineClient()

@app.post("/webhooks/spine")
async def spine_webhook(
    request: Request,
    spine_signature: str = Header(default=""),
):
    raw = await request.body()
    try:
        event = client.webhooks.construct_event(
            payload=raw,
            sig_header=spine_signature,
            secret=os.environ["SPINE_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError as exc:
        raise HTTPException(status_code=400, detail=str(exc))

    # Dedupe on event.id, then dispatch
    return {"ok": True}
```

## Flask example

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
from flask import Flask, request
from spine import SpineClient, WebhookSignatureError

app = Flask(__name__)
client = SpineClient()

@app.post("/webhooks/spine")
def spine_webhook():
    try:
        event = client.webhooks.construct_event(
            payload=request.get_data(),                      # raw bytes
            sig_header=request.headers.get("Spine-Signature", ""),
            secret=os.environ["SPINE_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError:
        return {"ok": False}, 400

    # Dedupe on event.id, then dispatch
    return {"ok": True}, 200
```

## Async

`AsyncSpineClient.webhooks.construct_event(...)` has the same signature. Verification is CPU-only, so the async variant exists for API-parity with the rest of the client rather than for performance.

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
from spine import AsyncSpineClient

async with AsyncSpineClient() as client:
    event = client.webhooks.construct_event(...)
```

## `WebhookEvent`

| Field         | Type          | Notes                                                       |
| ------------- | ------------- | ----------------------------------------------------------- |
| `id`          | `str`         | `evt_<uuid>`. Use for idempotency.                          |
| `type`        | `str`         | e.g. `"run.completed"`, `"run.failed"`, `"webhook.ping"`.   |
| `created`     | `int`         | Unix seconds.                                               |
| `livemode`    | `bool`        | `True` in production.                                       |
| `api_version` | `str \| None` | Envelope schema version.                                    |
| `data`        | `dict`        | Event payload. `data["object"]` carries the primary entity. |

## Notes

* Clock skew matters. If your server drifts more than 5 minutes from Spine's clock, deliveries will be rejected as stale. Run NTP.
* Rotation invalidates the previous secret immediately. Update your env var before rotating if you care about strict uptime.
* The SDK accepts multiple `v1=` values in one header, so a future dual-signing rotation strategy will be forward-compatible.
