Plataforma

Callbacks (webhooks)

En lugar de hacer polling, pásale un callback_url a cada solicitud de imagen o video. Cuando esté lista, o si falla, Zurelay envía el resultado por POST a esa URL, firmado para que puedas comprobar que viene de nosotros.

Solicita un callback

Añade callback_url a cualquiera de estas solicitudes. Debe ser una URL https accesible desde la internet pública.

EndpointQué ocurre
POST /v1/videosInicia el video como siempre. Al terminar, el callback te trae el video final (o el motivo del fallo). Sin polling.
POST /v1/images/generationsResponde al instante con 202 y el id de la solicitud en vez de esperar las imágenes, las genera en segundo plano y luego llama de vuelta.
POST /v1/images/editsLo mismo para las ediciones: un campo JSON, o un campo del formulario en una solicitud multipart.
curl https://api.zurelay.com/v1/videos \
-H "Authorization: Bearer $ZURELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.0",
"prompt": "A fox running through fresh snow at dawn",
"seconds": 5,
"callback_url": "https://example.com/hooks/zurelay"
}'

Solicitudes de imagen con callback

Sin callback_url, una solicitud de imagen espera y devuelve las imágenes, como la API de OpenAI. Con él, la solicitud se acepta de inmediato y responde con el trabajo. Solo se cobra si se generan imágenes:

Respuesta
HTTP/1.1 202 Accepted
x-request-id: req_6fJd0kQ3pW9xT2mB7cZ1hR4u
{
"id": "req_6fJd0kQ3pW9xT2mB7cZ1hR4u",
"object": "image.generation",
"model": "nano-banana-pro",
"status": "in_progress",
"created_at": 1791000000,
"completed_at": null,
"data": [],
"url_expires_at": null,
"cost": null,
"error": null,
"callback_url": "https://example.com/hooks/zurelay"
}
GET/v1/images/{id}

También puedes consultarlo por polling: GET /v1/images/{id} devuelve el mismo objeto, con enlaces nuevos en data en cuanto su status esté completed. Las devoluciones de llamada siempre incluyen enlaces, nunca base64, diga lo que diga response_format.

Qué enviamos

Un POST por evento, con un cuerpo JSON: type, timestamp y data, el trabajo exactamente como lo devuelve la API (el objeto de vídeo, o la solicitud de imagen de arriba), con enlaces de descarga válidos durante 24 horas.

EventoCuándo
video.completedEl vídeo está listo: data.url lo descarga.
video.failedNo se pudo crear: data.error indica por qué. No se te ha cobrado.
image.completedLas imágenes están listas: data.data contiene un enlace a cada una.
image.failedNo se creó ninguna imagen: data.error indica por qué. No se te ha cobrado.
Una devolución de llamada de vídeo
POST /hooks/zurelay HTTP/1.1
content-type: application/json
user-agent: Zurelay-Webhooks/1.0 (+https://zurelay.com/docs/webhooks)
webhook-id: msg_2c1f9e0b7a4d4f6e8b3a91c05d7e6f42
webhook-timestamp: 1791000094
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"type": "video.completed",
"timestamp": "2026-10-03T03:21:34.000Z",
"data": {
"id": "video_Qm3v8Rk2Lx7Pa1Zt9Ws4Yn6B",
"object": "video",
"model": "seedance-2.0",
"status": "completed",
"progress": 100,
"created_at": 1791000000,
"completed_at": 1791000093,
"expires_at": 1793592000,
"seconds": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"size": "1280x720",
"generate_audio": true,
"url": "https://…/video_Qm3v8Rk2Lx7Pa1Zt9Ws4Yn6B.mp4?token=…",
"url_expires_at": 1791086494,
"cost": 0.45,
"error": null,
"callback_url": "https://example.com/hooks/zurelay"
}
}
Una devolución de llamada de imagennn (cuerpo)
{
"type": "image.completed",
"timestamp": "2026-10-03T03:20:41.000Z",
"data": {
"id": "req_6fJd0kQ3pW9xT2mB7cZ1hR4u",
"object": "image.generation",
"model": "nano-banana-pro",
"status": "completed",
"created_at": 1791000000,
"completed_at": 1791000041,
"data": [{ "url": "https://…/req_6fJd0kQ3pW9xT2mB7cZ1hR4u-1.png?token=…" }],
"url_expires_at": 1791086441,
"cost": 0.07,
"error": null,
"callback_url": "https://example.com/hooks/zurelay"
}
}
CabeceraValor
webhook-idmsg_…: el mismo en cada reintento de esta devolución de llamada. Úsalo para omitir una que ya hayas gestionado.
webhook-timestampCuándo se envió este intento, en segundos Unix.
webhook-signaturev1, y luego la firma (ver más abajo). Dos de ellas, separadas por un espacio, durante un día después de rotar tu secreto.
user-agentZurelay-Webhooks/1.0

Verifica la firma

Las devoluciones de llamada siguen el formato Standard Webhooks, el mismo que usan los webhooks de OpenAI. La firma es un HMAC-SHA256 de <webhook-id>.<webhook-timestamp>.<body>, con clave el secreto de firma de tu espacio de trabajo, en base64. Encontrarás el secreto (empieza por whsec_) en la página Claves de API del panel, y guárdalo solo en tu servidor.

import os
from fastapi import FastAPI, HTTPException, Request
from standardwebhooks.webhooks import Webhook # pip install standardwebhooks
webhook = Webhook(os.environ["ZURELAY_WEBHOOK_SECRET"]) # whsec_…
app = FastAPI()
@app.post("/hooks/zurelay")
async def zurelay_hook(request: Request):
body = await request.body() # the raw bytes: the signature covers exactly these
try:
event = webhook.verify(body, dict(request.headers))
except Exception:
raise HTTPException(status_code=400)
handle(event) # event["type"], event["data"]; keep it quick, or queue it
return {"ok": True}
  • Comprueba el cuerpo sin procesar: analizar y volver a serializar el JSON cambia los bytes, y la firma con ellos.
  • Rechaza un webhook-timestamp con más de cinco minutos de antigüedad: así nadie puede reenviar una devolución de llamada que haya interceptado. Las librerías lo hacen por ti.
  • Rotar el secreto (propietarios y administradores, en la página API keys) te da uno nuevo al instante. El antiguo sigue firmando junto a él durante 24 horas, así que cambia tu servidor en el plazo de un día.

Responde y luego trabaja

Responde con cualquier 2xx en menos de 15 segundos y la devolución de llamada cuenta como entregada. Guarda el evento y responde primero; descarga el vídeo o las imágenes después. Una devolución de llamada puede llegar más de una vez (por ejemplo, si tu respuesta se perdió por el camino), así que usa webhook-id para hacer cada una solo una vez.

Reintentos

Cualquier otra cosa (un estado de error, una redirección, ninguna respuesta en 15 segundos o un servidor al que no podemos llegar) se reintenta, seis intentos en total:

IntentoEnviado
1En cuanto termina el trabajo
21 minuto después
35 minutos después
430 minutos después
52 horas después
68 horas después de eso, se marca como fallido

Cada callback aparece junto a su solicitud en Solicitudes del panel: a dónde fue, la respuesta de cada intento y cuándo es el próximo reintento. Ahí, Retry callback lo envía de nuevo al instante, se entregue o no. Los enlaces que contiene se renuevan en cada intento.

Los callbacks solo se envían a URLs https de internet pública: no a localhost, direcciones privadas ni nada que resuelva a una de ellas. No siguen redirecciones, así que usa la URL final. Para probarlos mientras desarrollas, usa un túnel (ngrok, Cloudflare Tunnel) o un inspector de peticiones.

¿Tienes dudas o falta algo? Escribe a nuestro equipo de soporte y te responderemos por email, normalmente en menos de un minuto.