---
title: Video
description: Character swap through the Videos API — send a clip and a picture of a new character, get the same shot back with that character, its motion, framing and voice kept — and video shots made from a prompt with tagged characters and scenes. Requests, polling, pricing, regions and limits.
---

Lovely Router makes videos through a Videos API shaped like OpenAI's: `POST /v1/videos` starts a job and returns a video object, you poll it until it is `completed`, then download the mp4 from `/v1/videos/{id}/content`. Try it without code in the [playground](/app/playground?mode=videos): upload a clip or find one on X, upload a picture or have any image model draw one, and let a chat model of your choice improve the character and scene prompts — each step billed to your balance like its API call.

For a video that keeps going while you watch, see [Live video](/docs/models/live-video); to turn a song into a music video, see [Music videos](/docs/models/music-videos).

## Models {#models}

| Model | Price | What it does |
| --- | --- | --- |
| `minimax-h3-character-swap` | $0.10 per second of video you get back | Re-shoots a clip with a new character: their face, hair, body and clothes come from a picture; the motion, framing, camera and soundtrack come from the clip. Up to 10 s per clip. |
| `minimax-h3-aftermidnight` | $0.12 per second of video you get back | The same character swap with an NSFW LoRA (AfterMidnight) for adult scenes: richer, more intimate surroundings and uncensored bodies. Up to 10 s per clip. |
| `minimax-h3-video` | $0.13 per rendered second, + $0.06 per second for each picture after the first | Makes [video shots](#shots) from a prompt with tagged characters and scenes, with sound and dialogue; joins shots into one video for free. 5–15 s per shot. |

All are powered by **MiniMax H3** and run on our own GPUs in the `creative-hosted` [channel group](/docs/models/channel-groups). They are under the [MiniMax H3 terms](/docs/models/minimax-h3-license): every request sets `consent: true`, accepting them — including not using it in the EU, the UK, South Korea or the US — and the responsibility for following them.

## Swap a character {#swap-a-character}

Send the source clip as `video` and a picture of the new character as `input_reference`, as files (multipart) or as URLs (`http(s)` or `data:`, in JSON or as multipart text fields).

```bash title="cURL"
curl https://lovelyrouter.com/v1/videos \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F model=minimax-h3-character-swap \
  -F video=@clip.mp4 \
  -F input_reference=@character.png \
  -F prompt="In front of a weathered concrete wall, soft window light" \
  -F consent=true
```

```python title="Python"
import os
import time
import requests

BASE = "https://lovelyrouter.com/v1"
H = {"Authorization": f"Bearer {os.environ['LOVELY_API_KEY']}"}

video = requests.post(
    f"{BASE}/videos",
    headers=H,
    data={
        "model": "minimax-h3-character-swap",
        "prompt": "In front of a weathered concrete wall, soft window light",
        "consent": "true",
    },
    files={"video": open("clip.mp4", "rb"), "input_reference": open("character.png", "rb")},
).json()

# Wait up to 60 s per poll until it finishes.
while video["status"] in ("queued", "in_progress"):
    video = requests.get(f"{BASE}/videos/{video['id']}?wait=60", headers=H).json()

if video["status"] == "completed":
    with open("swapped.mp4", "wb") as f:
        f.write(requests.get(f"{BASE}/videos/{video['id']}/content", headers=H).content)
else:
    print(video["error"])
```

```js title="Node.js"
import fs from 'node:fs';

const BASE = 'https://lovelyrouter.com/v1';
const H = { Authorization: `Bearer ${process.env.LOVELY_API_KEY}` };

const form = new FormData();
form.set('model', 'minimax-h3-character-swap');
form.set('video', new Blob([fs.readFileSync('clip.mp4')], { type: 'video/mp4' }), 'clip.mp4');
form.set('input_reference', new Blob([fs.readFileSync('character.png')], { type: 'image/png' }), 'character.png');
form.set('prompt', 'In front of a weathered concrete wall, soft window light');
form.set('consent', 'true');

let video = await (await fetch(`${BASE}/videos`, { method: 'POST', headers: H, body: form })).json();
while (video.status === 'queued' || video.status === 'in_progress') {
  video = await (await fetch(`${BASE}/videos/${video.id}?wait=60`, { headers: H })).json();
}
if (video.status === 'completed') {
  const mp4 = await fetch(`${BASE}/videos/${video.id}/content`, { headers: H });
  fs.writeFileSync('swapped.mp4', Buffer.from(await mp4.arrayBuffer()));
} else console.error(video.error);
```

A render takes about one minute for a 5-second clip and two to three minutes for a 10-second one (larger frames take longer). When the model hasn't been used for a while, add about a minute while a GPU wakes up. Jobs keep running if you disconnect.

## Request fields {#request}

:::params Body parameters
- `model` string, required
  `minimax-h3-character-swap`.
- `video` file, required
  The source clip: MP4, MOV or WebM, up to 80 MB, as a file or an `http(s)` / `data:` URL. Its motion, framing, camera and soundtrack are kept; up to 10 seconds of it are used, from `start`.
- `input_reference` file, required unless `character_id`
  A picture of the new character: PNG, JPEG or WebP, up to 20 MB, as a file or a URL. One person, face visible, evenly lit on a plain background works best.
- `character_id` string
  A [saved character](/docs/api/characters) of your account instead of `input_reference`: its turnaround sheet is the picture. Send exactly one of the two.
- `consent` boolean, required
  `true` confirms that you accept the [MiniMax H3 terms](/docs/models/minimax-h3-license#consent) and the responsibility for following them (including its territory clause), and that everyone shown in the video and the picture is 18 or older and agreed to this use of their likeness.
- `prompt` string
  The scene: place, light, mood (up to 4,000 characters). Only the clip's motion carries over, so a described scene keeps its look; without one the model picks a plain backdrop.
- `seconds` number
  The most seconds of the clip to use, from 3 to 10. Default: the whole clip, up to 10.
- `start` number
  Where in the clip to begin, in seconds (0–3600; the [playground](/app/playground?mode=videos)'s trim tool sets it). Default: 0.
- `background` string
  `new` (default): only the clip's motion carries over and `prompt` describes the place. `keep`: the clip's own background, objects and light stay and only the person is replaced; `prompt` is not needed. Same price.
- `seed` integer
  Repeat a render with the same seed (0–2147483647). Default: random.
:::

`?wait=0–300` on the POST holds the request until the video finishes (then `200`), else answers `202` with the queued object.

## Use a saved character {#saved-character}

The new character can come with each call or from your [saved characters](/docs/api/characters):

- **One-off:** `input_reference` — the picture, as a file or a URL ([above](#swap-a-character)).
- **Saved:** `character_id` — a character you saved with `POST /v1/characters`; its turnaround sheet is the picture. The video object's `character_id` shows it.

```bash title="cURL"
curl https://lovelyrouter.com/v1/videos \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F model=minimax-h3-character-swap \
  -F video=@clip.mp4 \
  -F character_id=chr_3b9e1f0c7a2d4e58b6c1a9f2 \
  -F consent=true
```

Sending both, or a `character_id` that isn't one of yours, is refused (`400` / `404`).

## Whole videos {#whole-videos}

`POST /v1/videos/replacements` takes a whole video — up to 3 minutes, with cuts and several people. Its shots and everyone in it are found first ($0.01 per second); then you map each person to a character (or keep them) and choose the whole person or only the head (face, hair and anything worn on it). Every shot with a swapped person is re-rendered and the shots are joined back over the original sound; you pay per second swapped ($0.20 per second for the SFW model, rendered on FastH3 Omni; AfterMidnight $0.12). The [video replacement endpoints](/docs/api/video-replacements) page has the whole flow. The [playground](/app/playground?mode=videos&tool=swap) uses it for every clip, short or long.

## Video shots {#shots}

`minimax-h3-video` makes video from scratch: `POST /v1/videos/shots` takes a prompt in which your [saved characters and scenes](/docs/api/characters#scenes) are tagged (`@Mia_Wong`, `@[Night Café]`), and every tagged one becomes a reference picture — up to 9 per shot. Quoted words are spoken by the character tagged last before them. Render the shots of a story one by one, then join them with `POST /v1/videos/joins` (free). The [video shot endpoints](/docs/api/video-shots) page has every field, the pricing and the projects API the playground uses for storyboards.  A character with a [voice](/docs/api/characters#voices) speaks in it (up to 3 voices a shot).  Pronouns outside the dialogue are replaced by the right tag before rendering ([the prompt sweep](/docs/api/video-shots#sweep)).

```bash title="cURL"
curl https://lovelyrouter.com/v1/videos/shots \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model": "minimax-h3-video", "consent": true, "seconds": 5,
       "prompt": "In @[Night Café], @Leo looks up as @Mia_Wong sits down: \"You came.\"",
       "characters": ["chr_3b9e1f0c7a2d4e58b6c1a9f2", "chr_1e4b7d0a9c3f2e5b8a6d0c1f"],
       "scenes": ["scn_5b2d9e0c1a7f4b3e8d6c2a1f"]}'
```

## The video object {#video-object}

```json title="Response"
{
  "id": "video_4f9d0c2b8e1a47d6a3c5b7e2",
  "object": "video",
  "model": "minimax-h3-character-swap",
  "status": "completed",
  "progress": 100,
  "created_at": 1792915200,
  "completed_at": 1792915288,
  "expires_at": 1793520088,
  "seconds": "5.17",
  "size": "768x1376",
  "prompt": "In front of a weathered concrete wall, soft window light",
  "character_id": null,
  "error": null,
  "url": "https://lovelyrouter.com/v1/videos/video_4f9d0c2b8e1a47d6a3c5b7e2/content",
  "charge_usd": 0.3875,
  "powered_by": "MiniMax H3"
}
```

`status` is `queued`, `in_progress`, `completed`, `failed` or `cancelled`. A failed video has `error: {code, message}` and costs nothing. `size` follows the clip's orientation on a 768-pixel short edge; the frame rate is 24 fps.

## Endpoints {#endpoints}

| Request | What it does |
| --- | --- |
| `POST /v1/videos` | Start a video. |
| `GET /v1/videos/{id}` | The video object. `?wait=1–300` holds until it changes from `queued`/`in_progress` or the time runs out. |
| `GET /v1/videos/{id}/content` | The mp4 (`Range` requests work, so players can seek). Kept for 7 days. |
| `GET /v1/videos` | Your recent videos, newest first: `?limit=1–100`, `?after=<id>` for the next page. |
| `DELETE /v1/videos/{id}` | Cancel a queued video (nothing is charged), or delete a finished video's file. |

## Pricing {#pricing}

You pay per second of video you get back, 3 to 10 seconds per clip (a shorter result is billed as 3 seconds): a 5.2-second clip costs $0.52. When you start a video, the most it can cost ($1.00, or `seconds` × the price) is reserved from your balance; the difference comes back when it finishes. Failed and cancelled videos cost nothing.

## Getting good results {#tips}

- **The clip**: one person, clearly visible, moving at a natural pace. Fast action and heavy occlusion are harder. Speech stays in sync because the soundtrack is kept and guides the mouth.
- **The picture**: the person alone, face visible, evenly lit, on a plain background. Its lighting and backdrop aren't copied, but strong colored light can bleed in. A character sheet — the same person from the front, three-quarter, side and back in one picture — works best when the camera moves around them; it still renders one person.
- **The scene**: describe the place in `prompt` to keep it close to the original, e.g. "a weathered concrete wall, soft window light".
- Hair *shape* follows the clip (a braid in the clip stays a braid); hair color, face and clothes follow the picture.

## Errors {#errors}

| Status | `code` / `type` | Why |
| --- | --- | --- |
| `400` | `consent_required` | `consent` wasn't `true`. |
| `400` | `invalid_request_error` | A missing or unreadable file, a file that's too large, `seconds` out of range, both `input_reference` and `character_id`. |
| `402` | `insufficient_quota` | The balance doesn't cover the reserve. |
| `404` | `not_found_error` | Unknown model or video, a model that doesn't make videos, or a `character_id` that isn't one of your saved characters. |
| `429` | `rate_limit_exceeded` | Five videos are already queued or rendering on your account, or your key's rate limit. |

Misuse can be reported at [/report](/report).
