Plateforme

Callbacks (webhooks)

Au lieu d’interroger régulièrement l’API, ajoutez un callback_url à votre requête d’image ou de vidéo. Une fois le résultat prêt, ou en cas d’échec, Zurelay l’envoie à cette URL par POST, avec une signature qui vous permet de vérifier qu’il vient bien de nous.

Demander un callback

Ajoutez callback_url à l’une de ces requêtes. Il doit s’agir d’une URL https accessible sur Internet.

EndpointCe qui se passe
POST /v1/videosLance la génération de la vidéo comme d’habitude. À la fin, le callback contient la vidéo terminée (ou la raison de l’échec). Inutile d’interroger régulièrement l’API.
POST /v1/images/generationsRépond immédiatement avec 202 et l’identifiant de la requête, sans attendre les images, les génère en arrière-plan, puis envoie un callback.
POST /v1/images/editsMême principe pour les retouches : un champ JSON, ou un champ de formulaire dans une requête 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"
}'

Requêtes d’image avec callback

Sans callback_url, une requête d’image attend la fin de la génération pour renvoyer les images, comme l’API d’OpenAI. Avec ce paramètre, la requête est acceptée immédiatement et renvoie la tâche. Elle n’est facturée que si des images sont générées :

Réponse
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}

Vous pouvez aussi consulter son état : GET /v1/images/{id} renvoie le même objet, avec de nouveaux liens dans data une fois que son status vaut completed. Les callbacks contiennent toujours des liens, jamais de base64, quelle que soit la valeur de response_format.

Ce que nous envoyons

Un POST par événement, avec un corps JSON : type, timestamp et data, la tâche telle que l’API la renvoie (l’objet vidéo, ou la requête d’image ci-dessus), avec des liens de téléchargement valables 24 heures.

ÉvénementQuand
video.completedLa vidéo est prête : data.url permet de la télécharger.
video.failedLa génération a échoué : data.error indique pourquoi. Rien ne vous a été facturé.
image.completedLes images sont prêtes : data.data contient un lien pour chacune.
image.failedAucune image n’a été générée : data.error indique pourquoi. Rien ne vous a été facturé.
Un callback vidéo
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"
}
}
Un callback image (corps)
{
"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"
}
}
En-têteValeur
webhook-idmsg_… : identique à chaque nouvelle tentative d’envoi de ce callback. Utilisez-le pour ignorer un callback déjà traité.
webhook-timestampDate d’envoi de cette tentative, en secondes Unix.
webhook-signaturev1, puis la signature (voir ci-dessous). Deux signatures, séparées par une espace, pendant un jour après le renouvellement de votre secret.
user-agentZurelay-Webhooks/1.0

Vérifier la signature

Les callbacks suivent le format Standard Webhooks, également utilisé par les webhooks d’OpenAI. La signature est un HMAC-SHA256 de <webhook-id>.<webhook-timestamp>.<body>, calculé avec le secret de signature de votre espace de travail et encodé en base64. Vous trouverez ce secret (qui commence par whsec_) sur la page API Clés du tableau de bord. Conservez-le uniquement sur votre serveur.

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}
  • Vérifiez le corps brut : analyser puis sérialiser à nouveau le JSON modifie les octets, et donc la signature.
  • Refusez tout webhook-timestamp datant de plus de cinq minutes : cela empêche de rejouer un callback intercepté. Les bibliothèques le font pour vous.
  • Le renouvellement du secret (par les propriétaires et administrateurs, sur la page Clés API) vous en fournit immédiatement un nouveau. L’ancien continue de signer en parallèle pendant 24 heures : mettez donc votre serveur à jour dans ce délai.

Répondez, puis traitez

Renvoyez un code 2xx sous 15 secondes pour que le callback soit considéré comme livré. Enregistrez l’événement et répondez d’abord ; téléchargez ensuite la vidéo ou les images. Un callback peut arriver plusieurs fois (si votre réponse s’est perdue en chemin, par exemple) : utilisez donc webhook-id pour ne le traiter qu’une seule fois.

Nouvelles tentatives

Dans tous les autres cas (code d’erreur, redirection, absence de réponse sous 15 secondes ou serveur inaccessible), l’envoi est réessayé, pour un total de six tentatives :

TentativeEnvoi
1Dès la fin de la tâche
21 minute plus tard
35 minutes après la précédente
430 minutes après la précédente
52 heures plus tard
68 heures plus tard, puis le callback est marqué comme ayant échoué

Chaque callback apparaît avec sa requête dans la section Requêtes du tableau de bord : sa destination, la réponse à chaque tentative et la date du prochain essai. Le bouton Réessayer le callback le renvoie immédiatement, qu’il ait déjà été reçu ou non. Les liens qu’il contient sont renouvelés à chaque tentative.

Les callbacks sont envoyés uniquement vers des URL https accessibles sur Internet : ni localhost, ni les adresses privées, ni les domaines qui pointent vers ces adresses. Ils ne suivent pas les redirections, alors utilisez l’URL finale. Pour les tester pendant le développement, utilisez un tunnel (ngrok, Cloudflare Tunnel) ou un inspecteur de requêtes.

Une question ou un élément manquant ? Contactez notre équipe d’assistance et nous vous répondrons par e-mail, généralement en moins d’une minute.