Un descuento en los planes premium al registrarteConsigue tu descuento

blog/developers·1 sept 2026·6 min·por el equipo de eroq

API asíncrona de generación de video: polling y webhooks firmados

Integra una API de video asíncrona sin perder dinero ni clips: envía trabajos, consulta o recibe webhooks firmados, y gestiona reembolsos y reintentos.


Un render de video tarda de uno a cinco minutos. Ninguna solicitud HTTP debería quedarse abierta tanto tiempo: los balanceadores de carga agotan el tiempo de espera, las conexiones móviles se caen y tus usuarios recargan la página. Por eso toda API seria de generación de video es asíncrona: envías un trabajo, recibes un id y te enteras más tarde. Los detalles de ese «más tarde» son donde las integraciones pierden dinero y pierden clips. Así está diseñado el endpoint de video de eroq y así se consume correctamente.

El contrato en un párrafo

POST /v1/videos/generations cobra el precio fijo en créditos, inicia el render y responde 202 con un id de trabajo. Después, o bien consultas GET /v1/videos/generations/{id} (gratis, cada pocos segundos), o bien recibes un webhook firmado cuando el trabajo termina. Un render que falla, o que no termina en 10 minutos, se reembolsa solo; un prompt fuera de la política de uso aceptable devuelve content_blocked y nunca se cobra. Solo pagas por un clip entregado. Los id de trabajo duran 24 horas.

Enviar un trabajo

curl https://eroq.ai/v1/videos/generations \
  -H "Authorization: Bearer $EROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "eroq-motion-one",
    "prompt": "Slow push-in on a lighthouse keeper in a yellow raincoat climbing a spiral stone staircase at night, lantern swinging, warm practical light against cold blue storm windows, rain streaking the glass, tense and patient mood, 35mm film grain.",
    "seconds": 10,
    "aspect": "9:16",
    "store": true
  }'

La respuesta:

{
  "id": "b7e6c2d4-…",
  "status": "processing",
  "usage": { "credits_spent": 60, "credits_remaining": 887 }
}

Dos parámetros merecen una nota.

seconds es la duración del clip, de 1 a 30. Tu plan la limita (10 s con pago por uso, 15 en Hobby, 20 en Creator, 30 en Studio y superiores) y la cuadrícula del motor la ajusta: Motion One renderiza 5 o 10 s, y Seedance 2.5, cualquier duración de 4 a 30. El ajuste ocurre antes de cobrar, así que el precio siempre corresponde al clip que se renderiza. Envía seconds: 8 a Motion One y se ajustará a la cuadrícula del motor y cobrará el clip que realmente genera; consulta /v1/engines si quieres conocer la cuadrícula de antemano.

aspect admite 16:9, 9:16, 1:1, 21:9, 4:3 o 3:4. Cuando el motor acepta una proporción de forma nativa, se le pasa tal cual; en los demás casos se integra en el prompt: una petición firme al motor, no una garantía absoluta. La entrada de relación de aspecto tiene el detalle por motor.

store: true es opcional y vale la pena en producción: el clip terminado va al eroq Store y el trabajo lleva una URL de CDN duradera en lugar de base64 en línea que caduca con el trabajo. Añade 2 créditos por cada bloque de 10 MB iniciado (precios).

Opción A: polling

Consultar el estado es gratis y la cadencia prevista es cada pocos segundos. Pon un plazo límite un poco más allá de la ventana de reembolso de 10 minutos, para que un trabajo atascado se resuelva solo antes de que tu bucle se rinda.

const BASE = 'https://eroq.ai/v1'
const headers = { Authorization: `Bearer ${process.env.EROQ_API_KEY}` }

async function waitForClip(jobId, { every = 4000, deadline = 11 * 60_000 } = {}) {
  const started = Date.now()
  while (Date.now() - started < deadline) {
    const job = await fetch(`${BASE}/videos/generations/${jobId}`, { headers }).then(r => r.json())
    if (job.status === 'succeeded') return job.data[0].url   // with store: true
    if (job.status === 'failed') throw new Error(job.error?.code ?? 'generation_failed')
    await new Promise(r => setTimeout(r, every))
  }
  throw new Error('timeout')
}

status pasa de processing a succeeded o failed. Con failed, los créditos ya están de vuelta en el saldo, así que «reintentar» significa «volver a enviar», no «discutir la factura».

Opción B: webhook firmado

Registra un endpoint en el panel de desarrolladores, suscríbelo a video.generation.succeeded y video.generation.failed, y copia el secreto whsec_: solo se muestra una vez. Cada entrega es un POST con JSON:

{
  "id": "evt_9f1c…",
  "type": "video.generation.succeeded",
  "data": {
    "id": "b7e6c2d4-…",
    "result_url": "https://…/b7e6c2d4.mp4"
  }
}

Un trabajo fallido lleva un objeto error en lugar de un result_url. Los archivos nunca viajan en el payload: recibes una URL o consultas el trabajo.

La firma es al estilo de Stripe, en la cabecera eroq-signature: t=<unix timestamp>,v1=<hex>, donde v1 es el HMAC-SHA256 de ${t}.${rawBody} con tu secreto. Verifica contra los bytes en bruto y analiza el JSON solo después de la comprobación.

import { createHmac, timingSafeEqual } from 'node:crypto'

export function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')))
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < toleranceSec
  const a = Buffer.from(expected)
  const b = Buffer.from(String(parts.v1 ?? ''))
  return fresh && a.length === b.length && timingSafeEqual(a, b)
}

La entrega es un único intento con un margen de 10 segundos, y que el servidor de un cliente esté caído nunca hace fallar el render de ese cliente. Así que responde 2xx de inmediato, haz el trabajo después y sigue consultando como respaldo para todo lo que no te llegue. La lista de la cola de 24 horas en GET /v1/videos/generations existe precisamente para esa conciliación.

Idempotencia: la parte que de verdad pierde dinero

Aquí todos los modos de fallo son duplicados: un webhook que llega después de que tu poller ya vio el éxito, un reintento tras un error de red al enviar, un worker que se cae entre el 202 y la escritura en la base de datos.

  • Guarda el id del trabajo en la misma transacción que el estado que ve el usuario. El 202 es el momento en que asumes un cargo; si ese id no está en disco antes de decirle al usuario «renderizando», una caída te cuesta un clip que no podrás encontrar. Concilia con la lista de la cola al reiniciar.
  • Haz upsert por id de trabajo y deduplica los eventos por id evt_. Ambos canales pueden informar de la misma finalización. El primero gana; el segundo no hace nada.
  • Nunca vuelvas a enviar tras un error de red en el envío sin comprobar antes. Puede que la solicitud sí haya llegado. Busca en la lista de la cola un trabajo con el mismo prompt en el último minuto antes de cobrarte dos veces.
  • Reintenta solo con failed. El reembolso ya se hizo, así que un envío nuevo es un único cargo nuevo. Respeta el límite de solicitudes por clave: el video tiene 6 solicitudes por minuto en el nivel base, multiplicadas según tu plan.

Los precios por créditos fijos hacen que todo esto sea fácil de auditar: cada trabajo corresponde a un cargo conocido, y el bloque usage de la respuesta te dice el saldo en ese momento.

Preguntas frecuentes

¿Cuánto tiempo tengo para recuperar el clip?

Los id de trabajo caducan 24 horas después de crearse, y el base64 en línea desaparece con ellos. Usa store: true para obtener una URL de CDN que perdura, o descarga el clip en cuanto termine.

¿Y si mi endpoint de webhooks está caído?

Un intento y nada más. Consulta GET /v1/videos/generations/{id} o lista las últimas 24 horas para ponerte al día. Los webhooks son una comodidad; el polling es la fuente de verdad.

¿Puedo obtener una banda sonora?

Solo con veo-3-fast, que renderiza ambiente, efectos y diálogo nativos en clips fijos de 8 segundos; lo modera su proveedor y requiere un plan Creator. Todos los demás motores renderizan video sin sonido: consulta el catálogo de motores.

Empieza con los 50 créditos gratis: consigue una clave de API y envía un trabajo.

Etiquetasvideo-apiwebhooksasyncpollingidempotency

Crea esto con los modelos detrás del artículo: empieza con 50 créditos gratis o explora todos los motores y sus precios.