---
title: Character endpoints
description: Every /v1/characters and /v1/scenes endpoint — save a character (a name, a description and a turnaround sheet) or a scene (a place and its picture), list, retrieve, update and delete them, download their pictures, and use them in music videos, character swaps and video shots.
layout: reference
---

A saved character is a person you want to use again: a name, a description (face, hair, build, the clothes they usually wear) and a turnaround sheet — the same person from the front, three-quarter, side and back, over close-ups of their face with six expressions, in one picture — plus, optionally, the photo it was made from and [a voice](#voices). Pass its id as `character_id` to [music videos](/docs/models/music-videos#character-profile) and [character swaps](/docs/models/videos#saved-character) instead of sending the picture every time.

Saved characters are optional. Every video endpoint still takes the picture and the text inline, so a one-off call needs no saved character at all.

The endpoints live under `https://lovelyrouter.com/v1/characters`. Authenticate with your API key as `Authorization: Bearer $LOVELY_API_KEY` ([Authentication](/docs/authentication)). Characters are free to keep, private to your account, and kept until you delete them; an account can keep up to 100.

## The character object

:::params Attributes
- `id` string
  Unique id, `chr_` followed by 24 characters.
- `object` string
  Always `character`.
- `name` string
  What you call the character, up to 80 characters.
- `description` string
  Who the person is, as you wrote it: face, hair, body and default outfit. Up to 4,000 characters; may be empty.
- `sheet_url` string
  The turnaround sheet. See [Download the pictures](#download-the-pictures). The `v` query value changes when you replace the sheet.
- `photo_url` string | null
  The source photo, when you saved one.
- `voice` object | null
  The character's voice in [video shots](/docs/api/video-shots#voices): `id` (a [saved voice](/docs/api/audio#the-voice-object)), `description` (how it was designed, or `null`) and `audio_url` (the sample). `null` when the character has no voice. See [Give a character a voice](#voices).
- `created_at` integer
  Unix seconds.
- `updated_at` integer
  Unix seconds of the last change.
:::

```json title="The character object" aside
{
  "id": "chr_3b9e1f0c7a2d4e58b6c1a9f2",
  "object": "character",
  "name": "Mara",
  "description": "A woman in her thirties with a long black braid, brown eyes, freckles and an athletic build; a green bomber jacket over a grey hoodie, black cargo trousers and white sneakers.",
  "sheet_url": "https://lovelyrouter.com/v1/characters/chr_3b9e1f0c7a2d4e58b6c1a9f2/sheet?v=mh2x7k1c",
  "photo_url": null,
  "voice": {
    "id": "voice_7d2a9c4e1b6f3a8d0c5e2b1f",
    "description": "A low, calm alto, unhurried",
    "audio_url": "https://lovelyrouter.com/v1/audio/voices/voice_7d2a9c4e1b6f3a8d0c5e2b1f/audio"
  },
  "created_at": 1792915200,
  "updated_at": 1792915200
}
```

## Create a character

::endpoint POST /v1/characters

Saves a character and returns `201` with the character object. Send multipart (`sheet` and `photo` as files) or JSON (the same as `http(s)` or `data:` URLs). To save a sheet you generated with the [images API](/docs/models/images), pass the image job's id as `sheet_image_job` instead of the file.

:::params Body parameters
- `name` string, required
  Up to 80 characters.
- `description` string
  Face, hair, body and default outfit, up to 4,000 characters. Music videos use it as the `character` brief when you don't send one.
- `sheet` file or string, required
  The turnaround sheet: PNG, JPEG or WebP up to 10 MB, as a file or an `http(s)` / `data:` URL (also `sheet_url`). Use `sheet_image_job` instead to take it from an image job.
- `sheet_image_job` string
  An image job id (`img_…`) of your account that has `succeeded`: its first image becomes the sheet. Send either this or `sheet`.
- `photo` file or string
  The photo the sheet was made from, PNG, JPEG or WebP up to 10 MB (also `photo_url`, or `photo_image_job`). Kept for your records; videos use the sheet.
- `voice_id` string
  One of your [saved voices](/docs/api/audio#save-a-voice) (`voice_…`) to give the character.
- `voice` file or string
  A new voice sample instead: WAV, MP3, OGG/Opus, FLAC, M4A or WebM up to 10 MB, as a file or an `http(s)` / `data:` URL. It is saved as one of your voices, named after the character, and linked. Needs `consent`.
- `voice_transcript` string
  What the sample says (up to 1,000 characters) — voices clone better with it.
- `consent` boolean
  `true` confirms the sample is your voice or you have the speaker's permission to use it. Required with `voice`.
- `voice_description` string
  How the voice was designed, e.g. the description you gave `qwen3-tts` (up to 1,000 characters). Kept with the voice for your records.
:::

```bash title="cURL" aside
curl https://lovelyrouter.com/v1/characters \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F name=Mara \
  -F description="A woman in her thirties with a long black braid, brown eyes and freckles; a green bomber jacket" \
  -F sheet=@mara-sheet.png
```

```python title="Python" aside
import os, requests

character = requests.post(
    "https://lovelyrouter.com/v1/characters",
    headers={"Authorization": f"Bearer {os.environ['LOVELY_API_KEY']}"},
    data={"name": "Mara", "description": "A woman in her thirties with a long black braid, brown eyes and freckles; a green bomber jacket"},
    files={"sheet": open("mara-sheet.png", "rb")},
).json()
```

```js title="Node.js" aside
// A sheet you generated with the images API (POST /v1/images/jobs), saved by its job id.
const res = await fetch('https://lovelyrouter.com/v1/characters', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.LOVELY_API_KEY}`, 'content-type': 'application/json' },
  body: JSON.stringify({ name: 'Mara', description: 'A woman in her thirties with a long black braid…', sheet_image_job: imageJob.id }),
});
const character = await res.json();
```

## List characters

::endpoint GET /v1/characters

Your characters, newest first.

:::params Query parameters
- `limit` integer, default `20`
  1–100.
- `after` string
  A character id: list the ones saved before it (the next page).
:::

```bash title="cURL" aside
curl "https://lovelyrouter.com/v1/characters?limit=50" \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

:::aside
```json title="Response"
{
  "object": "list",
  "data": [{ "id": "chr_3b9e1f0c7a2d4e58b6c1a9f2", "object": "character", "name": "Mara" }],
  "first_id": "chr_3b9e1f0c7a2d4e58b6c1a9f2",
  "last_id": "chr_3b9e1f0c7a2d4e58b6c1a9f2",
  "has_more": false
}
```
:::

## Retrieve a character

::endpoint GET /v1/characters/{id}

Returns the character object; `404` for an id that isn't one of yours.

```bash title="cURL" aside
curl https://lovelyrouter.com/v1/characters/CHARACTER_ID \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

## Update a character

::endpoint PATCH /v1/characters/{id}

Changes the fields you send and returns the character object. Takes JSON or multipart, like [Create](#create-a-character). Videos already made keep the sheet they were made with.

:::params Body parameters
- `name` string
  A new name.
- `description` string
  A new description (an empty string clears it).
- `sheet` file or string
  A new sheet, or `sheet_image_job`.
- `photo` file, string or null
  A new photo (or `photo_image_job`); `null` removes it.
- `voice_id` string or null
  Link one of your saved voices; `null` unlinks the voice (the saved voice itself stays).
- `voice` file, string or null
  A new sample (with `voice_transcript` and `consent`, as in [Create](#create-a-character)); `null` unlinks.
- `voice_description` string or null
  How the voice was designed. A newly linked voice takes the description you send with it, or none.
:::

```bash title="cURL" aside
curl -X PATCH https://lovelyrouter.com/v1/characters/CHARACTER_ID \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"description": "A woman in her thirties with a long black braid, now in a red raincoat"}'
```

## Download the pictures

::endpoint GET /v1/characters/{id}/sheet

The sheet image with its own content type; `GET /v1/characters/{id}/photo` is the photo (`404` when there is none). Only your key can read them.

```bash title="cURL" aside
curl https://lovelyrouter.com/v1/characters/CHARACTER_ID/sheet \
  -H "Authorization: Bearer $LOVELY_API_KEY" -o sheet.png
```

## Delete a character

::endpoint DELETE /v1/characters/{id}

Deletes the character and its pictures. Videos already made with it are not affected.

```bash title="cURL" aside
curl -X DELETE https://lovelyrouter.com/v1/characters/CHARACTER_ID \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

:::aside
```json title="Response"
{ "id": "chr_3b9e1f0c7a2d4e58b6c1a9f2", "object": "character.deleted", "deleted": true }
```
:::

## Give a character a voice {#voices}

A character can have a voice: one of your [saved voices](/docs/api/audio#save-a-voice). In a [video shot](/docs/api/video-shots#voices) a character with a voice speaks their lines in it — its timbre, pitch and manner, not its words.

- **Upload one**: send the sample as `voice` (with `voice_transcript` and `consent: true`) when you create or update the character. It is saved as a voice named after the character.
- **Design one**: make a sample with [`qwen3-tts`](/docs/models/speech) from a description and a line the character would say (`instructions` = the description, `input` = the line, no `voice`), save it with `POST /v1/audio/voices`, then link it as `voice_id` — with the description as `voice_description`.

`voice_id: null` (or `voice: null`) unlinks the voice. Deleting a character keeps its voice; deleting the voice unlinks it from every character.

```bash title="cURL" aside
# Link a saved voice to a character.
curl -X PATCH https://lovelyrouter.com/v1/characters/CHARACTER_ID \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"voice_id": "voice_7d2a9c4e1b6f3a8d0c5e2b1f", "voice_description": "A low, calm alto, unhurried"}'
```

## Saved scenes {#scenes}

::endpoint POST /v1/scenes

A saved scene is a place you want to use again in [video shots](/docs/api/video-shots): a name, a description of the place and its picture. Scenes work like characters, under `https://lovelyrouter.com/v1/scenes`, with the picture called `image` (a file, an `http(s)` / `data:` URL, or `image_image_job`) and no photo. Up to 100 per account, besides your characters; free.

The scene object: `{id: "scn_…", object: "scene", name, description, image_url, created_at, updated_at}`.

| Request | What it does |
| --- | --- |
| `POST /v1/scenes` | `name` (required, up to 80 characters), `description`, `image` (required). Returns `201`. |
| `GET /v1/scenes` | Your scenes, newest first (`?limit`, `?after`). |
| `GET /v1/scenes/{id}` | One scene. |
| `PATCH /v1/scenes/{id}` | `name`, `description`, `image`. |
| `GET /v1/scenes/{id}/image` | The picture (your key only). |
| `DELETE /v1/scenes/{id}` | Deletes the scene and its picture. |

```bash title="cURL" aside
curl https://lovelyrouter.com/v1/scenes \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F name="Night Café" \
  -F description="A small café at night, rain on the windows, warm lamps" \
  -F image=@cafe.jpg
```

## Use a character

Pass `character_id` where you would send the picture. The inline fields keep working, so pick per call:

| Endpoint | One-off call (inline) | With a saved character |
| --- | --- | --- |
| [`POST /v1/videos/music`](/docs/api/music-videos#create-a-music-video) | `character` (text) and/or `character_image` (file or URL) | `character_id`: its sheet is the reference picture, its description the `character` text unless you send one. Not with `character_image`, and only with `mode: "image"`. |
| [`POST /v1/videos`](/docs/models/videos#saved-character) | `input_reference` (file or URL) | `character_id`: its sheet is the reference picture. Send exactly one of the two. |
| [`POST /v1/videos/shots`](/docs/api/video-shots#create-a-shot) | `characters` / `scenes`: `{name, image}` objects | `characters` / `scenes`: ids; the prompt tags them (`@Mara`), each tagged one is a reference picture. |

The music video object and the video object show the `character_id` they were made with (`null` for inline calls).

```bash title="cURL" aside
# A one-off music video: the picture sent with the call.
curl https://lovelyrouter.com/v1/videos/music \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F model=ltx-music-video -F song=@song.mp3 -F consent=true \
  -F character="A woman with a long black braid" -F character_image=@mara.png

# The same with a saved character.
curl https://lovelyrouter.com/v1/videos/music \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F model=ltx-music-video -F song=@song.mp3 -F consent=true \
  -F character_id=chr_3b9e1f0c7a2d4e58b6c1a9f2
```

## Errors

Errors use the [standard error body](/docs/api/errors).

| Status | `type` | Why |
| --- | --- | --- |
| `400` | `invalid_request_error` | A missing `name` or `sheet`, a field too long, a picture that isn't PNG, JPEG or WebP or is larger than 10 MB, an image job that isn't yours or hasn't succeeded, or 100 characters already saved. A `voice_id` that isn't one of your voices, a `voice` that isn't audio or is larger than 10 MB, or one sent without `consent`, or 100 voices already saved. |
| `401` | `authentication_error` | A missing or invalid API key. |
| `404` | `not_found_error` | An unknown or deleted character, or one of another account — also as `character_id` in a video request. |
