1.Getting started
Every model, from Seedance 2.5 to Nano Banana 2, is one POST to the same endpoint. The request names the model; the answer is a generation you wait for or poll until its file is ready.
How it works
- 1Send
POST /v1/generationswith a model id, a prompt, your inputs and the settings. Anything the model cannot render is refused with a 400 before anything is charged. - 2Wait or pollAdd
Prefer: wait=55and a render that finishes within 55 seconds comes back done. A longer one answers202: askGET /v1/generations/{id}until its status issucceeded,failedorcanceled. - 3Download the fileEach output has a signed link, valid for 24 hours. Fetch the generation again for a fresh one.
API renders are final: delivered at full quality and charged once the file is ready. A render that fails or that you cancel costs nothing. The price of a request is fixed when it is submitted.
Quick start
- 1Create an API key
- 2Add fundsOn Billing. The API spends your API balance; nothing is charged until a file is delivered.
- 3Send your first requestBelow, Seedance 2.5 animating a still. Every model's page under Models has its own request, ready to copy, and a Playground to try it.
curl https://api.elyum.ai/v1/generations \
-H "Authorization: Bearer $ELYUM_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: wait=55" \
-d '{
"model": "bytedance/seedance-2.5/image-to-video",
"prompt": "The camera slowly pushes in as she turns toward the window",
"inputs": [
{
"role": "first_frame",
"url": "https://example.com/frame.jpg"
}
],
"resolution": "720p",
"duration": 5
}'{
"id": "g_81c2…",
"object": "generation",
"model": "bytedance/seedance-2.5/image-to-video",
"task": "image-to-video",
"status": "succeeded",
"delivery": "final",
"created_at": "2026-10-10T12:00:00.000Z",
"finished_at": "2026-10-10T12:01:24.000Z",
"settings": {
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"audio": true
},
"outputs": [
{
"index": 0,
"url": "https://elyum.ai/media/…?e=…&s=…",
"type": "video",
"content_type": "video/mp4",
"width": 1280,
"height": 720,
"duration": 5,
"expires_at": "2026-10-11T12:01:24.000Z"
}
],
"usage": {
"usd": 1.42,
"tokens": 108900,
"billed": "charged"
},
"error": null,
"metadata": {},
"fingerprint": "fp_6a0c…"
}Requests and responses
- Bodies are JSON (
Content-Type: application/json), except a file upload, which is a multipart form. - Every answer carries an
X-Request-Idheader (req_…). Quote it when you write to support. - Times are ISO 8601 in UTC. Ids carry their kind:
g_…a generation,file_…an upload,req_…a request. - Amounts are US dollars, as numbers (
1.42), to the hundredth of a cent where a price per second needs it.
2.Authentication
Send your API key as a bearer token on every request.
API keys
Authorization: Bearer ek_live_…Keys start with ek_live_ and are made on Keys in your console, where you can also revoke one at once. Never put a key in browser code or commit it: call the API from your server, with the key in an environment variable (ELYUM_API_KEY in every sample here).
Your key also opens the API's MCP server at https://api.elyum.ai/v1/mcp, for an agent that calls these endpoints as tools: see API MCP server. An MCP token (ek_mcp_…) belongs to the studio's own MCP server; sent here it is refused with 401 auth, and the error names the page for an API key.
Scopes
| Scope | Lets the key |
|---|---|
read | List models, read generations, files and the account. |
generate | Start renders and upload files. |
spend | Pay for them. A render is paid on delivery, so a key that renders needs generate and spend; without one the answer is 403 scope, naming it in needs. |
Daily spend cap
A key can carry a daily cap in dollars, set on Keys: what it may spend in a rolling 24 hours. A render that would go over it is refused with 429 key_cap and nothing is held, so a runaway loop stops at the cap, not at an empty balance. GET /v1/account shows the cap and what the key spent.
3.Models
Each model is listed with the tasks it runs, the inputs and settings each task takes, and its price.
List models
/v1/modelsevery model your key can call/v1/models/{id}one task, or every task of a familycurl https://api.elyum.ai/v1/models \
-H "Authorization: Bearer $ELYUM_API_KEY"The same catalog, with a Playground and a ready request per model, is at Models.
Model ids
A model id is owner/model/task, for example bytedance/seedance-2.5/reference-to-video. Tasks: text-to-video, image-to-video, reference-to-video, video-edit, video-extend, motion-control, video-upscale, text-to-image, image-edit, image-upscale, text-to-speech, text-to-music, text-to-sound-effects.
Short ids. Send owner/model (bytedance/seedance-2.5) and the task is worked out from your inputs: no inputs is text to video, a first_frame is image to video, references are reference to video, a source clip is an edit. The price is always the price of the task that runs.
The model object
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | Always | — | The model id, owner/model/task. |
task | string | Always | — | The task this entry runs. |
prompt | string | Always | — | required, optional or none. |
inputs | object | Always | — | The input roles the task takes, the media types of each and their limits. |
settings | object | Always | — | Every setting the model renders, with its allowed values and its default. |
options | string[] | Always | — | Model-specific settings that go in options; empty for most models. |
pricing | object | Always | — | The price in dollars (Prices). |
typical_seconds | number | No | — | How long a render usually takes, once we have enough of them. |
4.Create a generation
One call for every model and task. Only the model is always required; the model's entry says what else it needs.
/v1/generationsRequest parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
model | string | Yes | — | A full model id, or a short one whose task is worked out from the inputs. |
prompt | string | Most models | — | Required unless the model says optional or none. Sent to the model as written, never shortened. |
inputs | object[] | No | — | The files the task takes, each with a role (Inputs). Up to 40. |
duration | number | No | default per model | Seconds of output. |
resolution | string | No | default per model | 480p, 720p, 1080p, 1K, 2K… as the model lists. |
aspect_ratio | string | No | default per model | 16:9, 9:16, 1:1… auto follows the first frame, reference or clip where the model supports it. |
audio | boolean | No | default per model | Render sound, on models that can. |
voice | string | No | — | Speech models: a voice the model lists, or a voice id from its library. |
target | string | No | — | Upscalers: 2K, 4K, 6K or 8K, as the model's target values list. |
options | object | No | — | Model-specific settings listed under the model's options. |
n | integer | No | default 1 | One output per request for now. Send several requests for several outputs. |
max_usd | number | No | — | The most you will pay, in dollars. A render that would cost more is refused with 400 max_usd and nothing is held. |
metadata | object | No | — | Up to 16 string pairs (keys up to 64 characters, values up to 512), returned on the generation. |
An unknown field, a wrong type, an input the task cannot take or a value the model does not render is a 400 naming the field in param, before anything is held. We never drop a setting or round it to a nearby value. A setting you leave out takes the default the model lists.
Inputs
Each input has a role (reference when you leave it out) and exactly one source:
{ "role": "first_frame", "url": "https://example.com/frame.jpg" }
{ "role": "reference", "file": "file_3f9a…" }
{ "role": "source", "generation": "g_81c2…", "output": 0 }
{ "role": "reference", "type": "character", "id": "cr_5d1e…" }| Field | Type | Required | Default | Description |
|---|---|---|---|---|
role | string | No | default reference | first_frame, last_frame, reference, source or motion (below). |
url | string | No | — | An https URL, a base64 data: URI, or one of your /media/… paths. |
file | string | No | — | An id from POST /v1/files. |
generation | string | No | — | One of your earlier renders, with output its index. |
output | integer | No | default 0 | Which output of generation. |
id | string | No | — | A character from your library (type: "character"): every photo of that person rides along. |
type | string | No | — | image, video, audio or character. Needed only when a URL does not say what it is. |
name | string | No | — | A word you cite in the prompt as @name (below). |
| Role | Used by |
|---|---|
first_frame, last_frame | image-to-video |
reference | reference-to-video and video-edit (images, clips, audio), image-edit (images), motion-control (the character image) |
source | video-edit, video-extend and the upscalers |
motion | motion-control: the clip whose movement is copied |
Citing inputs in the prompt
References are numbered by type in the order you send them: @image1, @image2, @video1, @audio1. Give an input a name and cite it by that name instead.
curl https://api.elyum.ai/v1/generations \
-H "Authorization: Bearer $ELYUM_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: wait=55" \
-d '{
"model": "bytedance/seedance-2.5/reference-to-video",
"prompt": "@maya walks through @loft at golden hour, the camera tracking beside her",
"inputs": [
{
"url": "https://example.com/maya.png",
"name": "maya"
},
{
"url": "https://example.com/loft.jpg",
"name": "loft"
}
],
"duration": 8
}'Upload a file
/v1/filesmultipart form, field fileUpload a file once and use its id in any request. PNG, JPG and WebP images, MP4, MOV and WebM clips, MP3 and WAV audio, up to 60 MB.
curl https://api.elyum.ai/v1/files \
-H "Authorization: Bearer $ELYUM_API_KEY" \
-F "[email protected]"{
"id": "file_3f9a…",
"object": "file",
"type": "image",
"content_type": "image/png",
"bytes": 482113,
"name": "frame.png",
"created_at": "2026-10-10T12:00:00.000Z"
}5.Getting the result
A render takes from a few seconds to a few minutes. Hold the request open for up to 55 seconds, or submit and come back.
Submit and wait
Send Prefer: wait=N (up to 55) to hold the answer until the render ends or N seconds pass. A finished render answers 200; one still running answers 202 with a Location header pointing at the generation.
curl https://api.elyum.ai/v1/generations \
-H "Authorization: Bearer $ELYUM_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: wait=55" \
-d '{
"model": "bytedance/seedance-2.5/image-to-video",
"prompt": "The camera slowly pushes in as she turns toward the window",
"inputs": [
{
"role": "first_frame",
"url": "https://example.com/frame.jpg"
}
],
"resolution": "720p",
"duration": 5
}'Poll
/v1/generations/{id}status and outputs; also takes Prefer: waitWithout a wait the create call answers at once with status: "queued". Ask for the generation until the status is final; with Prefer: wait=55 on each call you need only a few calls, and no sleep between them.
# 1. Submit (answers 202 with the generation and a Location header)
curl https://api.elyum.ai/v1/generations \
-H "Authorization: Bearer $ELYUM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "bytedance/seedance-2.5/image-to-video",
"prompt": "The camera slowly pushes in as she turns toward the window",
"inputs": [
{
"role": "first_frame",
"url": "https://example.com/frame.jpg"
}
],
"resolution": "720p",
"duration": 5
}'
# 2. Ask again until status is succeeded, failed or canceled (each call may wait up to 55 s)
curl https://api.elyum.ai/v1/generations/g_81c2… \
-H "Prefer: wait=55" \
-H "Authorization: Bearer $ELYUM_API_KEY"The generation object
{
"id": "g_81c2…",
"object": "generation",
"model": "bytedance/seedance-2.5/image-to-video",
"task": "image-to-video",
"status": "succeeded",
"delivery": "final",
"created_at": "2026-10-10T12:00:00.000Z",
"finished_at": "2026-10-10T12:01:24.000Z",
"settings": {
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"audio": true
},
"outputs": [
{
"index": 0,
"url": "https://elyum.ai/media/…?e=…&s=…",
"type": "video",
"content_type": "video/mp4",
"width": 1280,
"height": 720,
"duration": 5,
"expires_at": "2026-10-11T12:01:24.000Z"
}
],
"usage": {
"usd": 1.42,
"tokens": 108900,
"billed": "charged"
},
"error": null,
"metadata": {},
"fingerprint": "fp_6a0c…"
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
status | string | Always | — | queued → running → succeeded, failed or canceled. |
delivery | string | Always | — | final: delivered at full quality and charged on delivery. |
settings | object | Always | — | What the render was asked for, defaults filled in. |
outputs[].url | string | No | — | A signed link, valid for 24 hours (expires_at). Fetch the generation again for a fresh one. |
outputs[] | object | No | — | type, content_type, and width, height, duration where they apply. Empty until the render succeeds. |
usage.usd | number | Always | — | What the render costs: held while it runs, charged once delivered, 0 when it failed or was canceled. |
usage.billed | string | Always | — | held, charged or refunded. |
usage.tokens | integer | No | — | Seedance 2.5 only: the tokens it was billed by (Prices). |
error | object | No | — | On a failed render { code, message }: content_refused, timeout or generation_failed; canceled on a canceled one. |
metadata | object | Always | — | Your metadata, as sent. |
fingerprint | string | No | — | Changes when what serves the model changes, so you can tell when output may differ. |
Cancel
/v1/generations/{id}/cancelStops a render that has not finished. Nothing is charged. A render that already ended answers 409 not_cancelable with its generation.
curl -X POST https://api.elyum.ai/v1/generations/g_81c2…/cancel \
-H "Authorization: Bearer $ELYUM_API_KEY"Retry safely
Send Idempotency-Key (up to 255 printable characters, no spaces) and a retry of the same request returns the first request's generation instead of rendering twice, with the header Idempotent-Replayed: true. The same key with a different body is refused with 409 idempotency_conflict.
curl https://api.elyum.ai/v1/generations \
-H "Authorization: Bearer $ELYUM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821-shot-3" \
-d '{
"model": "bytedance/seedance-2.5/image-to-video",
"prompt": "The camera slowly pushes in as she turns toward the window",
"inputs": [
{
"role": "first_frame",
"url": "https://example.com/frame.jpg"
}
],
"resolution": "720p",
"duration": 5
}'6.Prices and balance
Every price is in US dollars: per second of video, per image, per render, or per 1,000 characters of speech.
Prices
Each task's price is in its pricing object in GET /v1/models, and on its page under Models. Pricing compares ours with every other provider's, model by model. Prices follow the market and can change; a render is charged at the price quoted when it was submitted.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
currency | string | Always | — | usd. |
unit | string | Always | — | second, image, render or 1000_characters. |
usd_per_second | object | No | — | Video: the price of a second of output by resolution ({ "480p": 0.0207, … }), or under any when one price covers all. |
usd_per_image | object | No | — | Images: the price of one image by resolution ({ "1K": 0.03, … }). |
usd_per_render | number | No | — | A flat price per render. |
usd_per_1000_characters | number | No | — | Speech: the price of 1,000 characters of script. |
list_usd_per_second, list_usd_per_image | object | No | — | The list price where ours is below it, at the resolutions it is below. |
with_reference_clip | object | No | — | usd_per_second_of_clip_and_output: the rate for every second of a reference clip and the output together, by resolution; billed_seconds: how those seconds are counted (output + clip, or output + max(clip, output)). |
tokens | object | No | — | Seedance 2.5: usd_per_million tokens and the formula below. |
A reference clip's seconds are charged (Seedance 2.5, Seedance 2.0, Kling 4.0, Wan 3.0 and Wan 3.0 Prime; an edit's source clip included): they are added to the output's, and every second of the two is billed at with_reference_clip.usd_per_second_of_clip_and_output. Seedance and Kling 4.0 have a lower rate per second with a clip; Wan bills a clip's seconds at its usual rate. On Kling 4.0 a clip shorter than the output counts as the output's length, as every provider bills it. usage.billed_seconds says how many seconds were billed.
Seedance 2.5 counts both in tokens: width × height × (24 × output seconds + 1 + 24 × reference-clip seconds) ÷ 1024, at tokens.usd_per_million (the with_reference_clip rate once a clip is attached).
Quote a request
Add ?dry_run=true to a create call to get its exact price with nothing held and nothing rendered. The same checks run, so a request that would be refused is refused here too.
curl https://api.elyum.ai/v1/generations?dry_run=true \
-H "Authorization: Bearer $ELYUM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "bytedance/seedance-2.5/image-to-video",
"prompt": "The camera slowly pushes in as she turns toward the window",
"inputs": [
{
"role": "first_frame",
"url": "https://example.com/frame.jpg"
}
],
"resolution": "720p",
"duration": 5
}'{
"object": "quote",
"model": "bytedance/seedance-2.5/image-to-video",
"task": "image-to-video",
"settings": {
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"audio": true
},
"usage": {
"usd": 1.42,
"tokens": 108900
}
}Account
/v1/accountyour balance and this key's limits{
"object": "account",
"balance": {
"usd": 53,
"api_spendable": {
"usd": 3
}
},
"plan": {
"code": "pro",
"name": "Pro"
},
"key": {
"scopes": [
"read",
"generate",
"spend"
],
"can_generate": true,
"daily_cap_usd": 10,
"spent_24h_usd": 1.42,
"remaining_24h_usd": 8.58
}
}api_spendable is the part of your balance the API can spend: the funds you added. A plan's monthly allowance is for the Elyum studio. When it runs out a render is refused with 402 insufficient_balance.
Logs and usage
Logs lists every request, from a key or the Playground: filters by key, model, status, day and id, the request and the answer of each, and a CSV export of up to 10,000 rows. Usage shows what was charged per day, key and model. Both are in dollars.
7.Errors
Every refusal comes back in one shape, with a stable code your code can branch on.
The error object
{
"error": {
"code": "invalid_parameter",
"message": "bytedance/seedance-2.5/image-to-video renders 4–30 whole seconds, not 45.",
"param": "duration",
"fix": null,
"retryable": false,
"retry_after": null,
"request_id": "req_6f1c…",
"docs_url": "https://elyum.ai/open/docs#invalid-parameter"
}
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
code | string | Always | — | Stable; see Error codes. |
message | string | Always | — | What went wrong, for a person. |
param | string | No | — | The request field at fault: duration, inputs[1].url, model. |
fix | string | No | — | The next step, when there is one. |
retryable | boolean | Always | — | The same request can be sent again as is (after retry_after when given). |
retry_after | number | No | — | Seconds to wait before retrying, when the refusal is temporary. |
request_id | string | Always | — | The request's id, also in X-Request-Id. |
Some codes carry more: allowed (the values a field takes), usd (a quote over max_usd), needs (a missing scope).
Error codes
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_requestinvalid_json | The body is not a request we can read. |
| 400 | unknown_parameterinvalid_parameterunsupported_parameter | A field we do not know, or a value the model does not render. param names it; allowed lists the values where there are a few. |
| 400 | unknown_modeltask_mismatchambiguous_taskmissing_input | The model and the inputs do not fit together. |
| 400 | max_usd | The render costs more than max_usd (usd says how much). Nothing was held. |
| 401 | auth | No key, a key that is not valid, or an MCP token. fix names the page for an API key. |
| 402 | insufficient_balance | Insufficient funds. Add funds on the Billing page of your console. |
| 403 | scope | The key lacks a scope; needs names it. |
| 404 | not_foundunknown_model | No such generation, file or model on this account. |
| 409 | idempotency_conflictnot_cancelable | The key was used with another body, or the render already ended. |
| 413 | request_too_large | A file over 60 MB, or a body over the limit. |
| 415 | unsupported_media_type | A file that is not png, jpg, webp, mp4, mov, webm, mp3 or wav, or whose contents are not its declared type. |
| 429 | key_capconcurrencyrate_limited | Slow down: retry_after says when. |
| 503 | model_unavailableoption_unavailablemaintenance | We cannot run this right now. Nothing was charged; retry later. |
| 500 | internal | Our fault. Retry; nothing was charged. |
Limits
- Daily spend cap per key, when you set one:
429 key_cap. - Renders in flight per account: past the limit a new render answers
429 concurrency. Wait for one to finish (aPrefer: waitcall does it for you), then send again. - Request rate:
429 rate_limitedwithretry_after. - Per request: up to 40 inputs, files up to 60 MB, one output (
n: 1), waits up to 55 seconds.
8.Businesses
Inside an Elyum business a key belongs to one client, to the business level, or to any client. A key for one client renders for that client. A key that may act for any client names the client on each request with the Elyum-Client header (the client's id or its exact name). Monthly budgets per member and per client are hard limits: 429 member_budget and 429 client_budget hold nothing.
curl https://api.elyum.ai/v1/generations \
-H "Authorization: Bearer $ELYUM_API_KEY" \
-H "Content-Type: application/json" \
-H "Elyum-Client: Northwind" \
-d '{
"model": "bytedance/seedance-2.5/image-to-video",
"prompt": "The camera slowly pushes in as she turns toward the window",
"inputs": [
{
"role": "first_frame",
"url": "https://example.com/frame.jpg"
}
],
"resolution": "720p",
"duration": 5
}'9.Coming next
Not built yet: webhooks, server-sent events, an OpenAPI file, SDK packages and test keys. Until then, the samples here use plain HTTP (fetch, requests), and Prefer: wait stands in for a webhook on most renders.