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

> ## Agent Instructions
> Authenticate every request with your API key in the `X-API-Key` header or `Authorization: Bearer`. Keys start with `sk-nunchux-`.
> Image generation is synchronous: the image is in the response.
> Video generation is asynchronous: submit the job, poll its task until it reaches a terminal status, then download the output promptly, because output URLs expire.

# Image-to-video

> Animate a still image into a video clip with LTX. The clip is in the response.

# Image-to-video

Send one image and a text prompt that describes the motion. The image becomes the first frame of the clip. The response contains the generated video clip. The request is synchronous. There is no task to poll. A clip is 1 to 8 seconds long, at 1080p or 720p, with an optional soundtrack.

## Models

| Model | Model ID | Use it for |
| - | - | - |
| LTX 2.5 | `nunchux-ltx-2.5-video` | New work. The newer release, with the same controls as LTX 2.3. |
| LTX 2.3 | `nunchux-ltx-2.3-video` | Continuity with clips that you already made with LTX 2.3. |

Both models also serve [Text-to-video](/nunchux-optimized/text-to-video). See [Choosing a model](/nunchux-optimized/overview#choosing-a-model).

## Endpoint

```text theme={"system"}
POST https://api.nunchux.ai/v1/video/animations
```

The path `/v1/videos/animations` is an alias. It gives the same result and the same billing.

## Request

Every request must include your API key and a JSON content type. See [Authentication](/authentication).

<ParamField header="X-API-Key" type="string" required>
  Your API key.
</ParamField>

<ParamField header="Content-Type" type="string" required>
  Must be `application/json`.
</ParamField>

<ParamField body="model" type="string" required>
  The model that generates the clip.

  Options: `nunchux-ltx-2.5-video`, `nunchux-ltx-2.3-video`
</ParamField>

<ParamField body="prompt" type="string" required>
  A text description of the motion. Describe what moves, the camera movement and the light. Do not describe again what the image already shows.
</ParamField>

<ParamField body="messages" type="array" required>
  The input image. Send one message with one `image_url` part. The model accepts one image.

  <Expandable title="properties">
    <ParamField body="role" type="string" required>
      Must be `user`.
    </ParamField>

    <ParamField body="content" type="array" required>
      The parts of the message.

      <Expandable title="properties">
        <ParamField body="type" type="string" required>
          `image_url` for the image part.
        </ParamField>

        <ParamField body="image_url.url" type="string" required>
          The image, as a data URI (for example `data:image/jpeg;base64,...`) or as a public `https` URL. Use a PNG or JPEG image. The clip keeps the framing of the image.
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>

  You can also send the image as a top-level `image_url` string.
</ParamField>

<ParamField body="width" type="integer" default={1920}>
  Output width in pixels. Send it together with `height`. The size selects the tier, 1080p or 720p. 1080p costs more per second.

  Options: `1920` (1080p), `1280` (720p)
</ParamField>

<ParamField body="height" type="integer" default={1080}>
  Output height in pixels. Send it together with `width`.

  Options: `1080` (1080p), `720` (720p)
</ParamField>

<ParamField body="duration" type="number" default={6}>
  The clip length in seconds. Range: 1 to 8. The model renders at 24 frames per second, so the length of the clip can differ from your value by a fraction of a second. The response gives the length of the clip.
</ParamField>

<ParamField body="extra" type="object">
  Model options.

  <Expandable title="properties">
    <ParamField body="audio" type="boolean" default={false}>
      When `true`, the model generates a soundtrack with the picture. Describe the sound in the prompt.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="seed" type="integer">
  The random seed. When you do not send a seed, the model selects one and returns it in the response. The same seed with the same parameters gives the same result.
</ParamField>

<ParamField body="n" type="integer" default={1}>
  The number of clips to generate. Only 1 is supported.
</ParamField>

<ParamField body="response_format" type="string" default="b64_json">
  The format of the returned clip.

  Options: `b64_json`, `url`
</ParamField>

<Note>
  Unsupported fields

  LTX does not accept `steps`, `guidance_scale`, `negative_prompt` or `num_frames`. A request that contains one of them returns a 400.
</Note>

A minimal request body:

```json theme={"system"}
{
  "model": "nunchux-ltx-2.3-video",
  "prompt": "The scene comes to life with gentle motion",
  "width": 1280,
  "height": 720,
  "duration": 6,
  "extra": { "audio": false },
  "response_format": "b64_json",
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "image_url", "image_url": { "url": "https://example.com/first-frame.jpg" } }
      ]
    }
  ]
}
```

## Response

The response is JSON. The `data` array contains one MP4 clip, as base64 data or as a URL.

<ResponseField name="created" type="integer" required>
  The Unix timestamp of the generation.
</ResponseField>

<ResponseField name="model" type="string" required>
  The model that generated the clip.
</ResponseField>

<ResponseField name="tier" type="string" required>
  Always `radical_speed`. This field does not show the LTX tier. The `width` and `height` of the clip show it.
</ResponseField>

<ResponseField name="seed" type="integer" required>
  The seed of the clip. Send it again to get the same result.
</ResponseField>

<ResponseField name="data" type="array" required>
  The generated clip.

  <Expandable title="Item properties">
    <ResponseField name="b64_json" type="string">
      The MP4 clip as base64 data. Returned when `response_format` is `b64_json`.
    </ResponseField>

    <ResponseField name="url" type="string">
      The URL of the MP4 clip. Returned when `response_format` is `url`.
    </ResponseField>

    <ResponseField name="width" type="integer">
      The width of the clip in pixels.
    </ResponseField>

    <ResponseField name="height" type="integer">
      The height of the clip in pixels.
    </ResponseField>

    <ResponseField name="duration" type="number">
      The length of the clip in seconds.
    </ResponseField>

    <ResponseField name="frames" type="integer">
      The number of frames in the clip.
    </ResponseField>

    <ResponseField name="frame_rate" type="integer">
      The frame rate of the clip.
    </ResponseField>
  </Expandable>
</ResponseField>

```json theme={"system"}
{
  "created": 1234567890,
  "model": "nunchux-ltx-2.3-video",
  "tier": "radical_speed",
  "seed": 1873420651,
  "data": [
    {
      "b64_json": "AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQ...",
      "width": 1280,
      "height": 720,
      "duration": 6.0417,
      "frames": 145,
      "frame_rate": 24
    }
  ]
}
```

A base64 clip is large. A 6 second 1080p clip can be more than 10 MB of base64 data. Use `response_format: "url"` when you do not need the data in the response.

## Example

This example reads a local JPEG file and sends it as a data URI. A clip takes some time to render. Set the timeout of your HTTP client to 120 seconds.

<CodeGroup>
  ```bash cURL theme={"system"}
  # Encode input.jpg as base64 on one line.
  IMG_B64=$(base64 < input.jpg | tr -d '\n')

  curl -X POST https://api.nunchux.ai/v1/video/animations \
    --max-time 120 \
    -H "Content-Type: application/json" \
    -H "X-API-Key: $NUNCHUX_API_KEY" \
    -d '{
      "model": "nunchux-ltx-2.3-video",
      "prompt": "The scene comes to life with gentle motion",
      "width": 1280,
      "height": 720,
      "duration": 6,
      "extra": {"audio": false},
      "response_format": "b64_json",
      "messages": [{
        "role": "user",
        "content": [
          {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,'"$IMG_B64"'"}}
        ]
      }]
    }' | jq -r '.data[0].b64_json // error(.error.message // .detail // tostring)' | base64 -d > output.mp4
  ```

  ```python Python theme={"system"}
  import os
  import base64
  import requests

  with open("input.jpg", "rb") as f:
      image_uri = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()

  response = requests.post(
      "https://api.nunchux.ai/v1/video/animations",
      headers={
          "Content-Type": "application/json",
          "X-API-Key": os.environ["NUNCHUX_API_KEY"],
      },
      json={
          "model": "nunchux-ltx-2.3-video",
          "prompt": "The scene comes to life with gentle motion",
          "width": 1280,
          "height": 720,
          "duration": 6,
          "extra": {"audio": False},
          "response_format": "b64_json",
          "messages": [
              {
                  "role": "user",
                  "content": [{"type": "image_url", "image_url": {"url": image_uri}}],
              }
          ],
      },
      timeout=120,
  )
  response.raise_for_status()

  # Decode the base64 clip and save it. Change output.mp4 to any file name.
  clip_b64 = response.json()["data"][0]["b64_json"]
  with open("output.mp4", "wb") as f:
      f.write(base64.b64decode(clip_b64))
  ```

  ```javascript JavaScript theme={"system"}
  import { readFile, writeFile } from "node:fs/promises";

  const imageUri =
    "data:image/jpeg;base64," + (await readFile("input.jpg")).toString("base64");

  const response = await fetch("https://api.nunchux.ai/v1/video/animations", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.NUNCHUX_API_KEY,
    },
    body: JSON.stringify({
      model: "nunchux-ltx-2.3-video",
      prompt: "The scene comes to life with gentle motion",
      width: 1280,
      height: 720,
      duration: 6,
      extra: { audio: false },
      response_format: "b64_json",
      messages: [
        {
          role: "user",
          content: [{ type: "image_url", image_url: { url: imageUri } }],
        },
      ],
    }),
    signal: AbortSignal.timeout(120_000),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  const data = await response.json();

  // Decode the base64 clip and save it. Change output.mp4 to any file name.
  await writeFile("output.mp4", Buffer.from(data.data[0].b64_json, "base64"));
  ```
</CodeGroup>

## Tips

* Write the motion, not the image. Say what moves, how the camera moves, and how the light changes.
* Use an image with the framing that you want in the clip. The clip keeps the framing of the image.
* When `extra.audio` is `true`, describe the sound in the prompt.
* Start at 1080p and 6 seconds. Use 720p for faster and lower-cost previews.
* Keep the `seed` when you like a result. Then change one parameter at a time.

## Errors and limits

| Status | Code | Cause |
| - | - | - |
| 400 | none | `model` is missing or not valid, or a size value is not valid. The body is a plain `detail` string. |
| 400 | `invalid_request` | The request sets `tier` to a value other than `radical_speed`. On LTX, `width` and `height` select the tier. Do not send the `tier` field. |
| 400 | `unsupported_response_format` | `response_format` is not `b64_json` or `url`. |
| 400 | `engine_error` | The model cannot render the request, for example a size, a duration, an image that it cannot read, more than one image, or a field that it does not accept. |
| 422 | `request_too_large` | The requested size is larger than 1920x1080. `details` gives the requested value and the maximum. |
| 502 | `output_too_large` | The clip is too large to return. You are not charged. Request a shorter clip or a smaller size. |
| 504 | none | The clip did not complete in 120 seconds. Send the request again. |

A clip is billed per second of output video, at the rate of its tier: 1080p costs more per second than 720p. See the [pricing page](https://nunchux.ai/pricing) for current rates. Nunchux charges only for a successful clip. A request that fails is never billed.

Every request counts toward the requests-per-minute cap of your plan. A request also holds one simultaneous-jobs slot until the response returns. See [Rate limits](/rate-limits). For authentication and credit errors, see [Error codes](/errors).


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