# Submit a generation

`POST /v1/generations` answers at once with an id and the status `queued`. The work runs in the background: poll the id, then download the file. This is the whole flow.

```bash cURL
KEY=$(uuidgen)

ID=$(curl -s -X POST https://vidlab.ai/v1/generations \
  -H "Authorization: Bearer $VIDLAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{
    "model": "seedance-2.5",
    "input": {
      "prompt": "A slow push-in on a perfume bottle on wet black stone",
      "resolution": "720p",
      "duration": 5,
      "aspect_ratio": "9:16"
    },
    "max_cost_usd": 1.50
  }' | jq -r .data.id)

# Poll until it finishes. A failed one is free; to retry, make a new key.
while true; do
  STATUS=$(curl -s https://vidlab.ai/v1/generations/$ID \
    -H "Authorization: Bearer $VIDLAB_API_KEY" | jq -r .data.status)
  echo "$STATUS"
  case "$STATUS" in succeeded|failed) break ;; esac
  sleep 5
done

curl -sL https://vidlab.ai/v1/generations/$ID/content \
  -H "Authorization: Bearer $VIDLAB_API_KEY" -o out.mp4
```

```python Python
import os, time, uuid, requests

API = "https://vidlab.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['VIDLAB_API_KEY']}"}

r = requests.post(
    f"{API}/generations",
    headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
    json={
        "model": "seedance-2.5",
        "input": {
            "prompt": "A slow push-in on a perfume bottle on wet black stone",
            "resolution": "720p",
            "duration": 5,
            "aspect_ratio": "9:16",
        },
        "max_cost_usd": 1.50,
    },
)
r.raise_for_status()
job = r.json()["data"]

# Poll until it finishes. A failed one is free; to retry, make a new key.
while job["status"] in ("queued", "running"):
    time.sleep(5)
    job = requests.get(f"{API}/generations/{job['id']}", headers=HEADERS).json()["data"]

if job["status"] == "succeeded":
    file = requests.get(f"{API}/generations/{job['id']}/content", headers=HEADERS)
    open("out.mp4", "wb").write(file.content)
else:
    print(job["error"]["code"], job["error"]["message"])
```

```js JavaScript
import { writeFile } from "node:fs/promises";

const API = "https://vidlab.ai/v1";
const headers = { Authorization: `Bearer ${process.env.VIDLAB_API_KEY}` };

const res = await fetch(`${API}/generations`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({
    model: "seedance-2.5",
    input: {
      prompt: "A slow push-in on a perfume bottle on wet black stone",
      resolution: "720p",
      duration: 5,
      aspect_ratio: "9:16",
    },
    max_cost_usd: 1.5,
  }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
let { data: job } = await res.json();

// Poll until it finishes. A failed one is free; to retry, make a new key.
while (job.status === "queued" || job.status === "running") {
  await new Promise((r) => setTimeout(r, 5000));
  job = (await (await fetch(`${API}/generations/${job.id}`, { headers })).json()).data;
}

if (job.status === "succeeded") {
  const file = await fetch(`${API}/generations/${job.id}/content`, { headers });
  await writeFile("out.mp4", Buffer.from(await file.arrayBuffer()));
} else console.error(job.error.code, job.error.message);
```

The answer is `202` with the queued generation:

```json
{
  "data": {
    "id": "gen_4Fq9xT2mLr7Kd1sV8bN3pZ0c",
    "object": "generation",
    "status": "queued",
    "model": "seedance-2.5",
    "variant": "720p",
    "units": 5,
    "price_usd": 1.05,
    "outputs": [],
    "created_at": "2026-11-01T18:04:11.000Z",
    "status_url": "/v1/generations/gen_4Fq9xT2mLr7Kd1sV8bN3pZ0c",
    "result_url": "/v1/jobs/gen_4Fq9xT2mLr7Kd1sV8bN3pZ0c/result"
  }
}
```

| Field | Type | Required | What it is |
| - | - | - | - |
| `model` | string | Yes | A model id from `GET /v1/models`. |
| `input` | object | Yes | The model's inputs, see [Input fields](https://docs.vidlab.ai/input-fields). A field the model does not list is refused, never ignored. |
| `max_cost_usd` | number | No | The most you will pay. A route dearer than this is skipped; if none is cheaper the call answers `422 max_cost_exceeded` and nothing is held. Quote the exact request first and pass the quote. |
| `max_wait_s` | number | No | The longest you will wait, in seconds. A route expected to take longer is skipped, and no new route is tried once the time has passed. If no route fits, the call answers `422 max_wait_exceeded` and nothing is held. |
| `route` | string | No | `auto` (default) picks the cheapest healthy route and fails over on its own. `route-1`, `route-2` or `route-3` starts from that place in the ranking. |

| Header | Required | What it is |
| - | - | - |
| `Authorization` | Yes | `Bearer vidlab_sk_...` |
| `Idempotency-Key` | Recommended | 8 to 100 characters of letters, digits and `_ : . -`; a UUID works. The same key returns the same generation and never holds or charges twice. A failed generation is final: to retry, send a new key. See [Rate limits](https://docs.vidlab.ai/rate-limits#idempotency). |

## What happens when you submit

1. The body is checked against the model's options and priced. A problem answers `400` or `422` at once and nothing is held.
2. The price is reserved on your balance. Without room for it the call answers `429 api_wallet_exhausted`.
3. The generation is stored as `queued` and the answer is `202`.

The answer carries `x-wallet-remaining-cents` (what you can still spend after the reservation) and `x-wallet-resets-at`. These routes answer the same as `POST /v1/generations`: `POST /v1/video-router/generate` and `POST /v1/image-router/generate`.
