# Elyum API v1

One endpoint for every video, image and audio model. Name a model, attach your inputs, choose the settings, and get
the finished file back. Priced in US dollars, charged only when the file is delivered.

The same reference, laid out with a nav and code in curl, TypeScript and Python: https://elyum.ai/open/docs. The studio's
MCP server (for agents working with a person, previews you keep or kill) is a separate product:
https://elyum.ai/open/docs/mcp.

- **Base URL:** `https://api.elyum.ai/v1`
- **Auth:** `Authorization: Bearer ek_live_…`, an API key from https://elyum.ai/console/keys. A key that renders needs
  the `generate` and `spend` scopes. An MCP token (`ek_mcp_…`) opens the studio's MCP server only and is refused here
  with a 401 that says so.
- **MCP:** the same API as MCP tools, one per endpoint, at `https://api.elyum.ai/v1/mcp` with the same API key
  (`list_models`, `get_model`, `create_generation`, `get_generation`, `cancel_generation`, `upload_file`, `get_account`;
  https://elyum.ai/open/docs/mcp#api-mcp). Every answer is the object or error below, and a render is a /v1 render.
- **Billing:** API renders are **final**: delivered at full quality and charged once the file is ready. A render that
  fails or is canceled costs nothing. The price of a request is fixed when it is submitted.
- **Balance:** the API spends your API balance, the funds you add on https://elyum.ai/console/billing. A plan's monthly
  allowance is for the Elyum studio.
- **Errors:** every refusal comes back in one envelope with a stable `code` (see [Errors](#errors)).

## Quick start

1. Create an API key on https://elyum.ai/console/keys with the `generate` and `spend` scopes.
2. Add funds on https://elyum.ai/console/billing.
3. Send a request:

```bash
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
  }'
```

The answer is a [generation](#the-generation-object). With `Prefer: wait=55` it comes back finished when the render
takes under 55 seconds; otherwise poll `GET /v1/generations/{id}` until `status` is `succeeded`, `failed` or
`canceled`.

## 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-Id` header (`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.

## Authentication

| Scope | Lets the key |
|---|---|
| `read` | List models, read generations, files and the account. |
| `generate` | Start renders. |
| `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`. |

A key can carry a daily cap in dollars, set on https://elyum.ai/console/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.

## Models

`GET /v1/models` lists every model your key can call. `GET /v1/models/{id}` returns one task, or every task of a
family. The same catalog, with a Playground and a ready request per model: https://elyum.ai/open/models.

| Field | Meaning |
|---|---|
| `id` | `owner/model/task`, e.g. `bytedance/seedance-2.5/reference-to-video` |
| `task` | `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` |
| `prompt` | `required`, `optional` or `none` |
| `inputs` | the roles the task takes, their media types and limits |
| `settings` | every setting the model renders, with its allowed values and default |
| `options` | model-specific settings that go in `options` (empty for most models) |
| `pricing` | the price in dollars (see [Prices](#prices)) |
| `typical_seconds` | how long a render usually takes, once we have enough of them |

**Short ids.** Send `owner/model` (e.g. `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.

## Create a generation

`POST /v1/generations`

| Field | Type | Notes |
|---|---|---|
| `model` | string | Required. A full or short model id. |
| `prompt` | string | Required unless the model says `optional` or `none`. Sent to the model as written, never shortened. |
| `inputs` | list | Files the task takes, each with a `role` (below). Up to 40. |
| `duration` | number | Seconds of output. |
| `resolution` | string | e.g. `480p`, `720p`, `1080p`, `1K`, `2K`. |
| `aspect_ratio` | string | e.g. `16:9`, `9:16`, `1:1`. `auto` follows the first frame, reference or clip where the model supports it. |
| `audio` | boolean | Render sound, on models that can. |
| `seed` | integer | No model takes one since 2026-10-31 (only fal carried it, on Seedance 2.5): refused with `400`, `param: "seed"`. |
| `negative_prompt` | string | The same: refused with `400`, `param: "negative_prompt"`. |
| `voice` | string | Speech models: a voice the model lists, or a voice id from its library. |
| `target` | string | Upscalers: `2K`, `4K`, `6K` or `8K`, as the model's `target` values list (some stop at `4K`). |
| `options` | object | Model-specific settings listed under the model's `options`. |
| `n` | integer | `1` for now. Send several requests for several outputs. |
| `max_usd` | number | The most you will pay, in dollars. A render that would cost more is refused with `400 max_usd` (`usd` says how much) and nothing is held. |
| `metadata` | object | Up to 16 string pairs (keys up to 64 characters, values up to 512), returned on the generation. |

**Strict, never silent.** 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 (`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 listed for the model.

### Inputs

Each input has a `role` (`reference` when left out) and exactly one source:

```json
{ "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…" }
```

| Role | Used by |
|---|---|
| `first_frame`, `last_frame` | image-to-video |
| `reference` | reference-to-video, video-edit (images, clips, audio), image-edit (images), motion-control (the character image) |
| `source` | video-edit, video-extend, the upscalers |
| `motion` | motion-control (the clip whose movement is copied) |

- `url` is an https URL, a base64 `data:` URI, or one of your `/media/…` paths. Its type comes from `type`, the data
  URI or the file extension; send `type` (`image`, `video`, `audio`) when the URL does not say.
- `file` is an id from `POST /v1/files`.
- `generation` reuses an output of one of your earlier renders (`output` is its index, 0 by default).
- A character (`type: "character"`, `id`) attaches every photo of that person from your library.
- **Citing inputs in the prompt.** References are numbered by type in the order you send them: `@image1`, `@video1`,
  `@audio1`. Give an input a `name` (`"name": "maya"`) and cite it as `@maya` instead.

### Upload a file

`POST /v1/files` with a multipart form, field `file` (png / jpg / webp, mp4 / mov / webm, mp3 / wav; up to 60 MB). It needs a key with the `generate` scope. A workspace uploads at most 300 files an hour and 10 GB in a rolling day (429 `rate_limited` / `upload_quota`); a file already uploaded is reused by its id.
Answers `201` with `{ "id": "file_…", "object": "file", "type", "content_type", "bytes", "name", "created_at" }`.

## Getting the result

- `Prefer: wait=N` (up to 55) holds the answer until the render ends or N seconds pass. A finished render answers
  `200`; one still running answers `202` with a `Location` header. `GET /v1/generations/{id}` takes it too.
- Without a wait the create call answers at once with `status: "queued"`; ask for the generation until the status is
  final.
- `POST /v1/generations/{id}/cancel` stops a render that has not finished; nothing is charged. A render that already
  ended answers `409 not_cancelable` with its object.
- `Idempotency-Key: <up to 255 printable characters>` makes a retry safe: the same key with the same body returns the
  first request's generation (header `Idempotent-Replayed: true`). The same key with a different body is refused with
  `409 idempotency_conflict`. A retry that arrives while the first request is still being processed is refused with
  `409 idempotency_in_progress` (`retry_after` seconds): nothing is held for it; retry with the same key.

## The generation object

`GET /v1/generations/{id}` returns it, as does the create call.

```json
{
  "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…"
}
```

- `status`: `queued` → `running` → `succeeded` | `failed` | `canceled`.
- `usage.usd`: what the render costs; `usage.billed`: `held` while it renders, `charged` once delivered, `refunded`
  (and `usd` 0) when it failed or was canceled. `usage.tokens` on Seedance 2.5 only.
- `outputs[].url` is a signed link valid for 24 hours; fetch the generation again for a fresh one.
- `error` on a failed render: `{ "code": "content_refused" | "timeout" | "generation_failed", "message" }`; `canceled`
  on a canceled one.
- `fingerprint` changes when what serves the model changes, so you can tell when output may differ.

## Prices

Every price is in US dollars: per second of video, per image, per render, or per 1,000 characters of speech. Each
task's price is its `pricing` object in `GET /v1/models`; https://elyum.ai/open/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 | Meaning |
|---|---|
| `currency` | `usd` |
| `unit` | `second`, `image`, `render` or `1000_characters` |
| `usd_per_second` | video: a second of output by resolution (`{ "480p": 0.0207, … }`), or under `any` when one price covers all |
| `usd_per_image` | images: one image by resolution (`{ "1K": 0.03, … }`) |
| `usd_per_render` | a flat price per render |
| `usd_per_1000_characters` | speech: 1,000 characters of script |
| `list_usd_per_second`, `list_usd_per_image` | the list price where ours is below it, at the resolutions it is below |
| `with_reference_clip` | `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 they are counted (`output + clip`, or `output + max(clip, output)`) |
| `tokens` | Seedance 2.5: `usd_per_million` tokens and the `formula` |

**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).
`usd_per_second` shows what a second of output costs without a clip.

**Quotes.** `POST /v1/generations?dry_run=true` answers the price with nothing held and nothing rendered:
`{ "object": "quote", "model", "task", "settings", "usage": { "usd", "tokens"? } }`. The same checks run, so a request
that would be refused is refused here too.

## Account

`GET /v1/account` returns your balance, the part the API can spend, your plan and the key's limits:

```json
{
  "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 }
}
```

## Playground, Logs and Usage

Every model has a page at `https://elyum.ai/open/models/<owner>/<model>` (the gallery is
https://elyum.ai/open/models; both are public). Its Playground builds the request from the model's own schema, or takes
the JSON body, quotes it live and runs it. **A Playground run is a real API render**: final, charged on delivery from
your API balance, through the same code as `POST /v1/generations`. The page shows the same request as curl, TypeScript
and Python.

Logs (`/console/logs`) lists every request, from a key or the Playground, with filters by key, model, status, day and
id, the request and response of each, and a CSV export of up to 10,000 rows. Usage (`/console/usage`) shows what was
charged per day, key and model. Both are in dollars. Inside an agency the pages follow the active workspace: the Owner
and Admins see everyone's requests, a Member their own.

## Errors

```json
{
  "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"
  }
}
```

Every answer carries an `X-Request-Id` header; quote it when you contact support. Some codes carry more: `allowed` (the
values a field takes), `usd` (a quote over `max_usd`), `needs` (a missing scope).

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_request`, `invalid_json` | The body is not a request we can read. |
| 400 | `unknown_parameter`, `invalid_parameter`, `unsupported_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_model`, `task_mismatch`, `ambiguous_task`, `missing_input` | The model or 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, the key is not valid, or it is an MCP token (`fix` names the page for an API key). |
| 402 | `insufficient_balance` | Insufficient funds. Add funds at https://elyum.ai/console/billing. |
| 403 | `scope` | The key lacks a scope (`needs` names it). |
| 404 | `not_found`, `unknown_model` | No such generation, file or model on this account. |
| 409 | `idempotency_conflict`, `idempotency_in_progress`, `not_cancelable` | See above. |
| 429 | `key_cap`, `rate_limited`, `concurrency` | Slow down; `retry_after` says when. |
| 503 | `model_unavailable`, `option_unavailable`, `maintenance` | We cannot run this right now. Nothing was charged; retry later. |
| 500 | `internal` | Our fault. Retry; nothing was charged. |
| 500 | `accepted_unconfirmed` | The render was accepted, but its answer was lost. Do not resend: `GET /v1/generations/{id}` with the `id` it names (a retry with the same `Idempotency-Key` replays it), or find it under Activity when there is no `id`. |

`retryable: true` means the same request can be sent again as is (after `retry_after` when given).

**Limits.** A key's daily cap (`429 key_cap`), renders in flight per account (`429 concurrency`: wait for one to
finish, then send again), the request rate (`429 rate_limited`), and per request: up to 40 inputs, files up to 60 MB,
one output, waits up to 55 seconds.

## Agencies

A key scoped to 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.

## Coming next

Webhooks, server-sent events, an OpenAPI file, SDK packages and test keys. Until then `Prefer: wait` stands in for a
webhook on most renders.
