> ## 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.

# Put a photo into a view

> File one of your own photos as the reference for one named view, after a fit check.

`PUT /v1/characters/{id}/refs/{view}` files a photo **you** supplied as the reference for **one** of the eight named views. The photo is used **as-is** — it is never re-rendered, and a later [build](/api-reference/characters/build-character) leaves it alone and renders only the views you did not fill.

Pass the `upload_id` of a ticket from [`POST /v1/characters/uploads/init`](/api-reference/characters/upload-init) whose bytes you have already `PUT` to its `upload_url`. The route takes JSON only — there is no multipart form on the V1 surface.

The response is the full character with the updated `views[]` and a recalculated `build`.

## When to use

* You have a real photo for a view and want the platform to render only the rest.
* You want to replace a view — uploaded or generated — on a character you already own.

## Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PUT https://api.aurous-labs.com/v1/characters/char_01HXMQ7Z3K8Y2VNABCDEFGHJKM/refs/head_front \
    -H "X-Api-Key: $AUROUS_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    --max-time 60 \
    -d '{"upload_id": "upl_01HXMQ7Z3K8Y2VNABCDEFGHJKM"}'
  ```

  ```typescript Node.js theme={null}
  const character = await fetch(
    `https://api.aurous-labs.com/v1/characters/${id}/refs/head_front`,
    {
      method: "PUT",
      headers: {
        "X-Api-Key": process.env.AUROUS_API_KEY!,
        "Content-Type": "application/json",
        // One key per view — the view is part of the path.
        "Idempotency-Key": crypto.randomUUID(),
      },
      body: JSON.stringify({ upload_id: uploadId }),
      signal: AbortSignal.timeout(60_000),
    },
  ).then((r) => r.json());

  console.log(character.views.find((v) => v.view === "head_front").status); // "uploaded"
  ```

  ```python Python theme={null}
  import os, uuid, requests

  r = requests.put(
      f"https://api.aurous-labs.com/v1/characters/{character_id}/refs/head_front",
      headers={
          "X-Api-Key": os.environ["AUROUS_API_KEY"],
          "Idempotency-Key": str(uuid.uuid4()),
      },
      json={"upload_id": upload_id},
      timeout=60,
  ).json()

  print(next(v for v in r["views"] if v["view"] == "head_front")["status"])  # uploaded
  ```
</CodeGroup>

A replayed request (same `Idempotency-Key`, same body, within 24h) returns the cached response with `Aurous-Idempotent-Replayed: true`.

## Path parameters

| Parameter | Description                                                                                                                                                                                                                                                                    |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`      | Opaque character id (`char_…`).                                                                                                                                                                                                                                                |
| `view`    | One of `head_front`, `upper_front`, `lower_front`, `upper_back`, `lower_back`, `full_left`, `full_front`, `full_right`. Anything else is `400 invalid_format` (`param: "view"`) — and the same 400 fires whether or not the character exists, so it is not an existence probe. |

## Body

| Field       | Required | Description                                                                                                                                            |
| ----------- | -------: | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `upload_id` |      yes | The ticket from [`POST /v1/characters/uploads/init`](/api-reference/characters/upload-init), after you have `PUT` the image bytes to its `upload_url`. |

## Validation order

The photo passes four gates, in this order. The **first** failure is what you get back; nothing later runs, and nothing is stored until every gate has passed.

1. **Status.** A render in flight (`synthesizing`) is `409 character_busy` with `Retry-After`. Any status outside `draft`, `failed`, `reviewing`, `ready` is `400 character_status_invalid`.
2. **Bytes.** Format and dimensions. A format the platform does not accept, a body over the size ceiling, or a side over `constraints.max_dimension_px` is `400 invalid_format` / `400 value_out_of_range` (`param: upload_id`). A short side under `constraints.min_short_side_px`, or an aspect ratio over `constraints.max_aspect_ratio`, is `422 reference_unfit` with `reason: "too_small"` / `"extreme_aspect"`.
3. **Content policy.** A photo the policy declines is `400 reference_blocked` (`param: upload_id`).
4. **Fit.** A visual check reads what the photo actually shows and the rules below decide. A mismatch is `422 reference_unfit` with a `reason`.

**Check `constraints` first.** [`GET /v1/characters/views`](/api-reference/characters/list-views) returns `min_short_side_px`, `max_aspect_ratio`, `max_dimension_px` and `content_types` — the very values gate 2 enforces. Screening a file client-side against them turns a wasted round-trip into a local check.

## Nudity

* A photo showing nudity is **refused** on a `clothed` character (`reason: "nudity_mismatch"`) — **except on `head_front`, which carries no nudity rule**.
* A clothed body photo on a `nude` character is **accepted**, and the slot reports `views[].nudity: "clothed"` while the character's own `nudity` stays `nude`. That is a deliberate mixed set, not a failure — compare the two fields to detect one.
* `head_front` carries no nudity rule, so its `views[].nudity` describes **that photo** and is not a mixed-set signal: a head-and-shoulders shot normally reports `clothed` even on a `nude` character. Read the mixed-set signal off the body views.
* `nudity` is a property of the set, not of this request. Change it with `PATCH` while the character is still a `draft`; on teams that always render clothed, an explicit `nude` returns [`400 nudity_not_allowed`](/errors#nudity_not_allowed).

## Why a photo is refused

`422 reference_unfit` carries a machine-readable `reason` (and `detected_view` when the reason is `wrong_view`). Branch on `reason`, never on the prose `message`.

| `reason`          | `param`     | When                                                                                                                                                                                                                                                                                                            |
| ----------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `too_small`       | `upload_id` | The short side is under `constraints.min_short_side_px`.                                                                                                                                                                                                                                                        |
| `extreme_aspect`  | `upload_id` | Longest ÷ shortest side is over `constraints.max_aspect_ratio`.                                                                                                                                                                                                                                                 |
| `no_person`       | `upload_id` | No person was found in the photo.                                                                                                                                                                                                                                                                               |
| `multiple_people` | `upload_id` | More than one person is in frame — crop to the model alone.                                                                                                                                                                                                                                                     |
| `no_face`         | `upload_id` | `head_front` only: no clear, unobstructed face.                                                                                                                                                                                                                                                                 |
| `wrong_view`      | `view`      | The photo shows a different view than the one you addressed; `detected_view` names what it looks like. A `detected_view` of `other` means the photo matches none of the eight views (sitting, lying down, a close-up of something other than the face) — ask for a new photo rather than offering another slot. |
| `nudity_mismatch` | `upload_id` | The character's set is `clothed` and the photo shows nudity — **except on `head_front`, which carries no nudity rule**.                                                                                                                                                                                         |

Those seven are the complete `reason` set on a 422. `views[].reason` is a superset — it adds `invalid_format` and `value_out_of_range`, the two byte-stage refusals that come back as `400`s with no `reason` on the envelope. See [Views and lifecycle](/api-reference/characters/views-and-lifecycle#why-a-photo-is-refused).

## Retrying a refused photo

**A ticket is copied, never moved.** An accepted `PUT` copies the bytes into the character and leaves the ticket where it was; a refusal writes nothing at all. Either way the same `upload_id` stays usable for the rest of its 24-hour lifetime. That matters most for `wrong_view`:

```jsonc theme={null}
// PUT /v1/characters/{id}/refs/full_front → 422
{
  "error": {
    "type": "invalid_request",
    "code": "reference_unfit",
    "message": "This photo shows a different view than the one you selected.",
    "param": "view",
    "reason": "wrong_view",
    "detected_view": "full_left",
    "doc_url": "https://docs.aurous-labs.com/errors#reference_unfit",
    "request_id": "req_01HXMQ7Z3K8Y2VNABCDEFGHJKM"
  }
}
```

Re-send **the same `upload_id`** to `full_left` (with a fresh `Idempotency-Key` — a different view is a different key) and it lands. No re-upload, and the visual check is not run twice: the platform reuses what it already learned about those bytes within the ticket window.

An accepted `PUT` on a view that already holds a reference replaces it. The old image is removed.

## Limits

* **Cost**: free. Filing a photo renders nothing and charges nothing — you pay at [build](/api-reference/characters/build-character) time, and only for the views still missing.
* **Client timeout**: set **at least 60 seconds**. The visual check is allowed one retry, so the worst case is roughly 45 seconds before the response.
* **Rate limit**: bucket `characters_ref_upload` — 30 requests/min sustained, 40 burst per team. This route has its own bucket: it is free to call but does real work per request, so its ceiling is set explicitly rather than shared with the other cheap character writes.
* **Idempotency**: pass `Idempotency-Key`. Because the view is part of the path, use **one key per view** — reusing a key across two views returns `409 idempotency_key_in_use`. See [Idempotency](/idempotency).

## Errors

| Code                                                                 | HTTP | When                                                                                                                                  |
| -------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_format`                                                     | 400  | `view` is not one of the eight names (`param: view`), or the bytes are not an image format the platform accepts (`param: upload_id`). |
| `missing_field`                                                      | 400  | No `upload_id` in the body.                                                                                                           |
| `value_out_of_range`                                                 | 400  | The image is over the size or dimension ceiling (`param: upload_id`).                                                                 |
| `character_status_invalid`                                           | 400  | The character is not `draft`, `failed`, `reviewing` or `ready`; the message names the current status.                                 |
| `reference_blocked`                                                  | 400  | The photo was declined by content policy. `param` is `upload_id`.                                                                     |
| `invalid_api_key`                                                    | 401  | Missing, malformed, or revoked `X-Api-Key`.                                                                                           |
| `insufficient_scope`                                                 | 403  | The key does not carry the `write` scope.                                                                                             |
| `resource_not_found`                                                 | 404  | Unknown character id, or an `upload_id` that is not yours (`param` names which). Cross-team existence is never leaked.                |
| `character_busy`                                                     | 409  | A render is in flight; retry after `Retry-After` seconds.                                                                             |
| `idempotency_key_in_use`                                             | 409  | Same `Idempotency-Key` used with a different body, view or route.                                                                     |
| [`reference_unfit`](/errors#reference_unfit)                         | 422  | The photo does not fit the view. `reason` says why; `detected_view` is set for `wrong_view`.                                          |
| `too_many_requests`                                                  | 429  | Burst > 40 or sustained > 30/min.                                                                                                     |
| [`reference_check_unavailable`](/errors#reference_check_unavailable) | 503  | The visual check could not run. `Retry-After` is in **seconds** — retry shortly. Nothing was stored.                                  |
| `provider_unavailable`                                               | 503  | Character rendering is paused for your team; `Retry-After` is set.                                                                    |

<Warning>
  `reference_check_unavailable` and `provider_unavailable` are both `503` and mean different things. The first is a momentary hiccup in the fit check — `Retry-After` is 30 seconds. The second is a pause on your team's character rendering and carries a 24-hour `Retry-After`. Branch on `code`, not on the status.
</Warning>

<Note>
  These changes ship on the existing `2026-08-26` contract. No `Aurous-Version` pin isolates them: the version catalogue carries image and video pricing pointers only, not character pricing or the size of the reference set. Pinning an earlier `Aurous-Version` restores neither the smaller reference set nor the previous price. A character created before 2026-09-14 keeps the references it already has — add either of the two newer views on demand with [`POST /v1/characters/{id}/refs/{view}/regenerate`](/api-reference/characters/regenerate-view); every character created on or after that date carries eight.
</Note>

## Common pitfalls

* **A 30-second client timeout will abort a legitimate request.** Budget 60 seconds. Aborting does not undo anything — nothing is stored until every gate passes — but you lose the answer.
* A refusal does **not** empty the slot. Whatever the view already held stays; **on a draft**, an empty slot reads `rejected` until 24 hours pass with no further attempt, then reads `missing` again. `rejected` is surfaced on drafts only — a refused `PUT` on a `failed`, `reviewing` or `ready` character leaves an empty slot reading `missing`. See [Views and lifecycle](/api-reference/characters/views-and-lifecycle#how-a-slot-moves).
* One key per view. An `Idempotency-Key` reused across `head_front` and `full_front` is `409 idempotency_key_in_use`, not a replay.
* `PUT` replaces a **generated** view too. A view you overwrite this way becomes `uploaded`, which also means a later [regenerate](/api-reference/characters/regenerate-view) of that view returns [`400 reference_uploaded`](/errors#reference_uploaded) — replace it with another upload instead.


## OpenAPI

````yaml PUT /v1/characters/{id}/refs/{view}
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` — omit it and the platform default applies (currently
    `2026-08-26`).


    ## 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": "1_5k_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/characters/{id}/refs/{view}:
    put:
      tags:
        - Public API (v1) — Characters
      summary: Put a photo into one view
      description: >-
        Files an uploaded photo (ticket from POST /v1/characters/uploads/init)
        as the reference for ONE of the eight views. The photo is validated in
        order — dimensions, then content policy, then a visual check that it
        shows exactly one person in the requested view — and used as-is (never
        regenerated). A rejection returns 422 `reference_unfit` with `reason`
        (and `detected_view` for `wrong_view`); nothing is stored and the ticket
        stays usable, so the same photo can be re-sent to the view it actually
        shows. Replacing an existing view (uploaded or generated) is allowed
        while `draft`, `failed`, `reviewing` or `ready`; nothing is charged.
        Nudity: a nude photo cannot be filed on a `clothed` character; a clothed
        body photo on a `nude` character is accepted and reported as
        `views[].nudity: "clothed"`. Worst case ≈ 45 s (visual check with one
        retry): set a client timeout of at least 60 s. Check GET
        /v1/characters/views `constraints` locally first to skip obviously unfit
        files. Rate limit: `characters_ref_upload`.
      operationId: V1CharactersController_putReference
      parameters:
        - name: id
          required: true
          in: path
          description: Opaque character ID
          schema:
            example: char_01HXMQ7Z3K8Y2NABCDEFGHJKMR
            type: string
        - name: view
          required: true
          in: path
          description: The view this photo shows
          schema:
            type: string
            enum:
              - head_front
              - upper_front
              - lower_front
              - upper_back
              - lower_back
              - full_left
              - full_front
              - full_right
        - name: Idempotency-Key
          in: header
          description: >-
            Stripe-style idempotency key (1-256 chars). Same key + same
            canonical-JSON body returns the cached response with
            `Aurous-Idempotent-Replayed: true`. Same key against a different
            route (e.g. previously used on /v1/images) returns `409
            invalid_request / idempotency_key_in_use`. Replay window is 24
            hours. Absent header is treated as non-idempotent (each call
            processes anew). One key per view: the view is part of the path, so
            reusing a key across two views returns 409 idempotency_key_in_use.
          required: false
          schema:
            type: string
        - name: Aurous-Version
          in: header
          required: false
          description: >-
            Optional API version pin (YYYY-MM-DD). Omit the header to receive
            the platform default, currently `2026-08-26`.
          schema:
            type: string
            example: '2026-08-26'
            pattern: ^\d{4}-\d{2}-\d{2}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/V1PutCharacterRefDto'
      responses:
        '200':
          description: Photo filed; updated character returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CharacterResponse'
          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-08-26'
            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
            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: >-
            Unknown view (`invalid_format`, param `view`), a missing or
            malformed `upload_id` (`missing_field` / `invalid_format`, param
            `upload_id`), the character is in a state that does not accept a
            reference (`character_status_invalid`), or the photo was declined by
            content policy (`reference_blocked`, param `upload_id`)
          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-08-26'
            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
          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-08-26'
            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-08-26'
            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: Character or upload ticket not found / cross-team
          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-08-26'
            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: A render is in flight (`character_busy`) or Idempotency-Key reuse
          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-08-26'
            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), on
                503 provider_unavailable, and on 409 character_busy. Prefer this
                over computing X-RateLimit-Reset − now.
              schema:
                type: integer
                example: 12
        '422':
          description: Photo does not fit the view (`reference_unfit` with `reason`)
          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-08-26'
            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
          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-08-26'
            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), on
                503 provider_unavailable, and on 409 character_busy. Prefer this
                over computing X-RateLimit-Reset − now.
              schema:
                type: integer
                example: 12
        '503':
          description: >-
            Visual check unavailable (`reference_check_unavailable`, retry after
            `Retry-After` seconds) or synthesis paused for this team
            (`provider_unavailable`)
          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-08-26'
            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), on
                503 provider_unavailable, and on 409 character_busy. Prefer this
                over computing X-RateLimit-Reset − now.
              schema:
                type: integer
                example: 12
      security:
        - api-key: []
components:
  schemas:
    V1PutCharacterRefDto:
      type: object
      properties:
        upload_id:
          type: string
          description: >-
            Upload ticket from POST /v1/characters/uploads/init, after you have
            PUT the image bytes to its `upload_url`. The image is validated
            (dimensions, then content policy, then a visual check) before
            anything is stored; a rejection returns 422 `reference_unfit` and
            leaves the ticket usable, so you can retry the same photo into a
            different view without re-uploading.
          example: upl_01HXMQ7Z3K8Y2NABCDEFGHJKMR
      required:
        - upload_id
    CharacterResponse:
      type: object
      properties:
        id:
          type: string
          description: Opaque character ID.
          example: char_01HXMQ7Z3K8Y2NABCDEFGHJKMR
        object:
          type: string
          description: Discriminator
          example: character
          enum:
            - character
        name:
          type: string
          description: Display name.
          example: Aurora the Adventurer
        client_reference_id:
          type: string
          description: >-
            Caller-supplied reference echoed back (the value sent on create).
            Null if unset.
          nullable: true
          example: bot_8472
        status:
          type: string
          description: >-
            Lifecycle state. `draft`: references are being assembled; nothing
            has been charged and the character cannot be used for generation.
            New lifecycle states may be added in future — treat any status other
            than `ready` as "not yet usable". `synthesizing`: references are
            being built. `ready`: usable on POST /v1/images — both create flows
            advance here on their own. `reviewing`: reached only after POST
            /:id/resynthesize or POST /:id/build; call POST /:id/save to return
            to `ready`. `failed`: the build failed; `error_message` carries the
            reason and POST /:id/build (builder characters) or POST
            /:id/resynthesize retries. `deleted`: soft-deleted (filtered out of
            the list endpoint).
          example: ready
          enum:
            - draft
            - synthesizing
            - reviewing
            - ready
            - failed
            - deleted
        nudity:
          type: string
          description: >-
            What the reference set is rendered as — `nude` unless you chose
            `clothed`, or — when you omitted `nudity` — your team's default is
            `clothed`. An explicit value always wins. Fixed once the references
            are built.
          enum:
            - nude
            - clothed
          example: clothed
        attributes:
          description: Character attributes. Null when unset.
          nullable: true
          allOf:
            - $ref: '#/components/schemas/CharacterAttributesResponse'
        refs:
          description: >-
            Reference images, one per view the character has. A character built
            today has eight; an older one can have fewer than eight (typically
            six, some of the oldest four). Do not assume a count — read
            `views[]`, the canonical read model for new integrations, or
            `build.missing_views`. `refs[]` lists only the references that
            exist.
          type: array
          items:
            $ref: '#/components/schemas/CharacterRefEntry'
        views:
          description: >-
            One entry per named view, in canonical order — every reference that
            maps to a named view, keyed by view, plus what is missing or was
            rejected. A reference from before named views existed (`refs[].view`
            is `other`) appears only in `refs[]`.
          type: array
          items:
            $ref: '#/components/schemas/CharacterViewEntry'
        build:
          description: What a build would render and cost right now. Always present.
          allOf:
            - $ref: '#/components/schemas/CharacterBuildSummary'
        error_message:
          type: string
          description: >-
            Machine-readable failure code from the most recent
            synthesize/resynthesize attempt. One of `synthesis_failed`,
            `synthesis_timeout`, `provider_unavailable` — a closed set to switch
            on, not customer-facing prose. Null when the most recent attempt
            succeeded (or none has run yet). Set on `failed`, but NOT
            failed-exclusive: a failed *resynthesize* restores the character to
            its prior working status (`reviewing`) rather than overwriting a
            good generation, so a `reviewing` row can carry a non-null code here
            — check this field for failure, not `status`. Cleared on the next
            resynthesize attempt and on success.
          nullable: true
          example: null
        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-08-26'
        created_at:
          type: string
          description: Creation timestamp (ISO 8601).
          example: '2026-05-08T10:00:00Z'
        updated_at:
          type: string
          description: Last-update timestamp (ISO 8601).
          example: '2026-05-08T10:00:00Z'
      required:
        - id
        - object
        - name
        - status
        - nudity
        - refs
        - views
        - build
        - created_at
        - updated_at
    ErrorResponse:
      type: object
      properties:
        error:
          description: Error payload
          allOf:
            - $ref: '#/components/schemas/ErrorPayload'
      required:
        - error
    CharacterAttributesResponse:
      type: object
      properties:
        gender:
          type: string
          nullable: true
          example: female
        age:
          type: number
          nullable: true
          example: 28
        ethnicity:
          type: string
          nullable: true
          example: east-asian
        hair_color:
          type: string
          nullable: true
          example: black
        hair_style:
          type: string
          nullable: true
          example: shoulder-length straight
        eye_color:
          type: string
          nullable: true
          example: brown
        body_type:
          type: string
          nullable: true
          example: athletic
        additional_details:
          type: string
          nullable: true
          description: Free-text catch-all for attributes outside the typed fields.
          example: small scar above right eyebrow; warm smile
    CharacterRefEntry:
      type: object
      properties:
        pose:
          type: string
          description: Pose label for this reference image.
          enum:
            - portrait
            - front
            - side
            - back
            - other
          example: front
        view:
          type: string
          description: >-
            One of the eight named views, or `other` for rows created before
            named views existed. Prefer `view` over `pose` — `pose` is the
            original five-value label kept for compatibility.
          enum:
            - head_front
            - upper_front
            - lower_front
            - upper_back
            - lower_back
            - full_left
            - full_front
            - full_right
            - other
          example: full_front
        source:
          type: string
          description: >-
            `uploaded` — a photo you supplied for this view with PUT
            /v1/characters/{id}/refs/{view}, used as-is and never re-rendered (a
            build skips it, and a per-view regenerate refuses it with 400
            `reference_uploaded`). `generated` — rendered by the platform,
            whether by a create, a build or a regenerate. Both values occur
            today.
          enum:
            - uploaded
            - generated
          example: generated
        url:
          type: string
          description: >-
            Time-limited signed URL for the ref image, on the platform's storage
            host. TTL is 24h — re-fetch the character (GET /v1/characters/{id})
            to mint a fresh URL set when one expires. Treat the host as opaque
            and do not allowlist by hostname: it is not the API host and it is
            not part of the contract. Download the bytes, or re-read the
            character for a fresh URL.
          example: >-
            https://storage-host.example/storage/v1/object/sign/characters/full_front.jpg?token=eyJhbGciOi...
      required:
        - pose
        - view
        - source
        - url
    CharacterViewEntry:
      type: object
      properties:
        view:
          type: string
          description: >-
            The view this slot represents. Always one of the eight; never
            `other`.
          enum:
            - head_front
            - upper_front
            - lower_front
            - upper_back
            - lower_back
            - full_left
            - full_front
            - full_right
          example: full_front
        status:
          type: string
          description: >-
            Slot state. `generated` — holds a platform render. `uploaded` —
            holds a photo you supplied with PUT /v1/characters/{id}/refs/{view}.
            `rejected` — your most recent photo for this slot was refused (see
            `reason`) and no accepted reference took its place; it is surfaced
            while the character is a `draft` and expires 24 h after the attempt,
            at which point the slot reads `missing` again. `missing` — empty; it
            is what a build renders. This is an OPEN set: new states may be
            added, so treat an unrecognized value as `missing`.
          enum:
            - uploaded
            - generated
            - rejected
            - missing
          example: generated
        url:
          type: string
          description: >-
            Time-limited signed URL (24 h TTL) on the platform's storage host
            when `status` is `uploaded` or `generated`; `null` otherwise. Treat
            the host as opaque and do not allowlist by hostname — it is not the
            API host and it is not part of the contract.
          nullable: true
          example: null
        nudity:
          type: string
          description: >-
            Whether this specific reference shows nudity. On a `clothed`
            character EVERY view reads `clothed`, uploaded or generated — the
            read model reports the set's rendering intent there. On a `nude`
            character a generated view reads `nude` and an UPLOADED view reports
            what the photo itself shows, which is how a mixed set arises: a
            clothed body photo filed onto a `nude` character is accepted
            deliberately and reads `clothed` while the character's own `nudity`
            stays `nude`. Compare the two to detect a mixed set. (The reverse is
            refused at intake: a nude photo on a `clothed` character is 422
            `reference_unfit` with `reason: "nudity_mismatch"` — except on
            `head_front`, which carries no nudity rule.) `null` when the slot
            has no reference.
          enum:
            - nude
            - clothed
          nullable: true
          example: clothed
        reason:
          type: string
          description: >-
            Why the most recent upload for this slot was refused. Present only
            when `status` is `rejected`. The same set as the `reason` on a 422
            `reference_unfit`, plus the two byte-level codes (`invalid_format`,
            `value_out_of_range`) a photo can be refused for before the fit
            rules run — those two are error codes rather than fit reasons, so
            the 422 itself carries no `reason` for them. Branch on the value and
            treat anything unrecognized as a generic refusal.
          enum:
            - too_small
            - extreme_aspect
            - no_person
            - multiple_people
            - no_face
            - wrong_view
            - nudity_mismatch
            - invalid_format
            - value_out_of_range
          nullable: true
          example: null
        detected_view:
          type: string
          description: >-
            Which view the refused photo actually looked like. Present only when
            `reason` is `wrong_view` — offer the customer that slot instead of
            asking for a new photo: the same upload ticket can be re-sent to
            that view without re-uploading. `null` on every other slot.
          enum:
            - head_front
            - upper_front
            - lower_front
            - upper_back
            - lower_back
            - full_left
            - full_front
            - full_right
            - other
          nullable: true
          example: null
        made_from:
          description: >-
            Which of your references this view was rendered from, by view name.
            `null` for a view you uploaded (it was not rendered from anything)
            and for a `missing` or `rejected` slot; `[]` for a generated view
            whose provenance was not recorded (rows predating provenance).
            Otherwise the view names it was conditioned on.
          nullable: true
          example:
            - head_front
            - full_front
          type: array
          items:
            type: string
      required:
        - view
        - status
    CharacterBuildSummary:
      type: object
      properties:
        missing_views:
          type: array
          description: >-
            Views with no reference, in canonical view order — what a build (or
            the equivalent per-view regenerates) would render right now.
          example:
            - full_front
            - full_right
          items:
            type: string
            enum:
              - head_front
              - upper_front
              - lower_front
              - upper_back
              - lower_back
              - full_left
              - full_front
              - full_right
        cost_credits:
          type: number
          description: >-
            Credits the next build (or the equivalent per-view regenerates) will
            charge: `per_view_credits` × `missing_views.length`.
            Server-computed, and derived from the references that exist RIGHT
            NOW — so it is meaningful once the character is `ready` or
            `reviewing`. While it is `synthesizing` no view has been persisted
            yet, so this quotes every view still to come, not the remaining
            work.
        per_view_credits:
          type: number
          description: >-
            Current price of one generated view, in credits (also the per-view
            regenerate price). Read it; never hard-code it — it is
            configuration, not a constant. On a `character.completed` webhook
            payload a `0` here means the price could not be read at the moment
            the event was emitted, not that views are free — re-read `GET
            /v1/characters/{id}` for the live value.
      required:
        - missing_views
        - cost_credits
        - per_view_credits
    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
            - character_status_invalid
            - character_busy
            - nudity_not_allowed
            - reference_unfit
            - reference_uploaded
            - build_requires_identity
            - 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
            - reference_check_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
            - character_resynthesize_in_progress
            - 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
        reason:
          type: string
          description: >-
            Sub-code refining `code`, for codes that document one. Present today
            only on `reference_unfit`; `null` otherwise. Branch on `code` first
            and treat an unrecognized `reason` as a generic failure of that
            code.
          enum:
            - too_small
            - extreme_aspect
            - no_person
            - multiple_people
            - no_face
            - wrong_view
            - nudity_mismatch
          nullable: true
          example: wrong_view
        detected_view:
          type: string
          description: >-
            Present only on `reference_unfit` with `reason: "wrong_view"`: the
            view the rejected image actually depicts. Offer the customer that
            view instead of asking for a new photo — the same upload can be
            re-sent to it.
          enum:
            - head_front
            - upper_front
            - lower_front
            - upper_back
            - lower_back
            - full_left
            - full_front
            - full_right
            - other
          nullable: true
          example: full_left
        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_`).

````