API
Webhooks
Receive signed completion events on your backend and use polling only for fallback or recovery.
Lifecycle
Webhooks are completion notifications
After source ingest and generation create, webhooks usually replace polling, but `GET /generations/{job_id}` remains the recovery path.
| Mechanism | When to use it |
|---|---|
| Webhook | Default for production after the generation job has been created. Best when your backend can expose a public callback endpoint and you want to avoid polling loops. |
| Polling | Useful for local development, simple integrations, or as a fallback when webhook delivery fails. |
| GET after webhook | Use this to recover state, fetch fresh signed URLs, or confirm final status after consumer downtime. |
Mental model
A webhook tells your backend that a job reached a terminal state. It is not the authoritative record for the lifetime of the job, and it does not remove the need for a read endpoint.
Typical production flow
Ingest the source, create the job with a webhook URL, persist the returned job_id, verify the signed callback when it arrives, and call GET /generations/{job_id} only when you need repair, replay, or fresh signed URLs.
Setup
Webhook configuration
Each organization stores one signing secret. Jobs can use the default callback URL or override the destination per request.
Callback destination
Set a default callback URL in the dashboard if most jobs should go to the same consumer. Create and regenerate requests can still override the URL for one-off routing.
Signing
Every webhook delivery includes an x-katalo-signature header generated with HMAC-SHA256 over the raw request body.
Delivery
Delivery payload
Webhook payloads are small and correlation-friendly. They contain the public ids you need to map completion back to your own system.
| Field | Meaning |
|---|---|
| event_type | generation.completed or generation.failed. |
| job_id | The public job id to correlate parallel runs. |
| source_asset_id | The public source asset id for the request. |
| reference | Your original external identifier, echoed back if you sent one. |
| outputs | Signed image URLs for the passing outputs. |
| failure | Terminal failure details if the job failed. |
| metadata | Your original metadata echoed back in the payload. |
Verification
Verify the signature first
Verify the signature against the raw request body before parsing or processing the payload.
import crypto from "crypto";
function verifyWebhook(body: string, signature: string, secret: string) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}Implementation
Minimal consumer
A webhook consumer only needs to verify the signature, branch on `event_type`, and persist terminal state for the `job_id`.
import hashlib
import hmac
import os
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = os.environ["KATALO_WEBHOOK_SECRET"]
def verify_signature(raw_body: bytes, signature: str) -> bool:
expected = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/webhooks/katalo")
async def katalo_webhook(
request: Request,
x_katalo_signature: str = Header(alias="x-katalo-signature"),
):
raw_body = await request.body()
if not verify_signature(raw_body, x_katalo_signature):
raise HTTPException(status_code=401, detail="invalid signature")
event = await request.json()
job_id = event["job_id"]
event_type = event["event_type"]
if event_type == "generation.completed":
outputs = event["outputs"]
# Persist outputs or copy them into your own storage here.
elif event_type == "generation.failed":
failure = event["failure"]
# Mark the job as failed and record the failure code here.
return {"ok": True, "job_id": job_id}Operations
Recovery and reconciliation
Treat webhook delivery as at-least-once. Deduplicate by public ids and keep `GET /generations/{job_id}` available for repair and replay.
| Concern | Guidance |
|---|---|
| Deduplication | Use `job_id` plus `event_type` as the primary dedupe key for webhook processing. |
| Internal correlation | Use `reference` and `metadata` to reconnect the event to your internal records. |
| Consumer downtime | If the consumer is unavailable, re-read the job later with `GET /generations/{job_id}`. |
| Expired URLs | If a signed output URL expired before you copied it, call `GET /generations/{job_id}` again and use the fresh payload. |
Delivery rules
Delivery safety and retries
Webhook delivery targets only public networks and retries transient failures with bounded backoff.
| 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 you rotate it. |
| 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. |
