Webhooks

Soyez prévenu dès qu'une génération est terminée

Fini le polling. Flixly envoie un POST signé à votre URL à l'instant où une génération aboutit — que la requête ait été synchrone (modèles d'image) ou asynchrone (modèles vidéo, files d'attente lentes).

Signé en HMAC-SHA256

Chaque livraison porte une signature calculée sur `${timestamp}.${body}`, ce qui vous permet de vérifier qu'elle vient bien de nous et qu'elle n'a pas été altérée.

Protégé contre le rejeu

L'en-tête X-Flixly-Timestamp vous permet de rejeter les signatures de plus de 5 minutes environ — une défense contre les rejeux de requêtes capturées.

Réessais automatiques

3 tentatives avec backoff exponentiel (immédiat, +2s, +6s). Délai d'attente de 5 secondes par tentative. Interroger /generations/{id} reste votre solution de repli.

Un secret par clé

Chaque clé API a son propre secret de webhook. Vous pouvez le renouveler à tout moment depuis le tableau de bord : les anciennes signatures échouent immédiatement à la vérification.

Activer les webhooks

Deux façons d'indiquer à Flixly où envoyer vos événements en POST :

Option 1 — Valeur par défaut de la clé

Définissez-la sur votre clé API

Définissez une URL de webhook par défaut et générez un secret de signature sur chaque clé API. Toute génération soumise avec cette clé est livrée à cette URL.

Option 2 — Par requête

Passez-la dans le corps de la génération

Remplacez la valeur par défaut de la clé requête par requête en renseignant webhook_url dans le corps du POST. Pratique pour des destinations ponctuelles ou pour router selon l'environnement.

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

L'événement que vous recevez

Un POST par génération terminée (ou échouée). Corps JSON, en-têtes signés.

En-têtes de la requête

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

Corps JSON

{
  "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"
}

Référence des champs

event
generation.completed ou generation.failed.
id
Id de la tâche — celui que vous a renvoyé POST /api/v1/generate et que vous utilisez avec GET /api/v1/generations/{id}.
status
Toujours completed ou failed au moment de la livraison.
output_url
Une URL cdn.flixly.ai en cas de succès, null en cas d'échec.
credits_charged
Crédits débités, en nombre entier. 0 en cas d'échec (remboursement automatique).
error
Message d'erreur destiné à l'utilisateur en cas d'échec, null en cas de succès. Déjà nettoyé — vous pouvez l'afficher tel quel à vos utilisateurs.

Vérifier les signatures

Nous signons chaque livraison en HMAC-SHA256 sur `${timestamp}.${body}`. Vous recalculez le HMAC sur votre serveur avec le secret affiché lors de sa génération, puis vous comparez en temps constant.

Utilisez le corps brut.

JSON.parse → JSON.stringify modifie les espaces et l'ordre des champs, ce qui casse la signature. Sous Express, utilisez un middleware de raw body. Dans l'App Router de Next.js, appelez await req.text() avant 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");
}

Livraison et réessais

Chaque livraison suit le même schéma, en synchrone comme en asynchrone :

  1. 1
    Tentative 1 — immédiate
    Envoyée en POST juste après l'aboutissement de la génération. Délai d'attente de 5 secondes. Toute réponse 2xx vaut succès.
  2. 2
    Tentative 2 — après 2 secondes
    Même délai d'attente. Déclenchée si la tentative 1 a renvoyé autre chose qu'un 2xx ou a expiré.
  3. 3
    Tentative 3 — après 6 secondes (8s écoulées au total)
    La dernière. Ensuite, nous journalisons et abandonnons. La tâche elle-même reste intacte dans notre base — vous pouvez toujours interroger /generations/{id} en solution de repli.

Ce qui compte comme un succès

  • Toute réponse HTTP 2xx. Renvoyez 200 OK avec un corps vide si vous n'avez rien à dire.
  • Nous ne suivons pas les redirections. Assurez-vous que l'URL que vous nous donnez est bien le point de terminaison final.
  • Nous traitons 301, 302 et les autres réponses 3xx comme des échecs : elles supprimeraient silencieusement l'en-tête X-Flixly-Signature au saut suivant.

Idempotence

Chaque livraison porte un en-tête X-Flixly-Delivery-Id unique. Si des réessais vous font recevoir deux fois le même événement (par exemple, notre réessai est parti alors que votre première réponse mettait trop de temps à nous parvenir), servez-vous de cet en-tête pour dédoublonner de votre côté.

Le champ id de l'événement est l'id de la génération — identique d'un réessai à l'autre pour une même livraison. Le X-Flixly-Delivery-Id ne change d'une tentative à l'autre que si nous reconstruisons le payload (ce que nous ne faisons pas aujourd'hui).

Prêt à tout brancher ?

Définissez une URL de webhook sur votre clé API et générez un secret de signature — l'un et l'autre prennent environ 30 secondes.