Make a video or image
The key you use for text also makes images, videos and music. Pick a model at
uberllm.dev/models?type=video (or
?type=image, ?type=music); each one shows its price per video, picture or track.
Make an image
Images come back in the answer.
curl https://api.uberllm.dev/v1/images/generations \
-H "Authorization: Bearer $UBERLLM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "venice/sd35", "prompt": "a red apple on a white table", "n": 1, "size": "1024x1024"}'
{ "created": 1792429247, "data": [{ "url": "https://api.uberllm.dev/v1/media/artifacts/<id>/0" }] }
Send "response_format": "b64_json" to get the picture inline instead. Download a url
with the same key:
curl -L -o image.png -H "Authorization: Bearer $UBERLLM_API_KEY" \
"https://api.uberllm.dev/v1/media/artifacts/<id>/0"
n is 1 to 10. Models that edit pictures take the same JSON at /v1/images/edits.
Make a video
A video takes a minute or more, so it is a job: start it, check on it, download it.
1. Start it
curl https://api.uberllm.dev/v1/video/generations \
-H "Authorization: Bearer $UBERLLM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "venice/seedance-2-fast-t2v", "prompt": "a red ball bouncing on green grass", "duration_seconds": 5}'
HTTP 202:
{
"id": "6f1c…",
"object": "media.job",
"kind": "video",
"status": "queued",
"estimated_cost_usdc": "40000",
"max_cost_usdc": "…",
"poll_url": "/v1/video/generations/6f1c…",
"cancel_url": "/v1/video/generations/6f1c…"
}
Costs are in millionths of a dollar (40000 = $0.04). max_cost_usdc is the most the
job can cost; that much is held from your balance while it runs.
2. Check on it every 5–10 seconds:
curl -H "Authorization: Bearer $UBERLLM_API_KEY" \
https://api.uberllm.dev/v1/video/generations/<job id>
HTTP 202 while status is queued or running (with progress from 0 to 1). HTTP 200
once it ends, with status succeeded, failed, canceled or expired:
{
"id": "6f1c…",
"status": "succeeded",
"results": [{ "artifact_index": 0, "url": "https://api.uberllm.dev/v1/media/artifacts/6f1c…/0",
"content_type": "video/mp4", "duration_seconds": 5 }],
"usage": { "cost_usdc": "40000", "cost_usd": "0.04" }
}
3. Download it
curl -L -o video.mp4 -H "Authorization: Bearer $UBERLLM_API_KEY" \
https://api.uberllm.dev/v1/media/artifacts/<job id>/0
Download it soon after it is made. A file that is no longer kept answers 404
artifact_expired.
Stop a job you no longer need: DELETE /v1/video/generations/<job id>.
Get told when it is done — no polling
Instead of checking on the job, pass a webhook_url when you start it and we POST the
finished job to that URL once — the moment it ends, whether it succeeded or failed.
curl https://api.uberllm.dev/v1/video/generations \
-H "Authorization: Bearer $UBERLLM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "venice/seedance-2-fast-t2v", "prompt": "a red ball bouncing",
"duration_seconds": 5,
"webhook_url": "https://your-app.example.com/uberllm/hook",
"webhook_secret": "a-random-string-you-choose"}'
The start response echoes "callback_url" so you can confirm it is armed. Your URL must
be https:// and public (private, loopback and link-local addresses are rejected). You
can still poll the job as well; the callback is in addition, not instead.
When the job ends we POST this JSON to your URL:
{
"event": "media.succeeded",
"sent_at": 1730000000000,
"job": {
"id": "6f1c…", "status": "succeeded",
"results": [{ "artifact_index": 0, "url": "https://api.uberllm.dev/v1/media/artifacts/6f1c…/0" }],
"usage": { "cost_usdc": "40000", "cost_usd": "0.04" }
}
}
job is exactly what the poll endpoint returns, so the same code handles both. event
is media.succeeded, media.failed, media.canceled or media.expired.
Headers on the POST:
| Header | Meaning |
|---|---|
X-UberLLM-Event |
the event name, same as in the body |
X-UberLLM-Delivery |
the job id |
X-UberLLM-Timestamp |
when we sent it (ms since epoch) |
X-UberLLM-Signature |
sha256=<hex> — present only when you set webhook_secret |
Verify the signature (when you set a secret): compute HMAC-SHA256 of the raw request
body with your secret and compare, in constant time, to the hex in X-UberLLM-Signature.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const want = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(header || ""), b = Buffer.from(want);
return a.length === b.length && timingSafeEqual(a, b);
}
Answer 2xx to acknowledge. Any other status (or a timeout) is retried with backoff for
up to a few hours, so make your handler idempotent on the job id. A job started without a
webhook_url is never called back — poll it as above.
Music
The same three steps at /v1/music/generations. curl -OJ saves the track under the
file name we send.
What you pay
- The price shown for what was made, with UberLLM's 10% fee already included. It shows as
usage.cost_usdon the finished job and in Dashboard → Activity. The rest of the hold comes back. - Nothing for a job that
failed, wascanceledorexpired. - You don't have to keep checking for the bill to settle: a job nobody checks on is still finished and billed (or refunded) by UberLLM.
Errors
| HTTP | code | meaning |
|---|---|---|
| 400 | wrong_endpoint |
e.g. an image model sent to /v1/video/generations; the message names the right path |
| 400 | invalid_request |
the request is wrong (the message says why, e.g. a duration the model doesn't offer) |
| 402 | insufficient_credits |
your balance can't cover the job's maximum cost; the message says how much it needs |
| 404 | model_not_found / not_found |
no such model, or no such job on your account |
| 429 | rate_limited |
wait a few seconds and retry |
| 503 | no_available_provider |
no seller makes that right now; UberLLM already retried |