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

# Webhooks

> Signed callbacks when a session finishes — and how to verify them.

Register an HTTPS endpoint and Hooper POSTs an **event** to it when a session finishes. The event carries the full session object, so a single delivery gives you the highlights, shots and box score.

## Events

| `type`              | When                             | `data.object`                                                            |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------ |
| `session.processed` | Processing finished successfully | The session, with `highlights` / `shots` / `players` populated           |
| `session.failed`    | Processing failed (or timed out) | The session with `status: "failed"` and `error` set; nothing was charged |

```json theme={null}
{
  "id": "evt_91", "object": "event", "type": "session.processed", "created": 1756224000,
  "data": { "object": { "id": "sess_18234", "object": "session", "status": "processed", "…": "…" } }
}
```

## Registering an endpoint

```bash theme={null}
curl https://api.hooper.gg/v1/webhook_endpoints \
  -H "Authorization: Bearer $HOOPER_API_KEY" -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/hooper", "enabled_events": ["*"] }'
```

The response includes `"secret": "hws_prod_…"` — **shown once**, so store it now. Send a synthetic event to check your handler with `POST /v1/webhook_endpoints/we_2/test`.

## Verifying signatures

Every delivery carries:

```
Hooper-Signature: t=1756224000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e146c8a0…
Hooper-Event-Id: evt_91
```

`v1` is `HMAC-SHA256(secret, "{t}.{raw_body}")`. This is the same scheme Stripe uses, so an existing Stripe verifier works with the secret swapped. Verify before you parse, using the **raw** request body, and reject timestamps older than 5 minutes.

<CodeGroup>
  ```python Python theme={null}
  import hmac, hashlib, time

  def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      ts, sig = int(parts["t"]), parts["v1"]
      expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, sig) and abs(time.time() - ts) <= tolerance

  # FastAPI / Flask etc.
  if not verify(HOOPER_WEBHOOK_SECRET, request_body_bytes, request.headers["Hooper-Signature"]):
      return Response(status=400)
  ```

  ```javascript Node theme={null}
  import crypto from "node:crypto";

  export function verify(secret, rawBody, header, tolerance = 300) {
    const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
    const ts = Number(parts.t);
    const expected = crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
    const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
    return ok && Math.abs(Date.now() / 1000 - ts) <= tolerance;
  }
  // Express: use express.raw({ type: "application/json" }) so req.body is the raw bytes.
  ```

  ```python Python (with the stripe package) theme={null}
  import stripe
  # Same algorithm: Stripe's verifier accepts Hooper's header with Hooper's secret.
  stripe.WebhookSignature.verify_header(raw_body, sig_header, HOOPER_WEBHOOK_SECRET, tolerance=300)
  ```
</CodeGroup>

## Delivery semantics

* **Acknowledge with any 2xx.** Do it fast; process asynchronously.
* **At-least-once.** Retries happen on any non-2xx or timeout (10 s): after 1m, 5m, 30m, 2h, 6h, 12h, 24h — eight attempts over about two days. Dedupe on the event `id`.
* **Auto-disable.** An endpoint that has been failing for 7 straight days is disabled; re-create it when fixed.
* **Catch-up.** Missed something? `GET /v1/events?type=session.processed&created[gte]=…` lists every event, newest first.

## Endpoint requirements

Public `https://` URL, no credentials in the URL, resolving to a public address. Private, loopback and link-local destinations are rejected at registration.
