> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aurous-labs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a video

> Submit a video generation — text-to-video, image-to-video, or subject-driven with a saved character or reference images.

`POST /v1/videos` submits a video generation request. Credits are deducted based on duration × resolution. There are three ways to drive the generation: **text-to-video** (prompt only), **image-to-video** (a first frame, or first + last frame interpolation), and **subject-driven** (a saved character or up to 6 reference images, anchored across the video).

A first frame can additionally carry a **video model** — pinned via `video_lora_id`, or auto-matched when you omit it. The model supplies the motion, your image supplies the person and the scene. See [Start from a still with a video model](#start-from-a-still-with-a-video-model).

<Note>
  Want the byte-exact Seedance video API instead — SDK-pointable, forwarded
  verbatim? See the [raw Seedance access](/api-reference/seedance/overview) surface.
  This `POST /v1/videos` endpoint is the managed path (characters, reference
  handling, first/last-frame helpers); the raw API is a direct passthrough.
</Note>

<Note>
  **Polling video status**: video generations and image generations share the
  status endpoint. After `POST /v1/videos`, poll [`GET /v1/images/{id}`](/api-reference/images/retrieve-image) —
  the ID returned for videos is `vid_<ulid>` but the polling path is `/v1/images/{id}`.
  This unification is part of the v1.0 contract.
</Note>

## Text-to-video

Send `prompt` alone. This is the default mode — no frame, no character, no reference images.

## Image-to-video

Pass `first_frame_url` to animate from a single starting frame, or both `first_frame_url` and `last_frame_url` to interpolate between two frames. Each accepts an opaque `file_<ulid>` (from `POST /v1/files`) or an `https://` URL (server-side fetched through an SSRF-pinned client, max 2048 chars). `last_frame_url` requires `first_frame_url` — sending it alone returns `400 parameter_invalid_combination`.

### Start from a still with a video model

Send `first_frame_url` together with a video model — pinned via `video_lora_id`, or auto-matched by omitting it — and the generation runs in **reference mode**: the model supplies the motion while your image supplies the person, the scene, and the orientation.

Your video **starts near your image** — same person, same scene, same orientation — but frame 0 is recomposed, not copied. It is not a pixel-exact continuation of the still you sent. If you need the output to open on your exact frame, don't attach a video model: send `first_frame_url` with `video_lora_id: null`, which is guaranteed plain image-to-video.

<Warning>
  **`video_lora_id: null` costs more — usually a lot more.** It is a fidelity choice with a price attached, not a free one. A video model supplies `default_duration`, `default_resolution` **and** `default_ratio`, and those defaults are what get priced. Opt out and there is nothing to supply them, so the request falls back to adaptive duration (billed against the 15-second ceiling) at 1080p with no ratio pinned — the largest frame we might deliver.

  Same request, same still, only the tri-state differing:

  | `video_lora_id`       | Duration               | Resolution           | Credits held |
  | --------------------- | ---------------------- | -------------------- | ------------ |
  | `"action_…"` (pinned) | 5s (model default)     | 720p (model default) | **295.85**   |
  | `null` (matching off) | adaptive (15s ceiling) | 1080p                | **827.26**   |

  That is **\~2.8×**, and it is duration, resolution and ratio compounding, not duration alone. If you want the exact-frame guarantee at a predictable price, send `video_lora_id: null` **with** an explicit `duration` and `resolution` rather than letting both fall back.
</Warning>

| Request                                                                | What runs                           | `inference_type` |
| ---------------------------------------------------------------------- | ----------------------------------- | ---------------- |
| `first_frame_url` + `video_lora_id: "<id>"`                            | The pinned model drives your still  | `r2v`            |
| `first_frame_url`, `video_lora_id` omitted, a model matches your image | The matched model drives your still | `r2v`            |
| `first_frame_url`, `video_lora_id` omitted, nothing matches            | Plain image-to-video                | `i2v`            |
| `first_frame_url` + `video_lora_id: null`                              | Plain image-to-video, matching off  | `i2v`            |
| `first_frame_url` + `last_frame_url`, no video model                   | Frame interpolation                 | `i2v`            |

```bash cURL theme={null}
curl -X POST https://api.aurous-labs.com/v1/videos \
  -H "X-Api-Key: $AUROUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "prompt": "She turns toward the camera",
    "first_frame_url": "file_01HXMQ7Z3K8Y2VNABCDEFGHJKM",
    "video_lora_id": "action_01HXMQ7Z3K8Y2VNABCDEFGHJKM"
  }'
```

<Note>
  `inference_type: "r2v"` on this path does **not** mean you supplied a reference video. `reference_video_url` stays `null` — the motion reference is built by the platform from the video model you pinned or matched. Read `r2v` as "reference mode", and branch on `reference_video_url` if you need to tell the two apart.
</Note>

A **last** frame cannot combine with a video model: `first_frame_url` + `last_frame_url` + `video_lora_id` returns `400 parameter_invalid_combination` with `param: "last_frame_url"`. Use a first frame alone with the model, or drop the model for first + last interpolation — a first + last pair never auto-matches, so interpolation always runs as plain image-to-video. A subject (`character_id`, `reference_image_urls`, `subjects[]`) still cannot combine with any frame — on this path the still itself is what anchors identity.

### Minimum frame size

A first frame you attach to a video model becomes a short seed clip, and we scale it up to get there — capped, so a frame below the floor cannot be animated at all. It must be **at least 64 pixels on its shortest side** and **at least 6,400 pixels in total**: 80×80 is the smallest square accepted, and at a 64-pixel short side the long side needs to be at least 100. Larger is better.

When you pass a `file_<ulid>` (from `POST /v1/files`), we already know its dimensions, so an undersized frame is rejected up front with [`400 first_frame_too_small`](/errors#first_frame_too_small) and **nothing is held**. When you pass an `https://` URL the dimensions aren't known until the bytes are fetched, so the same rule is enforced during preparation: the generation is accepted, transitions to `failed` with `error_code: "first_frame_too_small"`, and the full hold is refunded. Either way the fix is the same — send a larger frame.

The floor applies only when a video model is in play (pinned, or left to auto-match). It fires on the *possibility* of a model, not on one actually being used — an un-pinned frame is rejected even if no model would have matched your image. With `video_lora_id: null` no seed clip is built, so no upscaling happens and the rule doesn't apply. Nor does it apply when a `last_frame_url` rides alongside — a first + last pair never auto-matches and builds no seed clip either, so no [`first_frame_too_small`](/errors#first_frame_too_small) ever fires on an interpolation request.

## Subject-driven video

Anchor the video's subject across every frame with either:

* **`character_id`** — a saved character (see [Create a character](/api-reference/characters/create-character)). Must be [`status: ready`](/api-reference/characters/retrieve-character) — otherwise `400 character_not_ready`.
* **`reference_image_urls`** — up to 6 ad-hoc reference images, each a `file_<ulid>` or an `https://` URL. Same shape as image generation's `reference_image_urls`.

A subject-driven request still requires a `prompt` describing the action — the subject supplies identity, not motion; it can't substitute for a prompt on its own. A subject-driven generation reports `inference_type: "i2v"` on the response.

<Note>
  `character_id` and `reference_image_urls` are **mutually exclusive** — sending both returns `400 mutually_exclusive_input`. A subject (either form) combined with `first_frame_url` / `last_frame_url` is also rejected, with `400 parameter_invalid_combination`. Pick one of the three modes per request.
</Note>

```bash cURL theme={null}
curl -X POST https://api.aurous-labs.com/v1/videos \
  -H "X-Api-Key: $AUROUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "prompt": "She turns toward the camera and smiles",
    "character_id": "char_01HXMQ7Z3K8Y2VNABCDEFGHJKM"
  }'
```

## Choosing a video model

`GET /v1/video_loras` lists the video models available to pin via `video_lora_id`. The field is tri-state:

* **Pin a model** by passing its catalog `id` or `slug` as `video_lora_id`. A pinned model supplies **three** generation defaults, not just length: `default_duration`, `default_resolution` and `default_ratio`. Anything you send explicitly (`duration`, `resolution`, `ratio`) wins over the model's default; anything you omit is filled from the model. All three feed the price — see [Duration and cost](#duration-and-cost).
* **Omit `video_lora_id`** and the platform picks a suitable model for you. With no first frame, matching reads your **prompt**; **when a first frame is attached, matching is based on what the image shows.** If nothing fits, your request generates as plain video — with a first frame, as plain image-to-video; with a subject attached, as a subject-driven video. Matching never fails a request: there is no "no model matched" error.
* **Send `video_lora_id: null`** to opt out of **image-based** matching. This is meaningful only when `first_frame_url` is attached, where it guarantees plain image-to-video. Without a first frame, `null` behaves exactly like omitting the field — your prompt is still auto-matched. There is no way to disable prompt-based matching; pin a model if you need a specific one. **`null` also opts out of the three defaults above**, so an otherwise-bare request falls back to adaptive duration at 1080p — measured at \~2.8× a pinned generation. Send `duration` and `resolution` alongside it, or see the [cost warning](#start-from-a-still-with-a-video-model).

Treat catalog ids as opaque — don't assume a fixed prefix. Whichever model ends up behind the generation, pinned or auto-matched, is echoed back on the response: `video_lora_id` (`null` for a plain generation) and `video_lora_name` (omitted for a plain generation).

<Note>
  **Estimates don't run matching, and the quote can miss in either direction.** `POST /v1/videos/estimate` prices exactly the request you sent. If you omit `video_lora_id` and a model then matches at create, the generation is priced as a **video-input** generation — and which way the charge moves depends on what you pinned:

  * **You left `duration` and `resolution` to us** — the usual shape for a bare first-frame request. The estimate has no model, so it prices adaptive duration (the 15-second ceiling) at 1080p; the matched model then supplies a *shorter, smaller* default. The quote is an **upper bound that can substantially over-state**. Measured on the first-frame path: `amount` **827.26**, actual settled charge **279.91**.
  * **You pinned `duration` and `resolution` yourself.** Your values win over the model's defaults, so a match can only *add* the motion reference to the price. Here the charge genuinely **exceeds** the quote.

  On an adaptive request, plan against `amount_min`–`amount_max`, not the `amount` headline — that band is much wider than the headline and it contained the real charge in the measurement above. **Pin `video_lora_id` whenever you need the estimate to be exact** — that is the only value that removes matching from the create path entirely. `video_lora_id: null` suppresses matching only when a first frame is attached, so it does not make a prompt-only quote exact. This has always been true of auto-matching; a first frame just gives matching one more thing to read.
</Note>

## Duration and cost

Video cost is `base_per_second × resolution_factor × duration`. `POST /v1/videos/estimate` prices the same request body with no side effects — no character resolution, no reference materialization, no credit hold.

### Adaptive duration (the default)

Pass `duration: -1`, or omit it when no video model is pinned or matched — the model chooses the natural length for your prompt, a whole number of seconds between 4 and 15. You're billed for the **delivered** length, not the ceiling: the 15s ceiling is reserved from your balance up front, and the difference is released once the actual length is known. A team with less than the ceiling available gets `402 balance_too_low` even if the eventual (shorter) length would have fit.

`POST /v1/videos/estimate` reflects this as a range:

```json theme={null}
{
  "object": "estimate",
  "estimated_cost": {
    "amount": 37.5,
    "adaptive": true,
    "amount_min": 10,
    "amount_max": 37.5,
    "breakdown": { "base_per_second": 2.5, "resolution_factor": 1, "duration_s_min": 4, "duration_s_max": 15 }
  },
  "currency": "credit"
}
```

`amount` equals `amount_max` — the ceiling actually held. The real charge, once the generation settles, lands between `amount_min` and `amount_max`.

### Fixed duration

Pass a whole number of seconds, 4–15, to pin an exact length at an exact price:

```json theme={null}
{
  "object": "estimate",
  "estimated_cost": {
    "amount": 12.5,
    "adaptive": false,
    "breakdown": { "base_per_second": 2.5, "resolution_factor": 1, "duration_s": 5 }
  },
  "currency": "credit"
}
```

If you omit `duration` while pinning (or auto-matching) a video model that defines its own `default_duration`, that model's default length applies — a fixed price, not adaptive. Omitting `duration` with no model pinned or matched falls back to adaptive.

The same substitution happens for **`resolution` and `ratio`**: a pinned or matched model supplies `default_resolution` and `default_ratio` for whichever of the two you left out, and both are priced. This is why the same still can settle at very different prices depending only on `video_lora_id` — a model that defaults to 5s / 720p prices roughly a third of the adaptive-15s / 1080p fallback you get with no model at all. Send `duration`, `resolution` and `ratio` explicitly whenever you want the price to be a property of your request rather than of whichever model ends up behind it.

## Audio

`generate_audio` defaults to `true` and is echoed back on the response as `video_generate_audio`. Audio can be declined by a content check independently of the video itself. If a generation fails for this reason, retry with `generate_audio: false` to skip audio generation, or adjust the prompt.

## Output

When the generation reaches `status: succeeded`, fetch the rendered file via `GET /v1/videos/{id}/output`. The output URL is valid for \~24 hours; save what you want to keep. A failed generation never produces output and settles at `cost: {"amount": 0, "refunded": true}` — the reserved hold is released, never charged.

## Webhooks

Register a [webhook endpoint](/webhooks) subscribed to `video.completed` / `video.failed` / `video.cancelled` (`POST /v1/webhook_endpoints`) to receive a POST callback when the video reaches a terminal state. See [Webhooks](/webhooks) for signature verification.

## Errors

| Code                                                                     | HTTP | When                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------------------------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`missing_field`](/errors#missing_field)                                 | 400  | Neither `prompt` nor `first_frame_url` was supplied.                                                                                                                                                                                                                                                                    |
| [`mutually_exclusive_input`](/errors#mutually_exclusive_input)           | 400  | Both `character_id` and `reference_image_urls` supplied.                                                                                                                                                                                                                                                                |
| [`parameter_invalid_combination`](/errors#parameter_invalid_combination) | 400  | A subject (`character_id` / `reference_image_urls`) combined with a frame (`first_frame_url` / `last_frame_url`); `last_frame_url` without `first_frame_url`; or `last_frame_url` combined with a video model (`param: "last_frame_url"` — a **first** frame with a video model is valid).                              |
| [`character_not_ready`](/errors#character_not_ready)                     | 400  | `character_id` resolves but isn't `status: ready`.                                                                                                                                                                                                                                                                      |
| [`first_frame_too_small`](/errors#first_frame_too_small)                 | 400  | A `file_<ulid>` `first_frame_url` below the [minimum frame size](#minimum-frame-size), on a request with a video model pinned or left to auto-match. Rejected before any hold. An `https://` frame fails the same rule asynchronously instead — `status: failed`, `error_code: "first_frame_too_small"`, hold refunded. |
| [`prompt_blocked`](/errors#prompt_blocked)                               | 400  | Prompt rejected by content policy.                                                                                                                                                                                                                                                                                      |
| [`balance_too_low`](/errors#balance_too_low)                             | 402  | Available balance is below the required hold.                                                                                                                                                                                                                                                                           |
| [`resource_not_found`](/errors#resource_not_found)                       | 404  | Unknown or cross-team `character_id`, reference image, or pinned video model.                                                                                                                                                                                                                                           |

See [Errors](/errors) for the complete catalog, retry policy, and the `request_id` support workflow.


## OpenAPI

````yaml POST /v1/videos
openapi: 3.0.0
info:
  title: Aurous Labs API
  description: >-
    Generate AI images with custom LoRA styles.


    ## Authentication

    All requests require an API key passed in the `X-Api-Key` header.

    Create API keys in your
    [dashboard](https://app.aurous-labs.com/dashboard/api-keys).


    ## Closed-beta access gate

    API keys are scoped to a user. If that user's account is not approved for
    the closed beta, every request returns `403` with one of these `error.code`
    values:


    - `account_pending` — awaiting review

    - `account_rejected` — declined post-signup

    - `account_suspended` — was approved, then suspended


    There is no retry — contact support to be approved. The same codes are
    emitted by the WebSocket gateway via 4001 close.


    ## Common headers

    Every response carries `Aurous-Request-Id` (a server-minted `req_<ULID>` for
    support tracing) and `Aurous-Version` (the API version applied to the
    response). Optionally pin a version on the request with `Aurous-Version:
    YYYY-MM-DD` — defaults to your team's pinned version.


    ## Quick Start

    ```bash

    curl -X POST https://api.aurous-labs.com/v1/images \
      -H "X-Api-Key: al_live_your_key" \
      -H "Content-Type: application/json" \
      -d '{"prompt": "A golden sunset over mountains", "lora_id": "your-lora-id", "size": "2k_1_1"}'
    ```
  version: 1.0.0
  contact: {}
servers:
  - url: https://api.aurous-labs.com
    description: Production
  - url: https://api.preprod.aurous-labs.com
    description: Preprod (staging)
security: []
tags:
  - name: Seedance (raw)
    description: >-
      Drop-in raw passthrough for Seedance video generation. Point the official
      Seedance provider SDK at this API's base URL and authenticate with your
      Aurous API key in the `X-Api-Key` header — request bodies are forwarded to
      the provider verbatim and responses come back shape-identical, so you keep
      the provider's exact request/response shapes. Task ids are Aurous-native
      `vid_…` ids. Billing rides response headers, not the body:
      `Aurous-Credits-Held` on the create response and `Aurous-Credits-Charged`
      on a settled, succeeded task read — the body itself stays provider-shaped.
paths:
  /v1/videos:
    post:
      tags:
        - Public API (v1)
      summary: Create a new video generation
      description: >-
        Submits a video generation request. Credits are deducted based on video
        duration × resolution factor. **text-to-video** (`prompt` only),
        **image-to-video** (`first_frame_url`, or
        `first_frame_url`+`last_frame_url`), and **reference mode** (a motion
        reference supplies the movement — either your own clip via
        `reference_video_url`, or one the platform builds; see below) are
        mutually exclusive generation modes. A `first_frame_url` may
        additionally carry a video model — pinned via `video_lora_id`, or
        auto-matched by omitting it — in which case the model supplies the
        motion, the still supplies the person/scene/orientation, and the
        generation reports `inference_type: "r2v"` with `reference_video_url`
        still `null` (the motion reference is built by the platform, not
        supplied by you). The output STARTS NEAR the still — same person, scene
        and orientation — but frame 0 is recomposed, not copied; send
        `video_lora_id: null` for a pixel-exact start via plain image-to-video.
        A `last_frame_url` cannot combine with a video model. Independently, an
        optional **cast** can anchor identity across the video: `subjects[]`
        composes up to 2 people — each a saved character or its own group of 1–4
        reference images, freely mixed — or use the single-subject legacy
        fields: `character_id` (must be `status: ready`) or up to 6 reference
        images via `reference_image_urls`. The two shapes are mutually exclusive
        with each other and with the frame inputs, but a cast MAY accompany a
        `reference_video_url` (the cast anchors identity while the clip supplies
        motion) — except when `video_task` is `extend`, which continues the clip
        as-is and cannot also carry a subject. A cast does not change the price
        of a video. For reference-to-video, `video_task` picks what happens to
        the clip: `reference` (default) generates a new scene borrowing its
        motion, `extend` continues the clip itself in `extend_direction`.
        Optionally add `reference_audio_url` to guide the generated soundtrack —
        it needs a companion (a reference video, a subject, or a pinned
        `video_lora_id`) and audio output enabled. Both reference fields accept
        a `file_<ulid>` from POST /v1/files or an HTTPS URL. `video_lora_id` is
        tri-state — omit it to let the platform pick a suitable model (matching
        reads your prompt, or, when a `first_frame_url` is attached, what the
        IMAGE shows; it falls back to plain video, plain image-to-video, or a
        subject-driven video when none matches, and never fails the request);
        send `null` to opt out of IMAGE-based matching — meaningful only when
        `first_frame_url` is attached, where it guarantees plain image-to-video,
        and otherwise identical to omitting the field (your prompt is still
        auto-matched); supply an id or slug to pin a specific model (not
        accepted with `video_task: "extend"`). A pinned model always wins over
        what the image looks like. 


        **Video models supply pricing defaults — `null` declines them.** A
        pinned or matched model supplies `default_duration`,
        `default_resolution` AND `default_ratio` for whatever you left out
        (anything you send explicitly wins), and all three are priced. With
        `video_lora_id: null` there is no model to supply them, so an
        otherwise-bare first-frame request falls back to adaptive duration
        (billed against the 15 s ceiling) at 1080p — measured at ~2.8× the same
        still with a model pinned (827.26 vs 295.85 credits held). `null` is a
        fidelity choice with a price attached; send `duration` and `resolution`
        alongside it if you want the exact-frame guarantee at a predictable
        cost.


        **Minimum frame size.** A `first_frame_url` that will be driven by a
        video model is upscaled into a short seed clip, so it must be at least
        64 px on its shortest side AND at least 6,400 px in total (80x80 is the
        smallest square accepted). A `file_<ulid>` frame below that is rejected
        with `400 first_frame_too_small` before any credit hold; an `https://`
        frame — whose dimensions are not knowable without fetching the bytes,
        which validation never does — is accepted, then settles at `status:
        failed` with `error_code: "first_frame_too_small"` and a full refund.
        The check fires on the POSSIBILITY of a model, not on one actually being
        used — an un-pinned frame is rejected even if no model would have
        matched your image. The rule does not apply with `video_lora_id: null`
        (no seed clip is built), nor when a `last_frame_url` rides alongside — a
        first+last pair never auto-matches and builds no seed clip either, so no
        `first_frame_too_small` ever fires on an interpolation request.


        Poll `GET /v1/images/{id}` to check status — the generation lookup
        endpoint accepts both `img_*` and `vid_*` IDs — or register a webhook
        endpoint (POST /v1/webhook_endpoints) subscribed to `video.completed` /
        `video.failed` / `video.cancelled` to receive event callbacks.


        Output URLs are valid for approximately 24 hours after generation.
        Download what you want to keep.


        When `duration` is `-1` (adaptive — the default), the 15 s ceiling is
        reserved from your balance and settled to the delivered length; a team
        without ≥ ceiling balance receives `balance_too_low` (402) even if a
        shorter fixed duration would fit.


        **Reference-to-video timing.** When `reference_video_url` (and/or
        `reference_audio_url`) is supplied, the generation stays in `processing`
        while the reference is prepared — typically well under a minute, with a
        10-minute budget. If preparation exceeds the budget or the reference is
        rejected during preparation, the generation transitions to `failed` with
        a full refund and a customer-safe `error_message`; retry with a **new**
        `Idempotency-Key` (the original key would otherwise replay the same
        failed result). A reference with bytes identical to one you already
        submitted skips preparation and resolves immediately.
      operationId: V1VideosController_createVideoGeneration
      parameters:
        - name: Idempotency-Key
          in: header
          description: >-
            Optional key to safely retry a request. Replaying the same key
            within 24h returns the original response with
            `Aurous-Idempotent-Replayed: true` and never creates a second
            generation; reusing a key with a different body returns 409
            `idempotency_key_in_use`. A request that failed validation is not
            cached — retry it with the same key. Same contract as POST
            /v1/images.
          required: false
          schema:
            type: string
        - name: Aurous-Version
          in: header
          required: false
          description: >-
            Optional API version pin (YYYY-MM-DD). Defaults to your team's
            pinned version, or the system default `2026-07-16` for
            unauthenticated requests.
          schema:
            type: string
            example: '2026-07-16'
            pattern: ^\d{4}-\d{2}-\d{2}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateVideoGenerationDto'
      responses:
        '201':
          description: Video generation created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
            Deprecation:
              description: >-
                Present (always the literal string `true`) on a 2xx response to
                POST /v1/images, /v1/images/estimate, /v1/videos or
                /v1/videos/estimate when the request body used the legacy
                `character_id` or `reference_image_urls` field instead of
                `subjects[]`. Absent otherwise. No removal date is set — see the
                linked guide.
              schema:
                type: string
                enum:
                  - 'true'
                example: 'true'
            Link:
              description: >-
                Paired with `Deprecation` — an RFC 8288 link to the subjects[]
                migration guide (`rel="deprecation"`). Same presence rule as
                `Deprecation`.
              schema:
                type: string
                example: >-
                  <https://docs.aurous-labs.com/guides/subjects>;
                  rel="deprecation"; type="text/html"
            Aurous-Idempotent-Replayed:
              description: >-
                Present (literal `true`) when this response was served from a
                stored idempotent replay — the same `Idempotency-Key` +
                canonical body was seen within the 24h window and the original
                response is returned WITHOUT re-executing (no second charge, no
                second task). Absent on the first (fresh) execution and on any
                request sent without an `Idempotency-Key`. Only on the
                idempotency-aware create routes.
              schema:
                type: string
                enum:
                  - 'true'
                example: 'true'
        '400':
          description: >-
            Validation failed, the prompt was rejected by content policy (code:
            prompt_blocked), the two subject shapes are mixed — subjects[] with
            character_id/reference_image_urls — or character_id and
            reference_image_urls are both present (code:
            mutually_exclusive_input), a cast character is not ready (code:
            character_not_ready), or the parameter combination is invalid (code:
            parameter_invalid_combination — e.g. more than 2 subjects, a subject
            element carrying both character_id and image_urls, a duplicate
            character or reference image across subjects, a subject with a
            frame, a subject with video_task: extend, last_frame_url without
            first_frame_url, last_frame_url combined with a pinned video_lora_id
            (param: last_frame_url — a FIRST frame with a video model is valid;
            a first+last pair never auto-matches), reference_video_url combined
            with first_frame_url, video_task without reference_video_url, or
            reference_audio_url without a companion or without audio output). A
            malformed id or a media-purpose file id in an image slot returns
            code: invalid_format. Your reference video/audio couldn't be fetched
            (code: reference_fetch_failed), or the fetched reference failed
            format/duration/resolution validation — including a file id whose
            upload purpose does not match the slot (code:
            reference_media_invalid). A `file_<ulid>` first_frame_url below the
            minimum animatable size — 64 px shortest side AND 6,400 px total —
            returns code: first_frame_too_small (param: first_frame_url) before
            any credit hold, on requests where a video model is pinned or left
            to auto-match.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
        '401':
          description: Unauthorized — missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
        '402':
          description: Insufficient credits
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
        '403':
          description: >-
            Account not approved for closed beta. error.code is one of
            `account_pending`, `account_rejected`, `account_suspended`. There is
            no retry — contact support to be approved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
        '404':
          description: >-
            A pinned video model, character, or reference image was not found or
            is inaccessible (resource_not_found)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
        '409':
          description: Idempotency-Key was reused with a different request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
        '429':
          description: >-
            Rate limit exceeded for this endpoint class. error.code is
            `too_many_requests` (request-count limit; LLM endpoints may instead
            return `tpm_rate_limit_exceeded` or `concurrency_limit_exceeded`).
            Retry after the number of seconds in the `Retry-After` response
            header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
            Retry-After:
              description: >-
                Seconds to wait before retrying. Present on 429 (rate limit) and
                on 503 provider_unavailable. Prefer this over computing
                X-RateLimit-Reset − now.
              schema:
                type: integer
                example: 12
        '503':
          description: >-
            Character generation is temporarily unavailable, or the platform
            could not verify your account/billing state or prepare the request
            (for example, reference images) before dispatch — error.code is
            `provider_unavailable` in every case. Retry after the number of
            seconds in the `Retry-After` header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          headers:
            Aurous-Request-Id:
              description: Server-minted request id. Quote this in support tickets.
              schema:
                type: string
                example: req_01HXMQ7Z3K8Y2NABCDEFGHJKMP
            Aurous-Version:
              description: API version pin applied to this response (YYYY-MM-DD).
              schema:
                type: string
                example: '2026-07-16'
            X-RateLimit-Limit:
              description: Bucket capacity (max tokens) for this endpoint class.
              schema:
                type: integer
                example: 120
            X-RateLimit-Remaining:
              description: Tokens remaining after this request.
              schema:
                type: integer
                example: 119
            X-RateLimit-Reset:
              description: >-
                Epoch seconds when the bucket would be full again, assuming no
                further requests.
              schema:
                type: integer
                example: 1700000060
            Retry-After:
              description: >-
                Seconds to wait before retrying. Present on 429 (rate limit) and
                on 503 provider_unavailable. Prefer this over computing
                X-RateLimit-Reset − now.
              schema:
                type: integer
                example: 12
      security:
        - api-key: []
components:
  schemas:
    CreateVideoGenerationDto:
      type: object
      properties:
        prompt:
          type: string
          description: >-
            Text prompt describing the video. Required unless first_frame_url is
            provided (image-to-video). Subject-driven generation (character_id
            or reference_image_urls) still requires a prompt describing the
            action — a subject alone cannot substitute for one.
          example: A golden sunset over the ocean with gentle waves
        video_lora_id:
          type: string
          nullable: true
          description: >-
            Optional, and TRI-STATE. An id (or slug, where one is set) from GET
            /v1/video_loras pins a specific video model. If OMITTED, the
            platform picks a suitable model for you: matching reads your prompt,
            or — when `first_frame_url` is attached — what the IMAGE shows. If
            none matches, your request is generated as plain video, plain
            image-to-video, or (with a subject attached) a subject-driven video;
            matching never fails the request. Send NULL to opt out of
            IMAGE-based matching — meaningful only when `first_frame_url` is
            attached, where it guarantees plain image-to-video. WITHOUT a first
            frame, `null` behaves exactly like omitting the field and your
            prompt is still auto-matched; pin an id if you need a specific
            model. A first frame plus a pinned or matched model reports
            `inference_type: "r2v"` and starts NEAR your image rather than
            exactly on it. PRICING: a pinned or matched model also supplies
            `default_resolution` and `default_ratio` alongside
            `default_duration` for whatever you left out (anything you send
            explicitly wins), and all three are priced — so `null` on an
            otherwise-bare first-frame request falls back to adaptive duration
            at 1080p and costs ~2.8x the same still with a model pinned. Send
            `duration` and `resolution` alongside `null` if you want the
            exact-frame guarantee at a predictable price.
          example: action_01HXMQ7Z3K8Y2VNABCDEFGHJKM
        character_id:
          type: string
          description: >-
            Drive the video from a saved character (reference-to-video). Must be
            `status: ready` — otherwise 400 `character_not_ready`. Cross-team
            ids 404 (existence never leaked). Mutually exclusive with
            `reference_image_urls` (→ 400 `mutually_exclusive_input`) and with
            `first_frame_url`/`last_frame_url` (→ 400
            `parameter_invalid_combination`). Omit both a subject and a frame
            for plain text-to-video. Superseded by `subjects[]` (which also
            supports multi-person casts); fully supported — successful responses
            carry an advisory `Deprecation: true` header when this field is
            used.
          example: char_01HXMQ7Z3K8Y2VNABCDEFGHJKM
          deprecated: true
        reference_image_urls:
          description: >-
            Up to 6 subject reference images — each a `file_<ulid>` (POST
            /v1/files) or an `https://` URL (SSRF-pinned server-side fetch).
            Image files only: a `file_<ulid>` uploaded with purpose
            `reference_video`/`reference_audio` is rejected (400
            `invalid_format`). The subject is anchored across the video.
            Mutually exclusive with `character_id` and with
            `first_frame_url`/`last_frame_url`. Superseded by `subjects[]`
            (which also supports multi-person casts); fully supported —
            successful responses carry an advisory `Deprecation: true` header
            when this field is used.
          example:
            - file_01HXMQ7Z3K8Y2NABCDEFGHJKMN
            - https://example.com/ref2.jpg
          minItems: 0
          maxItems: 6
          deprecated: true
          type: array
          items:
            type: string
        subjects:
          maxItems: 2
          description: >-
            Ordered cast for a multi-person video. Up to 2 subjects per
            generation (current engine limit — may increase; exceeding it
            returns 400 parameter_invalid_combination). Each subject is a saved
            character or its own group of 1–4 reference images; character and
            reference subjects can be mixed. Supersedes the single-subject
            `character_id`/`reference_image_urls` fields — sending both shapes
            returns 400 mutually_exclusive_input. A cast may accompany
            `reference_video_url` (the clip supplies motion, the cast supplies
            identity) but not `video_task: "extend"` and not
            `first_frame_url`/`last_frame_url`. Duplicate characters or a
            reference image reused across subjects return 400. Empty array is
            treated as omitted.
          type: array
          items:
            $ref: '#/components/schemas/VideoSubjectDto'
        first_frame_url:
          type: string
          description: >-
            First frame for image-to-video / first+last frame interpolation.
            Either an opaque `file_<ulid>` ID returned by POST /v1/files, or an
            `https://` URL pointing at a public host (max 2048 chars). Image
            files only — a `file_<ulid>` uploaded with purpose
            `reference_video`/`reference_audio` is rejected (400
            `invalid_format`). URLs are server-side fetched through an
            SSRF-pinned client (rejects private IPs / cloud metadata). MINIMUM
            SIZE — when a video model is pinned or left to auto-match, this
            frame is upscaled into a short seed clip, so it must be at least 64
            px on its shortest side AND at least 6,400 px in total (80x80 is the
            smallest square accepted; at a 64 px short side the long side needs
            100). A `file_<ulid>` below that returns 400 `first_frame_too_small`
            before any credit hold (dimensions were measured at upload); an
            `https://` URL is accepted and settles at `status: failed` with
            `error_code: "first_frame_too_small"` and a full refund, because
            validation never fetches the bytes. The check runs on the
            POSSIBILITY of a model, not on one actually being used — an
            un-pinned frame is rejected even if no model would have matched your
            image. No minimum applies with `video_lora_id: null`, nor when a
            `last_frame_url` rides alongside — a first+last pair never
            auto-matches and builds no seed clip either, so no
            `first_frame_too_small` ever fires on an interpolation request.
          example: file_01HXMQ7Z3K8Y2NABCDEFGHJKMN
          maxLength: 2048
        last_frame_url:
          type: string
          description: >-
            Last frame for first+last frame interpolation. Same rules as
            first_frame_url. Requires `first_frame_url` — sending
            `last_frame_url` alone returns 400 with code
            `parameter_invalid_combination`.
          example: https://example.com/last.jpg
          maxLength: 2048
        resolution:
          type: string
          description: Output video resolution
          enum:
            - 480p
            - 720p
            - 1080p
        ratio:
          type: string
          description: Output video aspect ratio
          enum:
            - '16:9'
            - '4:3'
            - '1:1'
            - '3:4'
            - '9:16'
            - '21:9'
            - adaptive
        duration:
          type: number
          description: >-
            Video length in seconds. Pass -1 for ADAPTIVE (default): the model
            selects the optimal whole-second length within its supported range
            (currently 4–15 s) and you are billed for the delivered length — a
            VARIABLE price. Call POST /v1/videos/estimate to see the min–max
            range before submitting. Pass a whole number 4–15 to pin a fixed
            length (fixed price). Omitting uses the video model's default length
            when your request pins or is matched to one; otherwise adaptive
            (-1). Adaptive holds the 15 s ceiling from your balance until it
            settles.
          enum:
            - -1
            - 4
            - 5
            - 6
            - 7
            - 8
            - 9
            - 10
            - 11
            - 12
            - 13
            - 14
            - 15
          default: -1
          example: 5
        generate_audio:
          type: boolean
          description: >-
            Whether the generated video includes synchronized audio. Defaults to
            the selected video model's audio setting (currently true for all
            models unless noted on the model).
        watermark:
          type: boolean
          description: Whether to add watermark
        enhance_prompt:
          type: boolean
          description: Whether to enhance the prompt with AI
        reference_video_url:
          type: string
          description: >-
            Your reference video — the motion source for a reference-mode
            generation, or the clip to continue when `video_task` is `extend`.
            Either an opaque `file_<ulid>` returned by POST /v1/files (uploaded
            with `purpose: reference_video` — format/duration/resolution were
            already validated at upload), or an HTTPS URL. Requirements: MP4 or
            MOV (H.264/H.265), 2–15 seconds, up to 50 MB, frame area between
            409,600 px (≈640×640) and 2,073,600 px (1920×1080, ≈1080p). URLs
            must be publicly fetchable or pre-signed, serve HTTPS directly
            (redirects are refused — pass the final URL), and respond within the
            fetch budget. Cannot be combined with
            `first_frame_url`/`last_frame_url`. While the reference is prepared
            the generation stays in `processing` — see the endpoint description
            for timing.
          example: https://cdn.example.com/clips/dance-loop.mp4
          maxLength: 2048
        reference_audio_url:
          type: string
          description: >-
            An audio reference (voice, music, or ambience) to guide the
            generated soundtrack — either a `file_<ulid>` from POST /v1/files
            (`purpose: reference_audio`) or an HTTPS URL. WAV or MP3, 2–15
            seconds, up to 15 MB. Needs a companion — a reference video, a
            subject (`subjects[]` or the legacy fields), or a pinned
            `video_lora_id` — and requires audio output (`generate_audio` must
            not be false). Same URL rules as `reference_video_url` (public or
            signed HTTPS, no redirects).
          example: https://cdn.example.com/audio/voiceover.mp3
          maxLength: 2048
        video_task:
          type: string
          description: >-
            What to do with `reference_video_url`. `reference` (default):
            generate a new scene that borrows the clip's motion. `extend`:
            continue the clip itself in `extend_direction`; extend takes the
            clip as-is, so it cannot be combined with `video_lora_id`. Only
            valid when `reference_video_url` is present.
          enum:
            - reference
            - extend
          default: reference
        extend_direction:
          type: string
          description: >-
            Direction to continue the clip when `video_task` is `extend`.
            Defaults to `forward`. Only valid with `video_task: "extend"`.
          enum:
            - forward
            - backward
          default: forward
    GenerationResponse:
      type: object
      properties:
        object:
          type: string
          description: >-
            Discriminator — always `inference`. Mirrors OpenAI's `object`-field
            convention so SDK clients can branch on the resource type without
            inspecting the ID prefix. A single canonical value (`inference`)
            covers both image and video generations; use `media_type` to
            distinguish the rendering kind.
          example: inference
          enum:
            - inference
          default: inference
        id:
          type: string
          description: Opaque generation ID
          example: img_01HXMQ7Z3K8Y2VNABCDEFGHJKM
        status:
          type: string
          description: >-
            Current generation status. Lifecycle: `pending` (created, awaiting
            dispatch) → `processing` (running) → one of the terminal values
            `succeeded` / `failed` / `cancelled`. Additional terminal values may
            be introduced in future API versions and will be announced via the
            changelog before they appear on the wire.
          example: succeeded
          enum:
            - pending
            - processing
            - succeeded
            - failed
            - cancelled
        media_type:
          type: string
          description: >-
            Distinguishes image vs video generation. May be null for older rows
            minted before this column existed.
          example: image
          enum:
            - image
            - video
          nullable: true
        prompt:
          type: string
          description: The text prompt used for generation
          example: A golden sunset over mountains, cinematic lighting
        output_urls:
          description: >-
            Generated image proxy URLs. Each URL is anonymous-read (no auth
            header required) and edge-cached for 24 hours. Available for ~24
            hours after generation. Save what you want to keep — long-term
            storage is intentionally not part of the platform. URLs return 410
            Gone after expiry.
          example:
            - >-
              https://api.aurous-labs.com/v1/images/img_01HXMQ7Z3K8Y2VNABCDEFGHJKM/output/0
          nullable: true
          type: array
          items:
            type: string
        video_url:
          type: string
          description: >-
            Generated video proxy URL (only present on `media_type: video`).
            Same 24h TTL as image output_urls.
          example: >-
            https://api.aurous-labs.com/v1/videos/vid_01HXMQ7Z3K8Y2VNABCDEFGHJKM/output?token=...
          nullable: true
        reference_image_urls:
          description: >-
            The reference image URLs you supplied as visual anchors for this
            generation, echoed back (snapshotted at inference time). Present
            only when the generation was driven by your own reference images
            (`reference_image_urls` or `reference` subjects). Omitted entirely
            for character-driven generations — a character's reference images
            are managed platform assets and are never echoed.
          example:
            - https://example.com/ref1.jpg
          nullable: true
          type: array
          items:
            type: string
        error_message:
          type: string
          description: >-
            Human-readable error message if the generation failed.
            Non-contractual prose — do not parse or match on this value. Switch
            on error_code instead.
          nullable: true
          example: Content policy violation
        error_code:
          type: string
          description: >-
            Machine-readable failure reason when `status` is `failed`, for the
            cases where you need to branch in code. `null` on every successful
            generation, on failures recorded before this field existed, and on
            failure paths that have no stable code — `error_message` is
            human-facing copy that is re-tuned over time, so never pattern-match
            it. Currently emitted: `generation_interrupted`,
            `reference_preparation_failed`, `content_filtered`,
            `generation_failed`, and `first_frame_too_small` (an `https://`
            `first_frame_url` attached to a video model whose dimensions were
            below the minimum — a `file_<ulid>` frame is rejected with the same
            code as a `400` before any credits are held; see [Errors](/errors)).
            The set is additive: new codes may appear, so treat an unrecognized
            value as a generic failure, keep a default branch in your switch
            statement, and fall back to `error_message`.
          nullable: true
          example: generation_interrupted
        duration_ms:
          type: integer
          description: Processing duration in milliseconds (set on terminal status)
          example: 14820
          nullable: true
        cost:
          description: >-
            Per-generation cost breakdown — same shape as the `estimated_cost`
            returned by `POST /v1/{images,videos}/estimate`. May be `null` for
            older rows from before this field existed; populated for all new
            generations. The `amount` reflects the committed charge for
            terminal-status rows — `0` (with `refunded: true`) on a `failed`
            generation, since the reserved hold was released, never charged.
          nullable: true
          example:
            amount: 2
            currency: credit
            breakdown:
              base: 1
              enhance: 1
          allOf:
            - $ref: '#/components/schemas/GenerationCost'
        width:
          type: number
          description: >-
            Resolved output image width in pixels (image generations only).
            Reflects the post-snap dimension actually generated; may differ from
            a custom-requested `width` by up to 31 px due to multiple-of-32
            snapping.
          example: 2048
        height:
          type: number
          description: >-
            Resolved output image height in pixels (image generations only).
            Reflects the post-snap dimension actually generated; may differ from
            a custom-requested `height` by up to 31 px due to multiple-of-32
            snapping.
          example: 2048
        image_count:
          type: number
          description: >-
            Number of images in the batch. Reflects the requested `count` while
            the generation is running; on a terminal status it reflects the
            number actually DELIVERED — when part of a batch fails you receive
            the successful images, this count re-stamps to match, and the
            difference is refunded automatically.
          example: 1
        size_preset:
          type: string
          description: >-
            Named size preset applied to this generation. `null` when the
            request used custom `width`/`height` instead of a preset.
          example: 2k_1_1
          enum:
            - 2k_1_1
            - 2k_3_2
            - 2k_2_3
            - 2k_4_3
            - 2k_3_4
            - 2k_16_9
            - 2k_9_16
            - 2k_21_9
            - 4k_1_1
            - 4k_3_2
            - 4k_2_3
            - 4k_4_3
            - 4k_3_4
            - 4k_16_9
            - 4k_9_16
            - 4k_21_9
          nullable: true
        inference_type:
          type: string
          description: >-
            Inference mode dispatched. Images: `t2i` (text-to-image) — reference
            images and characters are supplementary inputs to the t2i flow, not
            a separate mode. Videos (this list also returns `vid_*` rows): `t2v`
            (text-to-video), `i2v` (image-to-video — frame-driven,
            subject-driven, or a pinned video model on its own), or `r2v`
            (reference mode — the generation was driven by a motion reference).
            `r2v` does NOT imply you supplied that reference: it covers BOTH
            your own clip sent as `reference_video_url` AND a platform-built
            reference, produced when a first frame rides with a video model
            (pinned via `video_lora_id`, or auto-matched from the image). On the
            platform-built variant `reference_video_url` is `null` — branch on
            that field, not on `inference_type`, to tell the two apart. `i2i` is
            reserved for a future image-edit endpoint and is not currently
            emitted.
          example: t2i
          enum:
            - t2i
            - t2v
            - i2v
            - r2v
            - i2i
          nullable: true
        cfg_rescale:
          type: number
          description: >-
            CFG rescale factor the customer supplied on the request body, echoed
            back here. Range 0.0-1.0. Omitted when the customer did not supply a
            per-request value (the platform applied a precedence-chain default —
            LoRA, character override, or the global 0.7 — which is not exposed
            on the response).
          example: 0.7
          minimum: 0
          maximum: 1
          nullable: true
        denoise_strength:
          type: number
          description: >-
            Denoising strength the customer supplied on the request body, echoed
            back here. Range 0.0-1.0. Omitted when the customer did not supply a
            value or when the generation was a bare text-to-image request
            (denoise is only applied when reference images or a character are
            attached).
          example: 0.6
          minimum: 0
          maximum: 1
          nullable: true
        seed:
          type: integer
          description: >-
            The random seed the model actually used for this image generation.
            Populated even when you omit `seed` on the request — the platform
            requests a random seed and records the concrete value the provider
            rolled, so you can reproduce the result by passing it back as
            `seed`. Available once `status` is `succeeded`; `null` before then
            and for `failed`/`cancelled` generations. For multi-image batches
            (`image_count` > 1) this is the seed of the first image
            (`output_urls[0]`); per-image seeds are not yet exposed. Image
            generations only.
          example: 819572108
          nullable: true
        video_duration:
          type: integer
          description: Video duration in seconds (video generations only)
          example: 5
          nullable: true
        video_resolution:
          type: string
          description: Video resolution (video generations only)
          example: 480p
          enum:
            - 480p
            - 720p
            - 1080p
          nullable: true
        video_ratio:
          type: string
          description: Video aspect ratio (video generations only)
          example: '16:9'
          enum:
            - '16:9'
            - '4:3'
            - '1:1'
            - '3:4'
            - '9:16'
            - '21:9'
            - adaptive
          nullable: true
        video_generate_audio:
          type: boolean
          description: >-
            Whether the generated video includes synchronized audio (video
            generations only). Echoes the request `generate_audio` (default
            true).
          example: true
          nullable: true
        video_task:
          type: string
          description: >-
            Resolved reference-video task (video generations only). `reference`:
            the generation borrows the clip's motion for a new scene. `extend`:
            the generation continues the clip itself. `null` on every generation
            that did not supply `reference_video_url` (including all
            pre-existing rows). Additional task types may be introduced in
            future API versions — treat an unrecognized value as opaque.
          example: reference
          enum:
            - reference
            - extend
          nullable: true
        extend_direction:
          type: string
          description: >-
            Resolved extend direction (video generations only). Non-null only
            when `video_task` is `extend`; `null` otherwise (including every
            reference-mode and non-reference generation).
          example: forward
          enum:
            - forward
            - backward
          nullable: true
        reference_video_url:
          type: string
          description: >-
            The reference video URL you submitted, echoed back VERBATIM as you
            sent it — never re-signed, never a storage path. `null` for
            generations that did not supply `reference_video_url` (including
            every dashboard-originated row, and including an `inference_type:
            "r2v"` generation whose motion reference the platform built from a
            first frame plus a pinned or matched video model — this field, not
            `inference_type`, is what distinguishes your own clip from a
            platform-built reference).
          example: https://cdn.example.com/clips/dance-loop.mp4
          nullable: true
        reference_audio_url:
          type: string
          description: >-
            The reference audio URL you submitted, echoed back VERBATIM as you
            sent it. `null` for generations that did not supply
            `reference_audio_url`.
          example: https://cdn.example.com/audio/voiceover.mp3
          nullable: true
        video_lora_id:
          type: string
          description: >-
            The video model (an id or slug from GET /v1/video_loras) behind this
            generation (video generations only) — either the `video_lora_id` you
            pinned on the request, or the model the platform auto-matched to
            your prompt when none was pinned. `null` for a plain (no video
            model) generation, and always `null` on image generations.
          example: lora_01HXMQ7Z3K8Y2VNABCDEFGHJKM
          nullable: true
        video_lora_name:
          type: string
          description: >-
            Display name of the video model in `video_lora_id`, if any. Omitted
            for plain generations (no pinned or matched video model).
          example: Cinematic Pan
          nullable: true
        character_id:
          type: string
          description: >-
            Character ID supplied on the request (`char_<ulid>` or legacy UUID),
            echoed back. `null` when no character was attached to this
            generation.
          example: char_01HXMQ7Z3K8Y2VNABCDEFGHJKM
          nullable: true
        subjects:
          description: >-
            Ordered subjects composed into this generation (positional — entry N
            echoes entry N of the identity inputs sent on create; legacy
            `character_id` / `reference_image_urls` inputs are normalized into
            the same projection). Character entries carry the opaque
            `char_<ulid>`; reference entries echo only their image count, never
            URLs. Video generations echo their cast the same way (up to 2
            entries), matching the `subjects[]` array accepted by POST
            /v1/videos. Character entries on generations created before cast
            snapshotting may carry `character_id: null`; the id is never
            substituted with an internal identifier. `null` for generations that
            did not compose subjects (plain prompt-only or style generations,
            and context-image generations). The engine is not exposed.
          nullable: true
          example:
            - type: character
              character_id: char_01HXMQ7Z3K8Y2VNABCDEFGHJKM
              image_count: 1
            - type: reference
              character_id: null
              image_count: 2
          type: array
          items:
            $ref: '#/components/schemas/SubjectEcho'
        context_images:
          description: >-
            Count-only echo of the `context_images[]` sent on create — the
            number of loose reference images supplied, never the image URLs
            themselves. `null` for every generation that did not use
            `context_images` (plain prompt-only, style, subject, and video
            generations).
          nullable: true
          example:
            image_count: 3
          allOf:
            - $ref: '#/components/schemas/ContextImagesEcho'
        action:
          description: >-
            The composition act applied to this generation, echoed as `{ id,
            name }`. An act is applied either because you pinned it with
            `action_id` or because act detection matched one automatically. `id`
            is the act identifier from `GET /v1/actions`; `name` is its display
            name (or `null` when the name was not recorded). `null` for every
            generation that did not apply an act — plain prompt-only, style,
            context-image, and video generations. A new, always-present,
            nullable key: existing integrations that do not read it are
            unaffected.
          nullable: true
          example:
            id: 3f2b6c1e-8a4d-4e2b-9c7a-1d5e8f0a2b3c
            name: Over-the-shoulder
          allOf:
            - $ref: '#/components/schemas/ActionEcho'
        style:
          description: >-
            The prompt style applied to this generation, echoed as `{ id, name
            }`. A style is applied either because you pinned it with `lora_id`
            or because style matching resolved one from your prompt — the echo
            always reflects the style that actually ran (a retired id that
            aliases to a successor echoes the successor). `id` is the `lora_*`
            identifier from `GET /v1/loras` and round-trips into `lora_id`.
            `null` for every generation without a style — including `lora_id:
            null` requests, retired styles that generate plain, video
            generations, and all rows minted before styles shipped. A new,
            always-present, nullable key: existing integrations that do not read
            it are unaffected.
          nullable: true
          example:
            id: lora_06AAAAAAAAAAAAAAAAAAAAAAAA
            name: Ring-Light Creator
          allOf:
            - $ref: '#/components/schemas/StyleEcho'
        warnings:
          description: >-
            Non-fatal request adjustments, present ONLY on the `POST /v1/images`
            201 body (and the estimate response) — never on GET reads, list
            rows, or webhook payloads. Omitted entirely when empty. Current
            codes: `parameter_ignored`, `style_retired_plain`; new codes may be
            added without a version bump — ignore unknown codes. Idempotent
            replays return the original warnings verbatim.
          example:
            - code: style_retired_plain
              param: lora_id
              message: >-
                Style lora_06AAAAAAAAAAAAAAAAAAAAAAAA is retired and no longer
                applies a style — this request generates without one.
          type: array
          items:
            $ref: '#/components/schemas/WarningEntry'
        enhancer_outcome:
          type: string
          description: >-
            Outcome of the prompt-optimization step, echoed for observability.
            Current values: `no_request` (the request did not invoke it),
            `succeeded`, `identical`, `refused`, `transport_error`,
            `length_overflow`; `null` on rows minted before this field existed.
            INFORMATIONAL and deliberately an OPEN set (no schema enum — codegen
            clients must not mint a closed union): do not branch control flow on
            it; new values may be added without a version bump.
          example: succeeded
          nullable: true
        loras:
          description: >-
            LoRAs applied to this generation. `null` for prompt-only and
            pure-reference generations.
          nullable: true
          type: array
          items:
            $ref: '#/components/schemas/GenerationLoraEntry'
        aurous_version:
          type: string
          description: >-
            API contract version applied at the time this row was minted (D25 —
            frozen for replay across future version bumps).
          example: '2026-07-16'
        creation_request_id:
          type: string
          description: >-
            Aurous-Request-Id of the POST that created this row. Quote in
            support tickets to trace the original create request.
          example: req_01HXMQ7Z3K8Y2VNABCDEFGHJKM
        created_at:
          type: string
          description: Creation timestamp (ISO 8601)
          example: '2026-05-04T10:00:00Z'
        completed_at:
          type: string
          description: >-
            Terminal-status timestamp (ISO 8601). NULL until the generation
            reaches a terminal state.
          example: '2026-05-04T10:00:14Z'
          nullable: true
      required:
        - object
        - id
        - status
        - prompt
        - created_at
    ErrorResponse:
      type: object
      properties:
        error:
          description: Error payload
          allOf:
            - $ref: '#/components/schemas/ErrorPayload'
      required:
        - error
    VideoSubjectDto:
      type: object
      properties:
        type:
          type: string
          enum:
            - character
            - reference
          example: character
          description: Subject kind.
        character_id:
          type: string
          example: char_01HXMQ7Z3K8Y2VNABCDEFGHJKM
          description: >-
            Required for `character` subjects (and must be omitted for
            `reference` subjects — 400 `parameter_invalid_combination`).
            `char_<ulid>` or legacy UUID; must be a ready character your team
            owns.
        image_urls:
          minItems: 1
          maxItems: 4
          example:
            - file_01HXMQ7Z3K8Y2NABCDEFGHJKMN
          description: >-
            Required for `reference` subjects (and must be omitted for
            `character` subjects — 400 `parameter_invalid_combination`). 1–4
            `file_<ulid>` IDs (POST /v1/files) or https URLs. Image files only:
            a `file_<ulid>` uploaded with purpose
            `reference_video`/`reference_audio` is rejected (400
            `invalid_format`). The clearest full-body and portrait views are
            selected automatically to anchor this person across the video.
          type: array
          items:
            type: string
      required:
        - type
    GenerationCost:
      type: object
      properties:
        amount:
          type: number
          description: >-
            Total credit cost charged for this generation. For images this is
            the sum of `breakdown` values. For videos this is the token-based
            charge (not a sum of breakdown values); trust `amount` as the
            authoritative figure.
          example: 2
        currency:
          type: string
          description: Currency unit. Always `credit` at v1.0.
          example: credit
          enum:
            - credit
        breakdown:
          type: object
          description: >-
            Cost breakdown components, keyed by source. Image keys: `base`
            (always) and `enhance` (only when `enhance_prompt: true`); refs are
            free and do not add line items. When the team has a negotiated
            discount, an additional `discount_factor` line is present
            (multiplier, not a credit amount). For VIDEO the breakdown keys
            shift across the lifecycle. At HOLD time (the create 201, and while
            `pending`/`processing`) it carries `pricing_model` (always
            `video_tokens_v1`), `tokens_max` (the CEILING token count the hold
            was sized at), `credits_held` (the reserved credits), `video_input`
            (boolean), `resolution`, plus optional
            `enhance_flat_credits`/`discount_factor`. Once the video SETTLES
            (`status: succeeded`) the breakdown carries the ACTUAL figures
            instead: `tokens` (the realized token count — a DISTINCT key from
            the hold-time `tokens_max`, not a rename of it), `credits` (the
            settled charge), `credits_held` (the original ceiling), and
            `settled: true`. Video breakdown is NOT additive — `amount` is the
            authoritative charge, computed from the token count and per-model
            rate table. On a `refunded` generation, `breakdown` still reflects
            the original projection (what the attempt was priced at) even though
            `amount` is `0` — compare the two to see what you were NOT charged
            for. Values are typed `number | string | boolean`: video metadata
            like `pricing_model`/`resolution` are strings and
            `video_input`/`settled` are booleans, so read by name and ignore
            unknown keys (the field set is OPEN).
          example:
            pricing_model: video_tokens_v1
            tokens: 108000
            credits: 42
            credits_held: 51.75
            video_input: false
            resolution: 720p
            settled: true
        refunded:
          type: boolean
          description: >-
            True when the reserved credit hold for this generation was released
            without any charge — `amount` is `0` in that case, not the amount
            originally held. Set only on `failed` generations. Omitted (absent)
            on `succeeded`, `pending`, and `processing` rows.
          example: true
      required:
        - amount
        - currency
        - breakdown
    SubjectEcho:
      type: object
      properties:
        type:
          type: string
          description: >-
            Subject kind. `character` entries reference a saved character;
            `reference` entries were supplied as ad-hoc reference images.
          enum:
            - character
            - reference
          example: character
        character_id:
          type: string
          description: >-
            Opaque character ID (`char_<ulid>`) for `character` subjects — the
            same ID accepted by `subjects[].character_id` on create. `null` for
            `reference` subjects.
          example: char_01HXMQ7Z3K8Y2VNABCDEFGHJKM
          nullable: true
        image_count:
          type: integer
          description: Number of input images this subject contributed to the composition.
          example: 1
      required:
        - type
    ContextImagesEcho:
      type: object
      properties:
        image_count:
          type: integer
          description: Number of context images supplied on the request.
          example: 3
      required:
        - image_count
    ActionEcho:
      type: object
      properties:
        id:
          type: string
          description: >-
            Composition-act identifier — the same id returned by `GET
            /v1/actions` and accepted by `action_id` on `POST /v1/images`.
          example: 3f2b6c1e-8a4d-4e2b-9c7a-1d5e8f0a2b3c
          format: uuid
        name:
          type: string
          description: >-
            Display name of the applied act. `null` when the name was not
            recorded on the row.
          example: Over-the-shoulder
          nullable: true
      required:
        - id
    StyleEcho:
      type: object
      properties:
        id:
          type: string
          description: >-
            Style identifier — the same `lora_*` id returned by `GET /v1/loras`
            and accepted by `lora_id` on `POST /v1/images`. Pass it back as
            `lora_id` to reuse the style. `null` only on degenerate legacy rows.
          example: lora_06AAAAAAAAAAAAAAAAAAAAAAAA
          nullable: true
        name:
          type: string
          description: >-
            Display name of the applied style. `null` when the name was not
            recorded on the row.
          example: Ring-Light Creator
          nullable: true
      required:
        - id
    WarningEntry:
      type: object
      properties:
        code:
          type: string
          description: >-
            Machine-readable warning code. Current values: `parameter_ignored`
            (a parameter you sent has no effect on this generation path and was
            ignored) and `style_retired_plain` (the pinned `lora_id` is retired
            — the generation proceeded without a style). This is an OPEN set:
            new codes may be added without a version bump, so ignore unknown
            codes rather than failing on them.
          example: style_retired_plain
        param:
          type: string
          description: >-
            Machine-readable name of the request field the warning is about
            (e.g. `lora_id`, `seed`).
          example: lora_id
        message:
          type: string
          description: >-
            Human-readable explanation. Wording may change; branch on
            `code`/`param`, not on this text.
          example: >-
            Style lora_06AAAAAAAAAAAAAAAAAAAAAAAA is retired and no longer
            applies a style — this request generates without one.
      required:
        - code
        - param
        - message
    GenerationLoraEntry:
      type: object
      properties:
        id:
          type: string
          description: Opaque LoRA ID resolved at dispatch time
          example: lora_01HXMQ7Z3K8Y2VNABCDEFGHJKM
        name:
          type: string
          description: LoRA display name
          example: Sunset Style
    ErrorPayload:
      type: object
      properties:
        type:
          type: string
          description: Broad error category
          example: invalid_request
          enum:
            - invalid_request
            - authentication
            - not_found
            - rate_limit
            - server_error
        code:
          type: string
          description: >-
            Stable error code (programmatic discriminator). Closed-beta gate
            emits one of `account_pending`, `account_rejected`,
            `account_suspended` on 403.
          example: balance_too_low
          enum:
            - invalid_request
            - missing_field
            - invalid_format
            - value_out_of_range
            - unsupported_lora_for_mode
            - generation_not_cancellable
            - prompt_blocked
            - reference_blocked
            - output_moderation_rejected
            - unknown_version
            - mutually_exclusive_input
            - character_not_ready
            - parameter_invalid_combination
            - style_retired
            - parameter_invalid
            - too_many_reference_images
            - action_not_available
            - upload_invalid
            - balance_too_low
            - idempotency_key_in_use
            - api_key_not_found
            - payload_too_large
            - missing_api_key
            - invalid_api_key
            - revoked_api_key
            - resource_not_found
            - forbidden_resource
            - account_pending
            - account_rejected
            - account_suspended
            - already_approved
            - already_rejected
            - already_suspended
            - invalid_reinstate_target
            - invalid_suspend_target
            - cannot_moderate_admin
            - too_many_requests
            - concurrency_limit_exceeded
            - tpm_rate_limit_exceeded
            - internal_error
            - provider_unavailable
            - provider_timeout
            - provider_not_configured
            - invalid_time_range
            - invalid_bucket_width
            - too_many_buckets
            - too_many_group_by
            - invalid_filter
            - invalid_page_token
            - export_too_large
            - user_already_exists
            - self_invite_forbidden
            - invite_link_failed
            - invite_rate_limited
            - model_not_found
            - model_disabled
            - model_wrong_kind
            - max_tokens_exceeds_hard_cap
            - chat_model_misconfigured
            - embeddings_input_too_large
            - embeddings_unsupported_dimensions
            - pricing_frozen
            - provider_rate_limited
            - chat_provider_request_invalid
            - chat_provider_auth_failed
            - chat_provider_unavailable
            - max_input_tokens_exceeded
            - chat_provider_unknown_error
            - embeddings_provider_unknown_error
            - embeddings_batch_not_supported
            - embeddings_input_too_many_items
            - embeddings_video_unsupported
            - encoding_format_unsupported
            - missing_max_tokens_no_model_default
            - chat_cancel_target_not_found
            - chat_completion_not_found
            - chat_cancel_target_already_terminal
            - chat_cancel_target_not_cancellable
            - tool_choice_required_unsupported
            - response_format_too_large
            - response_format_too_deep
            - invalid_cursor
            - invalid_cursor_for_endpoint
            - model_slug_exists
            - output_expired
            - output_not_available
            - content_filtered
            - image_generation_failed
            - reference_media_invalid
            - reference_media_cap_reached
            - reference_fetch_failed
            - unsupported_auth_method
            - insufficient_scope
            - uploads_expired
            - provider_unknown_error
            - first_frame_too_small
        message:
          type: string
          description: Human-readable message
          example: Team available balance is 1.5 credits, generation requires 2.0.
        param:
          type: object
          description: Field name when the error is parameter-scoped
          example: prompt
          nullable: true
        doc_url:
          type: string
          description: Documentation link for this error code
          example: https://docs.aurous-labs.com/errors#balance_too_low
        request_id:
          type: string
          description: Echoes Aurous-Request-Id — quote in support tickets
          example: req_01HXMQ7Z3K8Y2VNABCDEFGHJKM
      required:
        - type
        - code
        - message
        - doc_url
        - request_id
  securitySchemes:
    api-key:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Your team API key (starts with `al_live_`).

````