Developer tutorial · Python & Node.js

Domo AI API Integration

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.

~30 minto production basics
2languages
8steps

How the pieces fit

  1. Your appUser clicks “Generate”
  2. Your backendHolds the API key · POST /video/…
  3. DomoAI APIReturns task_id · renders the video
  4. Webhook / pollStatus → SUCCESS with video URL
  5. Your storageDownload before the URL expires (8 h)
01
Step 1

Set up your key and environment

  1. Create a key in the DomoAI Enterprise dashboard under API Keys. It’s shown once — copy it straight into your secrets store.
  2. Add credits in Dashboard → Billing. Credits cost $0.02 and never expire.
  3. Store config in environment variables — never in code or the browser.
.env
# .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.

02
Step 2

Build a small, reusable client

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))
03
Step 3

Poll for results (the simple way)

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.

PENDING→QUEUING→PROCESSING→SUCCESSorFAILED/CANCELED

Use polling for

  • Scripts, CLIs and notebooks
  • Prototypes and internal tools
  • Small batch jobs

Switch to webhooks when

  • Users wait on many tasks at once
  • You run on serverless functions with short timeouts
  • You want fewer API calls and faster updates
04
Step 4

Receive webhooks (the production way)

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}"

Task object you’ll receive

Same shape as GET /tasks/{task_id}. The handler accepts the task wrapped in data or bare — log your first real callback to confirm.

Task (VideoTaskOut)
{
  "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.

05
Step 5

Save outputs before they expire

Output URLs in output_videos expire after 8 hours. Treat them as temporary download links, not permanent hosting.

Download immediately

On SUCCESS, stream each video to your own storage (S3, GCS, R2 or disk) — the client’s download() does this.

Store the metadata

Save task_id, inputs, credits, width/height and your storage key so you can trace and bill every video.

Serve from your CDN

Give users your own URL. If a download fails, retry — but before the 8-hour window closes.

06
Step 6

Handle errors the right way

ResponseMeaningRetry?What your code should do
401Bad or revoked keyNoAlert your team; check the key and Bearer header
402Out of creditsNoPause the queue, notify billing, show users a friendly message
403Organization banned (code 4002)NoStop requests and contact DomoAI support
422Invalid requestNoFix the input — e.g. seconds 1–10 (1–60 for avatar), prompt ≤ 2,000 chars
429 / 5xx / timeoutTemporary problemYesRetry with exponential backoff (1 s, 2 s, 4 s…) and a max attempt count
FAILED statusGeneration didn’t completeMaybeLog 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.

07
Step 7

Scale up: batching, queues & cost tracking

For many videos, run a small worker pool and add up the credits each task reports.

batch.py
# 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}")

Use a job queue

Put generation requests on a queue (Redis, SQS, Cloud Tasks) so traffic spikes don’t turn into failed calls.

Cap concurrency

Start with a few workers and raise the limit gradually while watching for 429s and slowdowns.

Budget per user

Credits per task are returned in the response — enforce per-user or per-plan limits before you create tasks.

$0.205 s · faster model
(2 cr/s)
$0.305 s · talking avatar
(3 cr/s)
$0.505 s · advanced model
(5 cr/s)
$2001,000 × 5 s
faster clips
08
Step 8

Launch checklist

Tick these off before you go live.

Can I call the DomoAI API from the browser or a mobile app?

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.

Should I use polling or webhooks?

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.

How do I send an image to image-to-video?

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).

How do I secure my webhook endpoint?

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.

Why did my video link stop working?

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.

How do I track API costs?

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.

Ready to integrate?

Create your API key, copy the client from Step 2, and generate your first video today.

Get an API key ↗ API reference