Skip to main content
A character is a set of eight named views. You can let the platform render every one of them (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 rejected rather than uploaded.
  • rejected is a memory of your last attempt, not a stored reference. It is surfaced only while the character is a draft (a built character has no pending refusals to annotate) and it expires 24 hours after the attempt, at which point the slot reads missing again.
  • A generated slot can be overwritten too: an accepted PUT turns it uploaded, and from then on regenerate refuses it with 400 reference_uploaded.
  • DELETE /v1/characters/{id}/refs/{view} sends a slot back to missing, and is accepted only while the character is a draft.

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 clothed character — 422 reference_unfit, reason: "nudity_mismatch"except on head_front, which carries no nudity rule.
  • A clothed body photo on a nude character is accepted. It is not a failure: the slot comes back as views[].nudity: "clothed" while the character’s own nudity stays nude. 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_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.

Why a photo is refused

A refusal is 422 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 malformed upload_id, a character whose status does 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 a 422 from the request alone, which is why it is its own status.
A failed visual check is neither: it is 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_views and views[] are derived from the references that exist right now. Re-read the character after every PUT rather than caching a plan.
  • A draft cannot be used for generation. POST /v1/images or POST /v1/videos with a draft’s character_id returns 400 character_not_ready.
  • A rejected slot is not an error state you have to clear. Upload a fitting photo, remove the slot, or leave it — a build renders whatever is missing or rejected all the same.