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 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.
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"),
);
}
Minimal consumer
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. |