> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Characters

> Train a reusable on-screen persona from reference photos, then render images as it — the same face, hair and build across a whole channel.

<Info>A character is the media sibling of an [identity](/docs/api/identities): an identity is *who an agent is being* to the world (an inbox, a number); a character is *who it renders as* on screen.</Info>

Two renders of "a young woman with freckles" are two different women, so a channel of shorts has no recurring host. A **character** fixes that: it is trained once from a handful of reference stills and then pinned into every later [image](/docs/api/images), so the same face carries across every render — the AI-influencer primitive. Training takes minutes, so `POST /v1/characters` does not wait: it answers `202` with the character in `training`, and you read it back until it is `ready`. An agent does the same with the [`create_character`](/docs/capabilities/tools#characters) family of tools; a character made here is one a session can render as, and the other way round.

## The character object

<ResponseField name="id" type="string">`chr_…`.</ResponseField>
<ResponseField name="object" type="string">Always `character`.</ResponseField>
<ResponseField name="name" type="string">How a person and an agent address it, up to 100 characters. Unique among the organization's live characters, ignoring case.</ResponseField>
<ResponseField name="description" type="string | null">Who the character is — looks, age, vibe, wardrobe — up to 1,024 characters. Read by the model when it renders as them.</ResponseField>
<ResponseField name="status" type="string">`training` → `ready` | `failed`. `ready` is the only state a render can pin.</ResponseField>
<ResponseField name="source_images" type="string[]">The reference stills it was trained from, **as you gave them**: a `fil_` id or a public URL. Never the short-lived signed link the trainer was handed.</ResponseField>
<ResponseField name="thumbnail_url" type="string | null">Reserved for a trained preview. Always `null` today: render a still with the character to see it.</ResponseField>
<ResponseField name="reason" type="string | null">Why a `failed` training failed; `null` otherwise.</ResponseField>
<ResponseField name="metadata" type="object">Arbitrary key/value pairs.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>
<ResponseField name="updated_at" type="string">When its status or fields last changed.</ResponseField>

## Create a character

`POST /v1/characters` → `202 Accepted` — scope `agents:write`, [`Idempotency-Key`](/docs/api/overview#idempotency) **required**.

<ParamField body="name" type="string" required>A short, unique name, such as `Ava`. 1–100 characters. A name a live character already holds is `validation_failed` before anything is trained or billed.</ParamField>
<ParamField body="description" type="string">Who the character is. Up to 1,024 characters. Defaults to `null`.</ParamField>
<ParamField body="image_urls" type="string[]" required>One to **sixteen** reference photos of the *same* person, each a stored file's `fil_` id (an upload, or an image a [render](/docs/api/images) filed) or a public `http(s)` URL. More, varied angles train a stronger likeness. A `fil_` this organization does not hold is `not_found`; one that is not an image, a private or loopback host, and a `data:` or `file:` URL are `validation_failed`.</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/characters \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -H "idempotency-key: $(uuidgen)" \
    -d '{
      "name": "Ava",
      "description": "Late twenties, freckles, copper bob, oversized denim jacket",
      "image_urls": ["fil_01J9A...", "fil_01J9B...", "https://example.com/ava-profile.jpg"]
    }'
  ```

  ```ts SDK theme={"system"}
  const ava = await client.characters.create({
    name: "Ava",
    description: "Late twenties, freckles, copper bob, oversized denim jacket",
    image_urls: ["fil_01J9A...", "fil_01J9B...", "https://example.com/ava-profile.jpg"],
  });
  ```

  ```bash CLI theme={"system"}
  vetta character create --name Ava --image fil_01J9A... --image fil_01J9B... --image https://example.com/ava-profile.jpg
  ```
</CodeGroup>

<ResponseExample>
  ```json 202 Accepted theme={"system"}
  {
    "id": "chr_01J9CD...",
    "object": "character",
    "name": "Ava",
    "description": "Late twenties, freckles, copper bob, oversized denim jacket",
    "status": "training",
    "source_images": ["fil_01J9A...", "fil_01J9B...", "https://example.com/ava-profile.jpg"],
    "thumbnail_url": null,
    "reason": null,
    "metadata": {},
    "created_at": "2026-10-15T12:00:00.000Z",
    "updated_at": "2026-10-15T12:00:00.000Z"
  }
  ```
</ResponseExample>

A replay under the same `Idempotency-Key` answers the character the first request made — it is not trained or billed twice. Once that character is deleted, a replay of its key is `409 idempotency_conflict`; send a new key to train another.

## Retrieve a character

`GET /v1/characters/{id}` → `200 OK` — scope `agents:read`. `{id}` is the `chr_` id **or** the name; an exact id wins over a name. Poll it after a create: each read advances the status from the trainer, so `training` turns into `ready` or `failed` on the read that sees it. A training takes a few minutes.

## List characters

`GET /v1/characters` → `200 OK`, [cursor-paginated](/docs/api/pagination), newest first — scope `agents:read`. `{ data: Character[], has_more, next_cursor }`.

<ParamField query="status" type="string">`training`, `ready` or `failed`.</ParamField>
<ParamField query="limit" type="number">Page size, 1–100.</ParamField>
<ParamField query="after" type="string">The `next_cursor` of the previous page.</ParamField>

## Delete a character

`DELETE /v1/characters/{id}` → `200 OK` — scope `agents:write`. `{id}` is the `chr_` id or the name. A soft delete: the character can no longer be rendered as and its name is free for a new one, while every file it rendered keeps its `source.character_id`.

```json Response theme={"system"}
{ "id": "chr_01J9CD...", "object": "character", "deleted": true }
```

## Rendering as a character

Pass `character` — the `chr_` id or name of a `ready` character — to [`POST /v1/media/images`](/docs/api/images#generate-images). The render is routed to a model that pins one, and the same face carries across every image. `character` and `image_urls` are not combined: the character *is* the reference.

Video takes no `character`. A character carries into video through the **still** it renders: generate an image as the character, and pass the still's `fil_` id as the `image_urls` first frame of [`POST /v1/media/videos`](/docs/api/video#generate-a-video).

```ts theme={"system"}
const still = await client.media.images.generate({ prompt: "Ava waves at the camera on a rooftop at golden hour", character: "Ava" });
const job = await client.media.videos.generate({ prompt: "she waves, the camera pushes in slowly", image_urls: [still.files[0].id] });
```

## Billing

A training costs dollars, not cents, so unlike a render it is **admitted first**: the organization's prepaid balance must cover one training, else `402 insufficient_credits` before anything is bought. It is then billed once, on the media tier, when the trainer accepts the photos — under the character the request's idempotency key names, so a replay charges nothing. A request refused before training (a taken name, an unreadable photo) is not billed. A training that later ends `failed` is refunded — credited back to the organization on the read that sees it fail. Reads, lists and deletes are free. See [Pricing](/docs/platform/pricing#media-generation--per-finished-job-media-component).

## Errors

| Code | When |
| - | - |
| `validation_failed` | Missing `Idempotency-Key`; `name` empty, over 100 characters or already held by a live character; zero or more than sixteen `image_urls`; a stored file that is not an image; a private-host, `data:` or `file:` URL. On a render: a `character` still `training` or `failed`, or combined with `image_urls`. |
| `insufficient_credits` | `402`: the prepaid balance cannot cover a training. Nothing is trained or billed. |
| `not_found` | No such `chr_` id or name in this organization, or it was deleted; an `image_urls` `fil_` this organization does not hold. |
| `provider_error` | The trainer refused the photos, or could not be reached. On a create nothing is created or billed; on a read the character is answered as it stood once the trainer answers again. |
| `idempotency_conflict` | `409`: the `Idempotency-Key` already created a character that has since been deleted. |
| `feature_not_configured` | This deploy has no media backend that trains characters. |

<Card title="In a session" icon="wand-magic-sparkles" href="/docs/capabilities/tools#characters">
  `create_character`, `get_character`, `list_characters` and `delete_character` — the same lifecycle as agent tools.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.