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

# Webhooks

> Receive signed completion events on your backend and use polling only for fallback or recovery.

Webhooks are completion notifications. After source ingest and generation create, webhooks usually replace polling, but `GET /api/v1/generations/{job_id}` remains the recovery path.

## Webhook configuration

Each organization stores one signing secret. Jobs can use the default callback URL or override the destination per request.

| Setting          | Behavior                                                                                        |
| ---------------- | ----------------------------------------------------------------------------------------------- |
| Callback URL     | Set a default in the dashboard, or pass `webhook.url` on create/regenerate for one-off routing. |
| Signing secret   | One organization secret signs every delivery until rotated.                                     |
| Signature header | Deliveries include `x-katalo-signature`, generated with HMAC-SHA256 over the raw request body.  |

## Delivery payload

| Field             | Meaning                                                 |
| ----------------- | ------------------------------------------------------- |
| `event_type`      | `generation.completed` or `generation.failed`.          |
| `job_id`          | Public job id to correlate parallel runs.               |
| `source_asset_id` | Public source asset id for the request.                 |
| `reference`       | Your original external identifier, echoed back if sent. |
| `outputs`         | Signed image URLs for passing outputs.                  |
| `failure`         | Terminal failure details if the job failed.             |
| `metadata`        | Your original metadata echoed back in the payload.      |

## Verify signatures

Verify the signature against the raw request body before parsing or processing the payload.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import crypto from "node:crypto";

  function verifyKataloWebhook(rawBody: Buffer, signature: string, secret: string) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(rawBody)
      .digest("hex");

    return crypto.timingSafeEqual(
      Buffer.from(signature, "hex"),
      Buffer.from(expected, "hex"),
    );
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_katalo_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(signature, expected)
  ```
</CodeGroup>

## Minimal consumer

```typescript theme={null}
export async function POST(request: Request) {
  const rawBody = Buffer.from(await request.arrayBuffer());
  const signature = request.headers.get("x-katalo-signature");

  if (!signature || !verifyKataloWebhook(rawBody, signature, process.env.KATALO_WEBHOOK_SECRET!)) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(rawBody.toString("utf8"));

  if (event.event_type === "generation.completed") {
    await markJobSucceeded(event.job_id, event.outputs);
  }

  if (event.event_type === "generation.failed") {
    await markJobFailed(event.job_id, event.failure);
  }

  return Response.json({ ok: true });
}
```

## Recovery and reconciliation

Treat webhook delivery as at-least-once.

| Concern              | Guidance                                                                        |
| -------------------- | ------------------------------------------------------------------------------- |
| Deduplication        | Use `job_id` plus `event_type` as the primary dedupe key.                       |
| Internal correlation | Use `reference` and `metadata` to reconnect the event to your internal records. |
| Consumer downtime    | Re-read the job later with `GET /api/v1/generations/{job_id}`.                  |
| Expired URLs         | Call `GET /api/v1/generations/{job_id}` again to receive fresh signed URLs.     |

## Delivery rules

| Rule                     | Behavior                                                                                      |
| ------------------------ | --------------------------------------------------------------------------------------------- |
| Public destinations only | Localhost, private IPs, link-local targets, and other non-public destinations are rejected.   |
| Shared secret            | One organization secret is reused for every webhook signature until rotation.                 |
| Retries                  | Transient delivery failures are retried with bounded exponential backoff.                     |
| Allowlist                | If the organization defines a webhook hostname allowlist, delivery is restricted to that set. |
