---
title: "Draw headshot candidates for one cast member (metered — TWO CALLS, price first)."
method: POST
path: "/story-projects/{projectId}/cast-portraits/design"
tags: ["Projects"]
---

# Draw headshot candidates for one cast member (metered — TWO CALLS, price first).

`POST /story-projects/{projectId}/cast-portraits/design`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/story-projects/{projectId}/cast-portraits/design`
- Auth: HTTP bearer

Draws N headshot candidates (`candidates`, default 3, at most 4) for one named cast member (`character`, by canon name, alias, cue or Series Bible name), so the writer chooses a face instead of taking the only one the cast draws. Each is drawn from the Series Bible's appearance and wardrobe for them with their LOOK after it: `direction` when given, else their look direction on the cast canon (PATCH /story-projects/{projectId}/cast-look), and `age` likewise. The look outranks the Bible where they differ. TWO CALLS: sent without `confirmed` it PRICES the candidates and draws nothing (`quotedOnly: true`, `credits` — one cast headshot per candidate — `balanceAfter`, `sufficient`, `note`, and the `direction` it will draw from); sent again with `confirmed: true` it reserves them, records the design and queues the draw (`designId`, `status: queued`). Poll GET .../cast-portraits/designs?designId= until `status` is `complete`. EVERY CANDIDATE DRAWN IS CHARGED, chosen or not; a candidate that cannot be drawn is released; choosing is free. RESTYLE KEEPING THE FACE: with `keepFace: true` each candidate is the person's CURRENT HEADSHOT on the cast canon, edited to the look (`direction`, else their saved look) on the model that keeps a face, so their hair, grooming, wardrobe or styling change and the person stays the same (`restyledFrom` names the headshot). Same price per candidate; casting coverage judges them and choosing works the same. Refused before the price: 404 `character_not_found` when nobody in the cast canon or Series Bible answers to the name; 409 `cast_precondition_failed` when there is nothing to draw them from, no image model is bound, or (with `keepFace`) they have no headshot yet (reason `cast_face_missing`) or no bound model keeps a face; 409 `project_precondition_failed` when the project has not reached casting.  Requires `Idempotency-Key`. Scope: `artifact:publish` (billable).

## Headers

- `Idempotency-Key` string, required

## Request body

- CastPortraitDesignRequest
  - `character` string, required: The cast member: canon name, alias, script cue or Series Bible name.
  - `direction` string: How they should look, in the writer's words. Omitted: their look direction on the cast canon, else the Series Bible alone.
  - `age` string: The age they are played at. Omitted: their cast canon age.
  - `candidates` integer: Headshot candidates to draw (default 3). Each is charged.
  - `keepFace` boolean: Restyle the person KEEPING THEIR FACE: each candidate is their current headshot on the cast canon, edited to the look (`direction`, else their saved look) on the model that keeps a face, so the hair, grooming, wardrobe or styling change and the person stays. Same price per candidate, judged by casting coverage and chosen like any headshot. Refused (409 `cast_precondition_failed`, reason `cast_face_missing`) when they have no headshot yet. Omit (or false) for new faces.
  - `confirmed` boolean: Omit (or false) to get the price without spending anything. true reserves and queues the draw, only after the writer has seen the price.

## Response `200`

The price (no `confirmed`) or the queued design.

- CastPortraitDesignEnvelope: Quote: the price and the plan, nothing drawn. Confirmed: `designId` and `status: queued` — poll GET .../cast-portraits/designs?designId= until `status` is `complete`.
  - `quotedOnly` boolean, required
  - `queued` boolean: Confirmed only.
  - `credits` integer, required
  - `costLabel` string, nullable: Quote only.
  - `balanceAfter` integer: Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short.
  - `sufficient` boolean: Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse.
  - `note` string, required: Show it to the writer. The quote says every candidate drawn is charged, chosen or not.
  - `character` string, required
  - `castPersonId` string, nullable
  - `direction` string, required
  - `directionSource` 'request' | 'cast_canon' | 'series_bible', required
  - `age` string, nullable
  - `candidates` integer, required: How many candidates will be (or are being) drawn.
  - `restyledFrom` string, nullable: A restyle that keeps the face (`keepFace`): the current headshot each candidate is edited from. Null for new faces.
  - `designId` string: Confirmed only.
  - `status` 'queued' | 'drawing' | 'complete' | 'failed': Confirmed only.

## Other responses

- `400`: Request payload failed validation. The error message names the offending field.
- `401`: Missing, malformed, revoked, or expired API key.
- `402`: Not enough Studio Credits to reserve this job (`insufficient_credits`), and nothing was reserved. `details` is InsufficientCreditsDetails: `required` and `available`, and `autoTopup` when auto top-up is on but did not cover it — `pending` (retry in a moment), `over_cap` (raise the monthly spending cap), `too_small` (buy a bigger pack) or `failed`.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `404`: Nobody in the project's cast canon or Series Bible answers to that name (`character_not_found`), or the project is not yours (`project_not_found`). Nothing was changed or charged.
- `409`: Casting cannot run on this project yet, and nothing was quoted or drawn: nobody castable, nobody with a description to draw from, no passing screenplay coverage, or no image model bound (`cast_precondition_failed`, `details.reason` names which) — or the project itself has not reached casting (`project_precondition_failed`, `details.stage` names the stage to finish). The message says what to do.
- `429`: Rate limit for this API key + IP + route family was exceeded.
- `500`: The API failed unexpectedly. Safe to retry with the same Idempotency-Key.

---

[API](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api.md) · [All operations](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/revisions/4534f5d25540?raw)
