Use polling for
- Scripts, CLIs and notebooks
- Prototypes and internal tools
- Small batch jobs
From first request to production-ready
A step-by-step guide to wiring DomoAI video generation into your app: a reusable client, image uploads, polling with backoff, secure webhooks, saving outputs before they expire, batching and cost tracking.
# .env (never commit this file) DOMOAI_API_KEY=sk-your-key-here DOMOAI_BASE_URL=https://api.domoai.com/v1 WEBHOOK_SECRET=a-long-random-string
Rule #1: all DomoAI calls go through your backend. Your frontend calls your server, never the DomoAI API directly.
One module that creates tasks (text or image), checks status, waits with backoff and downloads results. Images are sent as base64 in image.bytes_base64_encoded.
# domo_client.py — pip install requests
import os, time, base64, requests
API = os.environ.get("DOMOAI_BASE_URL", "https://api.domoai.com/v1")
HEADERS = {"Authorization": f"Bearer {os.environ['DOMOAI_API_KEY']}"}
FINAL = {"SUCCESS", "FAILED", "CANCELED"}
def _post(path, payload, retries=3):
for attempt in range(retries):
r = requests.post(f"{API}{path}", json=payload, headers=HEADERS, timeout=30)
if r.status_code >= 500 or r.status_code == 429:
time.sleep(2 ** attempt) # 1s, 2s, 4s backoff
continue
r.raise_for_status() # 401 / 402 / 403 / 422 -> raise
return r.json()["data"]
r.raise_for_status()
def create_text_video(prompt, seconds=5, **opts):
payload = {"prompt": prompt, "seconds": seconds,
"model": opts.pop("model", "t2v-2.4-faster"), **opts}
return _post("/video/text2video", payload)["task_id"]
def create_image_video(image_path, seconds=5, prompt="", **opts):
with open(image_path, "rb") as f:
img = base64.b64encode(f.read()).decode()
payload = {"model": opts.pop("model", "animate-2.4-faster"),
"image": {"bytes_base64_encoded": img},
"seconds": seconds, "prompt": prompt, **opts}
return _post("/video/image2video", payload)["task_id"]
def get_task(task_id):
r = requests.get(f"{API}/tasks/{task_id}", headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json()["data"]
def wait_for(task_id, timeout=600):
delay, waited = 3, 0
while waited < timeout:
task = get_task(task_id)
if task["status"] in FINAL:
return task
time.sleep(delay); waited += delay
delay = min(delay * 1.5, 15) # gentle backoff, max 15s
raise TimeoutError(f"task {task_id} still running after {timeout}s")
def download(task, folder="videos"):
os.makedirs(folder, exist_ok=True)
paths = []
for i, v in enumerate(task.get("output_videos") or []):
path = os.path.join(folder, f"{task['task_id']}_{i}.mp4")
with requests.get(v["url"], stream=True, timeout=120) as r:
r.raise_for_status()
with open(path, "wb") as f:
for chunk in r.iter_content(1 << 20):
f.write(chunk)
paths.append(path)
return paths
if __name__ == "__main__":
tid = create_text_video("A cat DJ in a neon club, anime style",
seconds=5, aspect_ratio="9:16", style="japanese_anime")
task = wait_for(tid)
print(task["status"], "credits:", task.get("credits"))
if task["status"] == "SUCCESS":
print(download(task))
// domoClient.mjs — Node 18+ (built-in fetch)
import fs from "node:fs/promises";
const API = process.env.DOMOAI_BASE_URL ?? "https://api.domoai.com/v1";
const HEADERS = {
Authorization: `Bearer ${process.env.DOMOAI_API_KEY}`,
"Content-Type": "application/json",
};
const FINAL = new Set(["SUCCESS", "FAILED", "CANCELED"]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function post(path, payload, retries = 3) {
for (let attempt = 0; attempt < retries; attempt++) {
const res = await fetch(`${API}${path}`, {
method: "POST", headers: HEADERS, body: JSON.stringify(payload),
});
if (res.status >= 500 || res.status === 429) {
await sleep(2 ** attempt * 1000); // 1s, 2s, 4s
continue;
}
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return (await res.json()).data;
}
throw new Error("DomoAI unavailable after retries");
}
export async function createTextVideo(prompt, seconds = 5, opts = {}) {
const { task_id } = await post("/video/text2video",
{ prompt, seconds, model: "t2v-2.4-faster", ...opts });
return task_id;
}
export async function createImageVideo(imagePath, seconds = 5, opts = {}) {
const b64 = (await fs.readFile(imagePath)).toString("base64");
const { task_id } = await post("/video/image2video", {
model: "animate-2.4-faster",
image: { bytes_base64_encoded: b64 },
seconds, ...opts,
});
return task_id;
}
export async function getTask(taskId) {
const res = await fetch(`${API}/tasks/${taskId}`, { headers: HEADERS });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return (await res.json()).data;
}
export async function waitFor(taskId, timeoutMs = 600_000) {
let delay = 3000; const start = Date.now();
while (Date.now() - start < timeoutMs) {
const task = await getTask(taskId);
if (FINAL.has(task.status)) return task;
await sleep(delay);
delay = Math.min(delay * 1.5, 15_000);
}
throw new Error(`task ${taskId} timed out`);
}
export async function download(task, folder = "videos") {
await fs.mkdir(folder, { recursive: true });
const out = [];
for (const [i, v] of (task.output_videos ?? []).entries()) {
const buf = Buffer.from(await (await fetch(v.url)).arrayBuffer());
const path = `${folder}/${task.task_id}_${i}.mp4`;
await fs.writeFile(path, buf);
out.push(path);
}
return out;
}
// Example
const id = await createTextVideo("A cat DJ in a neon club, anime style", 5,
{ aspect_ratio: "9:16", style: "japanese_anime" });
const task = await waitFor(id);
console.log(task.status, "credits:", task.credits);
if (task.status === "SUCCESS") console.log(await download(task));
Every task moves through these statuses. The wait_for / waitFor helper above polls GET /tasks/{task_id}, starting at 3 seconds and backing off to 15.
Pass a callback_url when you create a task, and DomoAI sends task updates to your server. Your handler should verify the request, ignore duplicates, reply fast and save the video.
# webhook_app.py — pip install flask requests
import os, hmac
from flask import Flask, request, abort
from domo_client import download
app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"]
seen = set() # use your database in production
@app.post("/hooks/domoai")
def domoai_hook():
# 1. Verify: we put a secret token in the callback_url we registered
if not hmac.compare_digest(request.args.get("token", ""), SECRET):
abort(401)
body = request.get_json(force=True)
task = body.get("data", body) # accept wrapped or bare task
task_id, status = task["task_id"], task["status"]
# 2. Idempotency: the same update may arrive more than once
if (task_id, status) in seen:
return "", 200
seen.add((task_id, status))
# 3. Respond fast; do heavy work in a background job in production
if status == "SUCCESS":
download(task) # URLs expire after 8 hours
elif status in ("FAILED", "CANCELED"):
app.logger.warning("task %s ended: %s", task_id, status)
return "", 200
# When creating a task, pass:
# callback_url = f"https://your-app.com/hooks/domoai?token={SECRET}"
// webhookServer.mjs — npm i express
import express from "express";
import crypto from "node:crypto";
import { download } from "./domoClient.mjs";
const app = express();
app.use(express.json({ limit: "1mb" }));
const SECRET = process.env.WEBHOOK_SECRET;
const seen = new Set(); // use your database in production
const safeEqual = (a, b) =>
a.length === b.length && crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
app.post("/hooks/domoai", async (req, res) => {
// 1. Verify the secret token from the callback_url
if (!safeEqual(String(req.query.token ?? ""), SECRET)) return res.sendStatus(401);
const task = req.body.data ?? req.body; // accept wrapped or bare task
const key = `${task.task_id}:${task.status}`;
// 2. Idempotency
if (seen.has(key)) return res.sendStatus(200);
seen.add(key);
// 3. Acknowledge quickly, then process
res.sendStatus(200);
if (task.status === "SUCCESS") await download(task); // URLs expire in 8 h
else if (["FAILED", "CANCELED"].includes(task.status))
console.warn(`task ${task.task_id} ended: ${task.status}`);
});
app.listen(3000, () => console.log("Webhook listening on :3000"));
// callback_url: `https://your-app.com/hooks/domoai?token=${SECRET}`
Same shape as GET /tasks/{task_id}. The handler accepts the task wrapped in data or bare — log your first real callback to confirm.
{
"code": 0,
"data": {
"task_id": "<TASK_ID>",
"status": "SUCCESS",
"category": "TEXT_TO_VIDEO",
"seconds": 5,
"output_videos": [
{ "url": "<VIDEO_URL>", "width": 1024, "height": 1024 }
],
"credits": 10,
"inputs": { "model": "t2v-2.4-faster", "prompt": "<PROMPT>", "seconds": 5 },
"created_at": "<TIMESTAMP>"
}
}Why a token in the URL? The docs don’t describe a signature header, so add your own secret to the callback URL and compare it in constant time. Always serve webhooks over HTTPS.
Output URLs in output_videos expire after 8 hours. Treat them as temporary download links, not permanent hosting.
On SUCCESS, stream each video to your own storage (S3, GCS, R2 or disk) — the client’s download() does this.
Save task_id, inputs, credits, width/height and your storage key so you can trace and bill every video.
Give users your own URL. If a download fails, retry — but before the 8-hour window closes.
| Response | Meaning | Retry? | What your code should do |
|---|---|---|---|
401 | Bad or revoked key | No | Alert your team; check the key and Bearer header |
402 | Out of credits | No | Pause the queue, notify billing, show users a friendly message |
403 | Organization banned (code 4002) | No | Stop requests and contact DomoAI support |
422 | Invalid request | No | Fix the input — e.g. seconds 1–10 (1–60 for avatar), prompt ≤ 2,000 chars |
429 / 5xx / timeout | Temporary problem | Yes | Retry with exponential backoff (1 s, 2 s, 4 s…) and a max attempt count |
FAILED status | Generation didn’t complete | Maybe | Log it, adjust the input or prompt, retry once; then surface the error |
Validate before you send: check lengths, ranges and allowed values (models, styles, aspect ratios, templates) in your own code — it saves round-trips and avoids 422s.
For many videos, run a small worker pool and add up the credits each task reports.
# batch.py — generate many videos without overloading anything
from concurrent.futures import ThreadPoolExecutor
from domo_client import create_text_video, wait_for, download
prompts = [
"Rainy Tokyo street at night, anime style",
"Cozy cafe with steam rising, 2.5D cinematic",
"Astronaut surfing a cosmic wave, 90s anime",
]
def run(prompt):
tid = create_text_video(prompt, seconds=5, aspect_ratio="9:16")
task = wait_for(tid)
return prompt, task["status"], task.get("credits", 0), download(task)
with ThreadPoolExecutor(max_workers=3) as pool: # keep concurrency modest
results = list(pool.map(run, prompts))
total = sum(r[2] for r in results)
print(f"{len(results)} videos, {total} credits = ${total * 0.02:.2f}")Put generation requests on a queue (Redis, SQS, Cloud Tasks) so traffic spikes don’t turn into failed calls.
Start with a few workers and raise the limit gradually while watching for 429s and slowdowns.
Credits per task are returned in the response — enforce per-user or per-plan limits before you create tasks.
Tick these off before you go live.
No — keep the API key on your server. Your app should call your own backend, which then calls the DomoAI API. Exposing the key in client code lets anyone spend your credits.
Polling GET /tasks/{task_id} is simplest for scripts and prototypes. For production apps, pass a callback_url so DomoAI notifies your server, which scales better and avoids long-running requests.
Base64-encode the image bytes and send them as image.bytes_base64_encoded in the JSON body, along with a model (animate-2.4-faster or animate-2.4-advanced) and seconds (1–10).
Serve it over HTTPS, include a long random secret token in the callback_url and compare it in constant time, make the handler idempotent, and respond quickly while processing in the background.
Output video URLs expire after 8 hours. Download each finished video to your own storage as soon as the task reaches SUCCESS, and serve it from there.
Each task response includes the credits it used. Multiply by $0.02 per credit, store it with the task_id, and enforce per-user or per-plan budgets before creating new tasks.
domoapi.com is an independent guide. Code samples are examples built on the official DomoAI Enterprise API docs (checked October 2026). Test in a development environment and confirm details in the docs before shipping.
Create your API key, copy the client from Step 2, and generate your first video today.