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

The X-Flixly-Timestamp header lets you reject signatures older than about 5 minutes — a defence 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 it 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 and generate a signing secret on each API key. Every generation submitted with that key delivers to that URL.

Option 2 — Per-request

Pass it in the generate body

Override the key default per request by setting webhook_url in the POST body. Useful for one-off destinations or for 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 — the same one you got back 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
A cdn.flixly.ai URL on success, null on failure.
credits_charged
Integer credits debited. 0 on failure (automatically refunded).
error
A user-facing error message on failure, null on success. Already sanitized — safe to surface to your users.

Verify signatures

We sign every delivery with HMAC-SHA256 over `${timestamp}.${body}`. You re-compute the HMAC on your server using the secret we showed you when you generated it, then 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 the 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 and retries

Every 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 counts as success.
  2. 2
    Attempt 2 — after 2 seconds
    Same timeout. Fires if attempt 1 returned a non-2xx response or timed out.
  3. 3
    Attempt 3 — after 6 seconds (8s total elapsed)
    The last try. After this we log it and give up. The task itself is fine in our database — you can always poll /generations/{id} as a 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 3xx responses as failures — they would silently strip the X-Flixly-Signature header on the next hop.

Idempotency

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

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

Ready to wire it up?

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