Ondarlin API
Generate videos from scripts, bots and AI agents. Every call uses your organization's credits, exactly like the studio. Base URL: https://app.ondarlin.com/api/v1
Machine-readable description (OpenAPI 3.1, including the webhook body): https://app.ondarlin.com/api/v1/openapi.json. Code generators and AI agents can build a client from it.
1. Authentication
An organization owner or admin creates a key under Studio → API. The key (h3k_…) is shown once; store it like a password. Send it on every request:
Authorization: Bearer h3k_…
A key belongs to one organization (no X-Org-Id header needed). It can read the balance, workflows and LoRAs, upload images, and create, read, cancel and download jobs. It cannot top up, manage members or create keys. It stops working when revoked, or when the person who created it leaves the organization.
Test keys (sandbox)
Choose Test when creating a key to get h3k_test_…. It calls the same endpoints with the same rules, but jobs run in a sandbox: no credits are used, no GPU is involved, and about 10 seconds after submission the job succeeds with a short sample video (whatever the prompt or images). Webhooks are sent as usual, with "test": true in the job and in the event. Test and live jobs are invisible to each other, and an organization can have at most 5 unfinished test jobs. Build and test your whole integration with a test key, then switch to a live key.
2. Endpoints
GET | /balance | Available credits |
GET | /workflows | Workflows you can use, with prices and allowed settings |
GET | /loras | LoRAs (styles and effects) you can add to a job |
POST | /assets/images | Upload an input image; returns asset_id |
POST | /jobs | Create a job (needs an Idempotency-Key header) |
GET | /jobs?limit=20&cursor=… | Your organization's jobs, newest first |
GET | /jobs?client_reference=… | Jobs carrying one of your references |
GET | /jobs/{job_id} | One job: status, progress estimate, result |
POST | /jobs/{job_id}/cancel | Cancel a job that has not finished |
GET | /jobs/{job_id}/artifacts/{asset_id}/download | Short-lived video link |
3. Choose a workflow
GET /workflows returns, for each workflow: workflow_id, task_type (t2va text to video, i2va image to video, fl2va first and last frame to video), input_images (how many images it needs), allowed_durations (seconds), allowed_resolutions and credits_per_second. A job costs credits_per_second × duration_seconds (1 credit = $0.01). Credits are reserved when the job is created and refunded automatically if it fails, is cancelled or times out. Always read the list instead of hard-coding ids: they change when models are upgraded.
Text-to-video resolutions are aspect ratios at 768p, such as 768p-16:9, 768p-9:16 or 768p-1:1. Image workflows use auto: the video follows the image's shape.
4. Create a job
POST /jobs
Authorization: Bearer h3k_…
Idempotency-Key: 6f1c0e3a-2b7d-4c55-9a0e-4d1f9b2c7e10
Content-Type: application/json
{
"workflow_id": "<from GET /workflows>",
"prompt": "a red fox running through fresh snow, golden hour",
"duration_seconds": 5,
"resolution": "768p-16:9",
"enhance_prompt": false,
"seed": 42, // optional
"input_asset_ids": [], // image workflows: asset ids, in order (first, last)
"lora_selections": [], // optional, up to 2: {"lora_id": "…", "strength": 1.0}
"client_reference": "order-1042", // optional: your own id, up to 200 characters
"metadata": {"sku": "TS-01"} // optional: up to 20 string pairs (keys ≤ 40, values ≤ 500)
}
→ 202 {"job_id": "…", "status": "QUEUED"}Idempotency-Key is required: 8–200 characters of A-Z a-z 0-9 . _ : -, unique per job (a UUID works). Sending the same key again returns the same job (200) instead of creating and charging a second one, so it is safe to retry after a timeout. enhance_prompt (default false): set true to have your prompt rewritten into a detailed shot description before generating.
client_reference and metadata are yours: they are stored with the job, returned by every job endpoint and in webhooks, and never affect generation. Use them to link a video to your order, customer or product; GET /jobs?client_reference=… finds the jobs for one reference.
5. Upload input images
Image workflows need images uploaded first. Send the raw bytes with their type (JPEG, PNG or WebP, up to 15 MB) and pass the returned asset_id in input_asset_ids. For first-and-last-frame workflows, the order is first frame, then last frame.
curl -s https://app.ondarlin.com/api/v1/assets/images -H "Authorization: Bearer $ONDARLIN_API_KEY" \
-H "Content-Type: image/jpeg" --data-binary @photo.jpg
→ {"asset_id": "…", "content_type": "image/jpeg", "size_bytes": …, "width": …, "height": …}6. Wait for the result
Poll GET /jobs/{job_id} every 10–15 seconds. A video usually takes a few minutes. status moves through QUEUED → SUBMITTED → RUNNING and ends as SUCCEEDED, FAILED, CANCELLED or TIMED_OUT. While running, eta_seconds (when known) is the usual generation time of similar jobs, counted from started_at. On failure, error says why.
When it succeeds, artifacts lists the video. Call GET /jobs/{job_id}/artifacts/{asset_id}/download for a link ({url, expires_in}, valid for about 10 minutes) and download the file right away; ask again for a fresh link later.
7. Webhooks (instead of polling)
An organization admin sets an HTTPS URL under Studio → API. When a job finishes, we POST a JSON event to it:
{
"id": "3b0c…", // same on every retry of this event: ignore duplicates
"type": "job.succeeded", // or job.failed, job.cancelled, job.timed_out (ping = test)
"created_at": "2026-11-12T08:15:02Z",
"data": {
"job_id": "…", "status": "SUCCEEDED", "workflow_id": "…",
"client_reference": "order-1042", "metadata": {"sku": "TS-01"},
"error": null, "reserved_credits": 30
}
}Answer with any 2xx within 10 seconds, then do the work (fetch the job and download the video) in the background. Anything else is retried after 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h and 12 h, then given up; the studio lists recent deliveries. Redirects are not followed.
Every request is signed with the secret shown on the API page: H3-Signature: t=<unix time>,v1=<hex HMAC-SHA256(secret, "t.body")>. Check it against the raw body, and reject old timestamps:
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < tolerance
return fresh and hmac.compare_digest(expected, parts["v1"])8. Errors and limits
401 | Missing, wrong or revoked key |
402 | Not enough credits: top up in the studio |
403 | The key is not allowed to do this, or the organization is suspended |
404 | No such job or asset in this organization |
409 | Idempotency-Key reused with a different request |
413 / 415 | Image too large / not JPEG, PNG or WebP |
422 | Invalid settings; detail says which (duration, resolution, image count, LoRA…) |
429 | Too many jobs in progress: wait for one to finish, then retry |
5xx | Temporary problem: retry later with the same Idempotency-Key |
Errors are JSON: {"detail": "…"}. Each organization can have only a few unfinished jobs at a time (by default counted per person; keys count toward the person who created them). Submit, wait for results, then submit more; on 429, back off for 30 seconds or so. If you integrate a product or service that needs more parallel jobs, contact us to raise your organization's limit.
9. Complete example (Python)
import os, time, uuid, requests
B = "https://app.ondarlin.com/api/v1"
H = {"Authorization": f"Bearer {os.environ['ONDARLIN_API_KEY']}"}
wf = next(w for w in requests.get(f"{B}/workflows", headers=H).json() if w["task_type"] == "t2va")
r = requests.post(f"{B}/jobs", headers={**H, "Idempotency-Key": str(uuid.uuid4())}, json={
"workflow_id": wf["workflow_id"], "prompt": "a red fox running through fresh snow",
"duration_seconds": wf["allowed_durations"][0], "resolution": wf["allowed_resolutions"][0],
"enhance_prompt": False,
})
r.raise_for_status()
job_id = r.json()["job_id"]
while True:
job = requests.get(f"{B}/jobs/{job_id}", headers=H).json()
if job["status"] in ("SUCCEEDED", "FAILED", "CANCELLED", "TIMED_OUT"):
break
time.sleep(15)
if job["status"] != "SUCCEEDED":
raise SystemExit(f"{job['status']}: {job['error']}")
video = job["artifacts"][0]
link = requests.get(f"{B}/jobs/{job_id}/artifacts/{video['asset_id']}/download", headers=H).json()
open(f"{job_id}.mp4", "wb").write(requests.get(link["url"]).content)For AI agents
Give the agent this page's address and a key (as an environment variable, not in the prompt). Read /workflows before creating jobs, use a new Idempotency-Key for each new video and the same one for retries, check /balance before large batches, and never print the key.