Webhooks

Get notified when generations complete

Skip the polling. Flixly POSTs a signed event to your URL the moment a generation lands — whether the request was sync (image models) or async (video models, slow queues).

HMAC-SHA256 signed

Every delivery carries a signature over `${timestamp}.${body}` so you can verify it actually came from us and wasn't tampered with.

Replay-protected

X-Flixly-Timestamp header lets you reject signatures older than ~5 minutes — defends against captured replays.

Auto-retried

3 attempts with exponential backoff (immediate, +2s, +6s). 5-second timeout per attempt. Polling /generations/{id} is your fallback.

Per-key secrets

Each API key has its own webhook secret. Rotate at any time from the dashboard — old signatures fail verification immediately.

Enable webhooks

Two ways to tell Flixly where to POST your events:

Option 1 — Per-key default

Set on your API key

Set a default webhook URL + generate a signing secret on each API key. Every generation submitted with that key delivers to that URL.

Option 2 — Per-request

Pass in the generate body

Override the key default per-request by setting webhook_url in the POST body. Useful for one-off destinations or routing by environment.

POST /api/v1/generate
{
  "model": "veo-3-fast",
  "prompt": "...",
  "webhook_url": "https://you/hook"
}

The event you receive

One POST per completed (or failed) generation. JSON body, signed headers.

Request headers

POST https://your-server.example.com/flixly-webhook
Content-Type: application/json
User-Agent: Flixly-Webhook/1.0
X-Flixly-Event: generation.completed
X-Flixly-Timestamp: 1781085600
X-Flixly-Signature: sha256=abc123...
X-Flixly-Delivery-Id: wh_a1b2c3d4e5f6g7h8

JSON body

{
  "event": "generation.completed",
  "id": "j5h2k9...",
  "status": "completed",
  "type": "TEXT_TO_IMAGE",
  "model": "flux-dev",
  "output_url": "https://cdn.flixly.ai/outputs/...",
  "credits_charged": 1,
  "error": null,
  "created_at": "2026-06-06T12:00:00Z",
  "completed_at": "2026-06-06T12:00:05Z"
}

Field reference

event
generation.completed or generation.failed
id
Task id — same one you got from POST /api/v1/generate and use with GET /api/v1/generations/{id}.
status
Always completed or failed by the time we deliver.
output_url
cdn.flixly.ai URL on success, null on failure.
credits_charged
Integer credits debited. 0 on failure (auto-refunded).
error
User-facing error message on failure, null on success. Already sanitized — safe to surface to users.

Verify signatures

We sign every delivery with HMAC-SHA256 over `${timestamp}.${body}`. You re-compute the HMAC server-side using the secret we showed you when you generated it, and compare in constant time.

Use the raw body.

JSON.parse → JSON.stringify changes whitespace and field order, which breaks the signature. In Express, use a raw-body middleware. In Next.js App Router, call await req.text() before await req.json().

import { Flixly } from "@flixly/sdk";

// In your webhook handler — Express, Hono, Next.js, etc.
export async function POST(req) {
  const rawBody = await req.text();   // MUST be raw — re-stringifying breaks the signature
  const signature = req.headers.get("x-flixly-signature");
  const timestamp = req.headers.get("x-flixly-timestamp");

  const valid = await Flixly.verifyWebhookSignature({
    secret:    process.env.FLIXLY_WEBHOOK_SECRET,
    timestamp,
    signature,
    body:      rawBody,
    tolerance: 300,   // optional — defaults to 300s replay window
  });

  if (!valid) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(rawBody);
  // event.event === "generation.completed" | "generation.failed"
  // event.id, event.status, event.output_url, event.credits_charged, ...
  await handleGenerationEvent(event);
  return new Response("ok");
}

Delivery + retries

Each delivery follows the same pattern, sync or async:

  1. 1
    Attempt 1 — immediate
    POSTed right after the generation lands. 5-second timeout. Any 2xx response is success.
  2. 2
    Attempt 2 — after 2 seconds
    Same timeout. Fires if attempt 1 returned non-2xx or timed out.
  3. 3
    Attempt 3 — after 6 seconds (8s total elapsed)
    Last try. After this, we log + give up. The task itself is fine in our DB — you can always poll /generations/{id} as fallback.

What counts as success

  • Any HTTP 2xx response. Return 200 OK with an empty body if you have nothing to say.
  • We don't follow redirects. Make sure the URL you give us is the final endpoint.
  • We treat 301, 302, and other 3xxs as failures — they would silently strip the X-Flixly-Signature header on the next hop.

Idempotency

Each delivery carries a unique X-Flixly-Delivery-Id header. If retries cause you to receive the same event twice (e.g. our retry fired but your earlier response just took too long to reach us), use that header to dedupe on your side.

The event's id field is the generation id — same across retries of the same delivery. The X-Flixly-Delivery-Id changes per attempt only if we rebuild the payload (we don't currently).

Ready to wire it up?

Set a webhook URL on your API key and generate a signing secret — both take ~30 seconds.