POST /v1/characters), or assemble the set yourself with the builder — create a draft, file your own photo into a view, then build whatever is still missing.
This page is the shared vocabulary those routes assume: the views, the slot states, the statuses each operation accepts, and the reasons a photo is refused. Every endpoint page links back here rather than repeating it.
The eight views
That is canonical order — the order
views[] comes back in, the order refs[] is sorted in, and the order field on GET /v1/characters/views. Fetch the catalogue rather than hardcoding the list; it also carries the constraints to pre-check a photo against locally.
view and the legacy pose label
refs[].pose predates named views and is locked at five values for compatibility. refs[].view is the real view name. Four of the eight views project onto other, so pose can never round-trip back to a view — read view (or views[]) in new code.
The views[] read model
Every character read carries views[] with exactly eight entries, always — a view the character does not have is present with status: "missing", never absent. Each entry carries view, status, url, nudity, reason, detected_view and made_from.
The set is open — treat an unrecognized value as
missing.
How a slot moves
- A refusal stores nothing: the slot keeps whatever it already held, and a slot that held nothing becomes
rejectedrather thanuploaded. rejectedis a memory of your last attempt, not a stored reference. It is surfaced only while the character is adraft(a built character has no pending refusals to annotate) and it expires 24 hours after the attempt, at which point the slot readsmissingagain.- A
generatedslot can be overwritten too: an acceptedPUTturns ituploaded, and from then on regenerate refuses it with400 reference_uploaded. DELETE /v1/characters/{id}/refs/{view}sends a slot back tomissing, and is accepted only while the character is adraft.
Which operation each status allows
After a build fails, a view can be replaced (
PUT) but not removed (DELETE requires draft) until the next successful build. Nothing was lost: the references you uploaded are stored on the character, not as expiring upload tickets, so POST /v1/characters/{id}/build from failed is the recovery path and needs no re-upload.Nudity
nudity (nude or clothed) is a property of the whole reference set. Set it on POST /v1/characters or POST /v1/characters/drafts, change it with PATCH while the character is still a draft, and after that it is fixed. Omit it and the character takes your team’s default. On teams that always render clothed, an explicit nude returns 400 nudity_not_allowed.
Rules for a photo you file into a view:
- A photo showing nudity is refused on a
clothedcharacter —422 reference_unfit,reason: "nudity_mismatch"— except onhead_front, which carries no nudity rule. - A clothed body photo on a
nudecharacter is accepted. It is not a failure: the slot comes back asviews[].nudity: "clothed"while the character’s ownnuditystaysnude. Compare the two to detect a mixed set. - Every generated view reports the character’s own
nudity, so a mixed set can only come from photos you uploaded. head_frontcarries no nudity rule, so itsviews[].nuditydescribes that photo and is not a mixed-set signal: a head-and-shoulders shot normally reportsclothedeven on anudecharacter. Read the mixed-set signal off the body views.
Why a photo is refused
A refusal is422 reference_unfit with a machine-readable reason (and detected_view when the reason is wrong_view). Branch on reason, never on the prose message. Nothing is stored, nothing is charged, and the upload ticket stays usable — re-send the same photo to a different view without re-uploading the bytes.
Those seven are the complete
reason set on a 422.
views[].reason is a superset of the table above. A photo can also be refused before the fit rules ever run — for a format the platform does not accept, or for dimensions outside the hard ceiling — and those two refusals are error codes rather than fit reasons: they come back as 400 invalid_format or 400 value_out_of_range with no reason on the envelope, while the slot has to remember something. So views[].reason is the seven reasons above plus invalid_format and value_out_of_range — nine values in total. Keep a default branch either way.400 or 422?
400— the request is wrong in a way you can see from your own side: an unknown{view}, a missing or malformedupload_id, a character whosestatusdoes not allow the operation, a format the platform does not accept.422— the request is well-formed and the platform had to look at the bytes to decide: dimensions, and what the photo actually depicts. You cannot predict a422from the request alone, which is why it is its own status.
503 reference_check_unavailable with a Retry-After in seconds. Retry — nothing was stored and nothing was charged.
Version 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; every character created on or after that date carries eight.Common pitfalls
build.missing_viewsandviews[]are derived from the references that exist right now. Re-read the character after everyPUTrather than caching a plan.- A
draftcannot be used for generation.POST /v1/imagesorPOST /v1/videoswith a draft’scharacter_idreturns400 character_not_ready. - A
rejectedslot is not an error state you have to clear. Upload a fitting photo, remove the slot, or leave it — a build renders whatever ismissingorrejectedall the same.

