Webhooks

Entérate en cuanto una generación termina

Olvídate del polling. Flixly envía un POST con un evento firmado a tu URL en el momento en que una generación se completa, tanto si la petición fue síncrona (modelos de imagen) como asíncrona (modelos de vídeo, colas lentas).

Firmado con HMAC-SHA256

Cada entrega lleva una firma sobre `${timestamp}.${body}`, así que puedes comprobar que viene realmente de nosotros y que nadie la ha manipulado.

Protegido contra reenvíos

La cabecera X-Flixly-Timestamp te permite rechazar firmas con más de 5 minutos de antigüedad: una defensa frente a reenvíos capturados.

Reintentos automáticos

3 intentos con backoff exponencial (inmediato, +2s, +6s). Tiempo límite de 5 segundos por intento. Consultar /generations/{id} es tu alternativa.

Secretos por clave

Cada API key tiene su propio secreto de webhook. Puedes rotarlo cuando quieras desde el panel: las firmas antiguas dejan de validarse al instante.

Activar los webhooks

Dos formas de indicarle a Flixly a dónde enviar tus eventos por POST:

Opción 1 — Valor por defecto de la clave

Configúralo en tu API key

Define una URL de webhook por defecto y genera un secreto de firma en cada API key. Todas las generaciones enviadas con esa clave se entregan en esa URL.

Opción 2 — Por petición

Pásalo en el cuerpo de la generación

Sobrescribe el valor por defecto de la clave en cada petición indicando webhook_url en el cuerpo del POST. Útil para destinos puntuales o para enrutar según el entorno.

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

El evento que recibes

Un POST por cada generación completada (o fallida). Cuerpo JSON, cabeceras firmadas.

Cabeceras de la petición

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

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

Referencia de campos

event
generation.completed o generation.failed.
id
Id de la tarea: el mismo que recibiste de POST /api/v1/generate y que usas con GET /api/v1/generations/{id}.
status
Siempre completed o failed en el momento de la entrega.
output_url
Una URL de cdn.flixly.ai si todo va bien, null si falla.
credits_charged
Créditos descontados, en número entero. 0 si falla (se reembolsan automáticamente).
error
Mensaje de error orientado al usuario si falla, null si todo va bien. Ya viene saneado: puedes mostrarlo a tus usuarios sin problema.

Verificar las firmas

Firmamos cada entrega con HMAC-SHA256 sobre `${timestamp}.${body}`. Tú recalculas el HMAC en tu servidor con el secreto que te mostramos al generarlo y lo comparas en tiempo constante.

Usa el cuerpo sin procesar.

JSON.parse → JSON.stringify cambia los espacios y el orden de los campos, lo que rompe la firma. En Express, usa un middleware de raw body. En el App Router de Next.js, llama a await req.text() antes que a 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");
}

Entrega y reintentos

Todas las entregas siguen el mismo patrón, sean síncronas o asíncronas:

  1. 1
    Intento 1 — inmediato
    Se envía por POST justo después de que la generación termina. Tiempo límite de 5 segundos. Cualquier respuesta 2xx cuenta como éxito.
  2. 2
    Intento 2 — a los 2 segundos
    Mismo tiempo límite. Se dispara si el intento 1 devolvió una respuesta distinta de 2xx o agotó el tiempo.
  3. 3
    Intento 3 — a los 6 segundos (8s transcurridos en total)
    El último intento. Después de este lo registramos y desistimos. La tarea sigue intacta en nuestra base de datos: siempre puedes consultar /generations/{id} como alternativa.

Qué cuenta como éxito

  • Cualquier respuesta HTTP 2xx. Devuelve 200 OK con el cuerpo vacío si no tienes nada que decir.
  • No seguimos redirecciones. Asegúrate de que la URL que nos das es el endpoint final.
  • Tratamos 301, 302 y las demás respuestas 3xx como fallos: eliminarían silenciosamente la cabecera X-Flixly-Signature en el siguiente salto.

Idempotencia

Cada entrega lleva una cabecera X-Flixly-Delivery-Id única. Si los reintentos hacen que recibas el mismo evento dos veces (por ejemplo, nuestro reintento se disparó pero tu respuesta anterior tardó demasiado en llegarnos), usa esa cabecera para deduplicar por tu lado.

El campo id del evento es el id de la generación: el mismo en todos los reintentos de una misma entrega. El X-Flixly-Delivery-Id solo cambia entre intentos si reconstruimos el payload (algo que hoy no hacemos).

¿Listo para conectarlo?

Define una URL de webhook en tu API key y genera un secreto de firma: las dos cosas llevan unos 30 segundos.