Register a webhook endpoint to receive video generation results asynchronously instead of polling.
Video generation is asynchronous: you submit a job and learn its result when it ends. Name a callback_url when you submit, and OpenDunes sends the end to it — no polling loop. Polling stays available beside it.
curl https://opendunes.com/v1/videos \
-H "Authorization: Bearer $OPENDUNES_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/veo-3.1-fast",
"prompt": "Saharan sand dunes at twilight, time-lapse clouds, cinematic",
"duration": 8,
"callback_url": "https://soukbot.example/hooks/video"
}'callback_url must be a public HTTPS address. The answer is 202:
{
"id": "a2d19c12-4b5e-4d7a-9c10-2f3e4a5b6c7d",
"polling_url": "https://opendunes.com/v1/videos/a2d19c12-4b5e-4d7a-9c10-2f3e4a5b6c7d",
"status": "pending"
}When the video ends — completed, failed, cancelled or expired — OpenDunes sends a POST to your callback_url:
{
"type": "video.generation.completed",
"created_at": "2026-09-24T09:20:11.482Z",
"data": {
"id": "a2d19c12-4b5e-4d7a-9c10-2f3e4a5b6c7d",
"status": "completed",
"generation_id": "gen-3bhGkxlo4XFrqiabUM7NDtwDzWwG",
"model": "google/veo-3.1-fast",
"unsigned_urls": ["https://opendunes.com/v1/videos/a2d19c12-4b5e-4d7a-9c10-2f3e4a5b6c7d/content?index=0"],
"usage": { "cost": 129.6, "is_byok": false }
}
}A failed video carries the provider's reason in data.error, and no unsigned_urls.
from flask import Flask, request
app = Flask(__name__)
seen = set()
@app.post("/hooks/video")
def video_hook():
key = request.headers["X-OpenDunes-Idempotency-Key"]
if key in seen: # a retry of an event you already handled
return "", 200
seen.add(key)
event = request.get_json()
video = event["data"]
if video["status"] == "completed":
print(video["id"], video["unsigned_urls"][0], video["usage"]["cost"], "DA")
else:
print(video["id"], video["status"], video.get("error"))
return "", 200X-OpenDunes-Idempotency-Key: <id>-<status> — the same value on every retry of the same event.2xx. Anything else is retried — six tries in all, the waits growing from 10 seconds to 2 hours.Fetch the clip with your API key at the unsigned_urls address (GET /v1/videos/{id}/content). It is also in Files.
Without a callback_url, or beside one, poll GET /v1/videos/{id} until the job ends:
import os, time, httpx
headers = {"Authorization": f"Bearer {os.environ['OPENDUNES_API_KEY']}"}
while True:
video = httpx.get(job["polling_url"], headers=headers).json()
if video["status"] == "completed":
print(video["unsigned_urls"][0])
break
if video["status"] in ("failed", "cancelled", "expired"):
print(video["status"], video.get("error"))
break
time.sleep(5)Statuses are pending, in_progress, then one of completed, failed, cancelled or expired. Polling is free at any balance.
A video is billed once, at delivery, on the seconds produced; usage.cost is the charge in DA. Failed, cancelled and expired videos charge nothing. See Video Generation for every field.