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

# Webhooks

> Receive real-time event notifications via HMAC-SHA256 signed HTTP POST deliveries.

## Overview

Webhooks let your server receive notifications when asynchronous events
complete - for example, when a generation job finishes. Every delivery is
signed so you can verify it originated from the platform.

Webhooks are **project-scoped**: a subscription belongs to one project,
and every endpoint URL is re-validated against an SSRF blocklist on each
delivery attempt.

## Registering an endpoint

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.kitefrost.io/v1/projects/$PROJECT_ID/webhooks \
    -H "Authorization: Bearer sk_live_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://yourapp.com/webhooks/kitefrost",
      "events": ["generation.completed", "generation.failed"],
      "description": "Production server"
    }'
  ```

  ```json Response theme={null}
  {
    "id": "wh_01HZ...",
    "url": "https://yourapp.com/webhooks/kitefrost",
    "events": ["generation.completed", "generation.failed"],
    "description": "Production server",
    "secret": "whsec_abcdef1234567890...",
    "active": true,
    "created_at": "2026-05-19T10:00:00Z"
  }
  ```
</CodeGroup>

Store the `secret` value - it is shown only once and is used to verify
signatures. Lost it? Use the rotate-secret endpoint to get a new one.

## Verifying signatures

Each delivery includes three headers:

| Header                | Description                                  |
| --------------------- | -------------------------------------------- |
| `X-Webhook-Signature` | `sha256=<hex>` HMAC-SHA256 digest            |
| `X-Webhook-Timestamp` | Unix epoch (seconds) of the delivery attempt |
| `X-Webhook-Id`        | Unique event ID (use this for idempotency)   |

The signed payload is `{timestamp}.{raw_request_body}`. Reject events
older than 5 minutes to defend against replay.

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

  def verify_webhook(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
      if abs(time.time() - int(timestamp)) > 300:
          return False
      signed_payload = f"{timestamp}.".encode() + body
      expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
      return hmac.compare_digest(f"sha256={expected}", signature)
  ```

  ```typescript TypeScript theme={null}
  import crypto from "crypto";

  function verifyWebhook(secret: string, timestamp: string, body: Buffer, signature: string): boolean {
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
    const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`), body]);
    const expected = "sha256=" + crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  }
  ```
</CodeGroup>

## Event catalog

| Event                        | Fired when                                                    |
| ---------------------------- | ------------------------------------------------------------- |
| `generation.completed`       | A generation job produced output successfully                 |
| `generation.failed`          | A generation job failed after all retries                     |
| `generation.requires_review` | Human-in-the-loop review is needed                            |
| `export.ready`               | An export file is available for download                      |
| `player.event.processed`     | A player event was ingested and knowledge graph updated       |
| `entity.created`             | A new entity was created in the project                       |
| `entity.updated`             | An existing entity was updated                                |
| `entity.deleted`             | An entity was deleted                                         |
| `session.started`            | A runtime session was started                                 |
| `session.ended`              | A runtime session ended                                       |
| `ingest.completed`           | A story-ingest pipeline completed successfully (all 5 stages) |
| `ingest.failed`              | A story-ingest pipeline aborted with an unrecoverable error   |

## Delivery and retries

* Deliveries time out after **10 seconds**.
* Failed deliveries (non-2xx, timeout, or connection error) are retried
  with exponential backoff: **5s, 30s, 2m, 15m, 1h, 4h** (7 attempts
  including the initial one; \~5h 18m total window).
* After **3 consecutive failed events** on the same endpoint, the
  endpoint is auto-disabled and a notification is sent to the project
  owner. Re-enable it by `PATCH`ing `{ "active": true }`.

## Managing endpoints

```
GET    /v1/projects/{id}/webhooks
POST   /v1/projects/{id}/webhooks
PATCH  /v1/projects/{id}/webhooks/{wh_id}
DELETE /v1/projects/{id}/webhooks/{wh_id}

POST   /v1/projects/{id}/webhooks/{wh_id}/test            # send a test event
GET    /v1/projects/{id}/webhooks/{wh_id}/deliveries      # delivery history
POST   /v1/projects/{id}/webhooks/{wh_id}/rotate-secret   # mint a new signing secret
```
