Platform
Callbacks (webhooks)
Instead of polling, give an image or video request a callback_url. When it’s ready, or if it fails, Zurelay POSTs the result to that URL, signed so you can tell it came from us.
Ask for a callback
Add callback_url to any of these requests. It must be an https URL on the public internet.
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" }'Image requests with a callback
Without callback_url, an image request waits and answers with the images, as OpenAI’s API does. With it, the request is accepted straight away and answers with the job. It’s charged only if images are made:
HTTP/1.1 202 Acceptedx-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"}/v1/images/{id}You can also poll it: GET /v1/images/{id} returns the same object, with fresh links in data once its status is completed. Callbacks always carry links, never base64, whatever response_format says.
What we send
One POST per event, with a JSON body: type, timestamp and data, the job exactly as the API returns it (the video object, or the image request above), with download links that work for 24 hours.
POST /hooks/zurelay HTTP/1.1content-type: application/jsonuser-agent: Zurelay-Webhooks/1.0 (+https://zurelay.com/docs/webhooks)webhook-id: msg_2c1f9e0b7a4d4f6e8b3a91c05d7e6f42webhook-timestamp: 1791000094webhook-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" }}{ "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" }}Verify the signature
Callbacks follow the Standard Webhooks format, the same one OpenAI’s webhooks use. The signature is an HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<body>, keyed with your workspace’s signing secret, in base64. Find the secret (it starts with whsec_) on the dashboard’s API keys page, and keep it on your server only.
import osfrom fastapi import FastAPI, HTTPException, Requestfrom standardwebhooks.webhooks import Webhook # pip install standardwebhookswebhook = 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}- Check the raw body: parsing and re-serializing the JSON changes the bytes, and the signature with them.
- Refuse a
webhook-timestampmore than five minutes old: it stops anyone replaying a callback they caught. The libraries do this for you. - Rotating the secret (owners and admins, on the API keys page) gives you a new one at once. The old one keeps signing alongside it for 24 hours, so switch your server over within the day.
Answer, then work
Reply with any 2xx within 15 seconds and the callback counts as delivered. Save the event and answer first; download the video or images afterwards. A callback can arrive more than once (when your answer was lost on the way, say), so use webhook-id to do each one only once.
Retries
Anything else (an error status, a redirect, no answer within 15 seconds, or a server we can’t reach) is tried again, six attempts in all:
Every callback shows with its request in the dashboard’s Requests: where it went, each attempt’s answer, and when the next try is. Retry callback there sends it again at once, delivered or not. The links inside are fresh on every attempt.
Questions, or something missing? Ask our support team and we’ll answer by email, usually within a minute.