Skip to main content
PUT
Put a photo into one view
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 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 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

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

Path parameters

Body

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

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. 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 400s with no reason on the envelope. See Views and lifecycle.

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

Errors

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.
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; every character created on or after that date carries eight.

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.
  • 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 of that view returns 400 reference_uploaded — replace it with another upload instead.

Authorizations

X-Api-Key
string
header
required

Your team API key (starts with al_live_).

Headers

Idempotency-Key
string

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.

Aurous-Version
string

Optional API version pin (YYYY-MM-DD). Omit the header to receive the platform default, currently 2026-08-26.

Pattern: ^\d{4}-\d{2}-\d{2}$
Example:

"2026-08-26"

Path Parameters

id
string
required

Opaque character ID

Example:

"char_01HXMQ7Z3K8Y2NABCDEFGHJKMR"

view
enum<string>
required

The view this photo shows

Available options:
head_front,
upper_front,
lower_front,
upper_back,
lower_back,
full_left,
full_front,
full_right

Body

application/json
upload_id
string
required

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"

Response

Photo filed; updated character returned

id
string
required

Opaque character ID.

Example:

"char_01HXMQ7Z3K8Y2NABCDEFGHJKMR"

object
enum<string>
required

Discriminator

Available options:
character
Example:

"character"

name
string
required

Display name.

Example:

"Aurora the Adventurer"

status
enum<string>
required

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

Available options:
draft,
synthesizing,
reviewing,
ready,
failed,
deleted
Example:

"ready"

nudity
enum<string>
required

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.

Available options:
nude,
clothed
Example:

"clothed"

refs
object[]
required

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.

views
object[]
required

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[].

build
object
required

What a build would render and cost right now. Always present.

created_at
string
required

Creation timestamp (ISO 8601).

Example:

"2026-05-08T10:00:00Z"

updated_at
string
required

Last-update timestamp (ISO 8601).

Example:

"2026-05-08T10:00:00Z"

client_reference_id
string | null

Caller-supplied reference echoed back (the value sent on create). Null if unset.

Example:

"bot_8472"

attributes
object | null

Character attributes. Null when unset.

error_message
string | null

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.

Example:

null

aurous_version
string

API contract version applied at the time this row was minted (D25 — frozen for replay across future version bumps).

Example:

"2026-08-26"