> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ltx.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ltx.io/_mcp/server.

# Migrate from V1 to V2

> Migrate an LTX API integration from synchronous V1 video generation endpoints to the asynchronous V2 job API.

The V1 video generation endpoints listed below will no longer be available after October 26, 2026 at 11:59 PM UTC. Migrate your integration to V2 before then.

V2 uses the same authentication and generation request parameters as V1. The main change is the response flow: V1 keeps one HTTP request open and returns the generated file, while V2 creates a background job that you poll for its result.

## Endpoint mapping

Change the version in each generation endpoint:

| V1 endpoint               | V2 endpoint               |
| ------------------------- | ------------------------- |
| `POST /v1/text-to-video`  | `POST /v2/text-to-video`  |
| `POST /v1/image-to-video` | `POST /v2/image-to-video` |
| `POST /v1/audio-to-video` | `POST /v2/audio-to-video` |
| `POST /v1/retake`         | `POST /v2/retake`         |
| `POST /v1/extend`         | `POST /v2/extend`         |

> **Note**
>
> The media upload endpoint remains `POST /v1/upload`. You do not need to change uploaded `ltx://` URIs or replace `/v1/upload` as part of this migration.

## What changes

| V1                                                        | V2                                                                         |
| --------------------------------------------------------- | -------------------------------------------------------------------------- |
| `POST` returns `200 OK` after generation finishes.        | `POST` returns `202 Accepted` immediately.                                 |
| The response body is the generated MP4 file.              | The response body is JSON containing a job `id`.                           |
| One connection remains open during generation.            | Poll `GET /v2/{endpoint}/{id}` until the job finishes.                     |
| A generation failure is returned by the original request. | A generation failure appears as `status: "failed"` with an `error` object. |
| Concurrency limits can return `concurrency_limit_error`.  | Queue limits can return `rate_limit_error`.                                |

Your API key, request body, model selection, input URIs, and pricing do not change.

## Migrate a request

For example, this V1 request waits for generation and writes the response body directly to a file:

**`Python`**

```python title="Python"
import requests

response = requests.post(
    "https://api.ltx.io/v1/text-to-video",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "prompt": "A majestic eagle soaring through clouds at sunset",
        "model": "ltx-2-5-pro",
        "duration": 8,
        "resolution": "1920x1080",
    },
)
response.raise_for_status()

with open("video.mp4", "wb") as output:
    output.write(response.content)
```

**`TypeScript`**

```typescript title="TypeScript"
import { writeFile } from "node:fs/promises";

const response = await fetch("https://api.ltx.io/v1/text-to-video", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    prompt: "A majestic eagle soaring through clouds at sunset",
    model: "ltx-2-5-pro",
    duration: 8,
    resolution: "1920x1080",
  }),
});
if (!response.ok) {
  throw new Error(`Generation failed with status ${response.status}`);
}

await writeFile("video.mp4", Buffer.from(await response.arrayBuffer()));
```

**`cURL`**

```curl title="cURL"
curl -X POST https://api.ltx.io/v1/text-to-video \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A majestic eagle soaring through clouds at sunset",
    "model": "ltx-2-5-pro",
    "duration": 8,
    "resolution": "1920x1080"
  }' \
  -o video.mp4
```

With V2, send the same body to the V2 endpoint. The response contains a job ID:

**`Python`**

```python title="Python"
import requests

response = requests.post(
    "https://api.ltx.io/v2/text-to-video",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "prompt": "A majestic eagle soaring through clouds at sunset",
        "model": "ltx-2-5-pro",
        "duration": 8,
        "resolution": "1920x1080",
    },
)
response.raise_for_status()
job = response.json()
```

**`TypeScript`**

```typescript title="TypeScript"
const response = await fetch("https://api.ltx.io/v2/text-to-video", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    prompt: "A majestic eagle soaring through clouds at sunset",
    model: "ltx-2-5-pro",
    duration: 8,
    resolution: "1920x1080",
  }),
});
if (!response.ok) {
  throw new Error(`Submit failed with status ${response.status}`);
}
const job = await response.json();
```

**`cURL`**

```curl title="cURL"
curl -X POST https://api.ltx.io/v2/text-to-video \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A majestic eagle soaring through clouds at sunset",
    "model": "ltx-2-5-pro",
    "duration": 8,
    "resolution": "1920x1080"
  }'
```

**`202 Accepted`**

```json title="202 Accepted"
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created_at": "2026-09-06T12:00:00.000Z"
}
```

Poll the matching endpoint with that ID:

**`Python`**

```python title="Python"
response = requests.get(
    f"https://api.ltx.io/v2/text-to-video/{job['id']}",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
response.raise_for_status()
job = response.json()
```

**`TypeScript`**

```typescript title="TypeScript"
const pollResponse = await fetch(
  `https://api.ltx.io/v2/text-to-video/${job.id}`,
  { headers: { Authorization: "Bearer YOUR_API_KEY" } },
);
if (!pollResponse.ok) {
  throw new Error(`Poll failed with status ${pollResponse.status}`);
}
const updatedJob = await pollResponse.json();
```

**`cURL`**

```curl title="cURL"
curl https://api.ltx.io/v2/text-to-video/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

When `status` is `completed`, download the output from `result.video_url`:

**`Completed job`**

```json title="Completed job"
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "completed",
  "created_at": "2026-09-06T12:00:00.000Z",
  "completed_at": "2026-09-06T12:02:30.000Z",
  "result": {
    "video_url": "https://storage.googleapis.com/example/video.mp4"
  }
}
```

## Update your application flow

Replace code that reads a video from the `POST` response with a submit, poll, and download loop.

We recommend waiting at least **5 seconds** between polls. Choose a slightly different delay each time, to spread out requests when polling multiple jobs. Stop when the status is `completed` or `failed`.

These minimal examples stop on HTTP errors. The cURL version uses [`jq`](https://jqlang.github.io/jq/) to read fields out of the JSON responses. For production, add retries for polling and downloads as described in the [production checklist](#production-checklist).

**`Python`**

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

api_key = "YOUR_API_KEY"
headers = {"Authorization": f"Bearer {api_key}"}
endpoint = "text-to-video"

submit_response = requests.post(
    f"https://api.ltx.io/v2/{endpoint}",
    headers={**headers, "Content-Type": "application/json"},
    json={
        "prompt": "A majestic eagle soaring through clouds at sunset",
        "model": "ltx-2-5-pro",
        "duration": 8,
        "resolution": "1920x1080",
    },
)
submit_response.raise_for_status()
job = submit_response.json()

while True:
    time.sleep(random.uniform(5, 6))
    poll_response = requests.get(
        f"https://api.ltx.io/v2/{endpoint}/{job['id']}",
        headers=headers,
    )
    poll_response.raise_for_status()
    job = poll_response.json()

    if job["status"] == "completed":
        break
    if job["status"] == "failed":
        raise RuntimeError(job["error"]["message"])

video_response = requests.get(job["result"]["video_url"])
video_response.raise_for_status()
with open("video.mp4", "wb") as output:
    output.write(video_response.content)
```

**`TypeScript`**

```typescript title="TypeScript"
import { writeFile } from "node:fs/promises";

const apiKey = "YOUR_API_KEY";
const headers = { Authorization: `Bearer ${apiKey}` };
const endpoint = "text-to-video";

const submitResponse = await fetch(`https://api.ltx.io/v2/${endpoint}`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "A majestic eagle soaring through clouds at sunset",
    model: "ltx-2-5-pro",
    duration: 8,
    resolution: "1920x1080",
  }),
});
if (!submitResponse.ok) {
  throw new Error(`Submit failed with status ${submitResponse.status}`);
}

let job = await submitResponse.json();
while (job.status !== "completed") {
  if (job.status === "failed") {
    throw new Error(job.error.message);
  }

  const pollDelayMs = 5000 + Math.random() * 1000;
  await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
  const pollResponse = await fetch(
    `https://api.ltx.io/v2/${endpoint}/${job.id}`,
    { headers },
  );
  if (!pollResponse.ok) {
    throw new Error(`Poll failed with status ${pollResponse.status}`);
  }
  job = await pollResponse.json();
}

const videoResponse = await fetch(job.result.video_url);
if (!videoResponse.ok) {
  throw new Error(`Download failed with status ${videoResponse.status}`);
}
await writeFile("video.mp4", Buffer.from(await videoResponse.arrayBuffer()));
```

**`cURL`**

```curl title="cURL"
set -euo pipefail

api_key="YOUR_API_KEY"
endpoint="text-to-video"

job=$(curl -sS --fail-with-body -X POST "https://api.ltx.io/v2/$endpoint" \
  -H "Authorization: Bearer $api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A majestic eagle soaring through clouds at sunset",
    "model": "ltx-2-5-pro",
    "duration": 8,
    "resolution": "1920x1080"
  }')
id=$(jq -r .id <<<"$job")

while true; do
  sleep $((5 + RANDOM % 2))
  job=$(curl -sS --fail-with-body "https://api.ltx.io/v2/$endpoint/$id" \
    -H "Authorization: Bearer $api_key")

  case "$(jq -r .status <<<"$job")" in
    completed) break ;;
    failed)
      jq -r .error.message <<<"$job" >&2
      exit 1
      ;;
  esac
done

curl -sS --fail-with-body -L -o video.mp4 "$(jq -r .result.video_url <<<"$job")"
```

## Production checklist

* Use the recommended polling interval above and stop on both `completed` and `failed`.
* Retry transient network and `5xx` errors on polling and download `GET` requests with exponential backoff.
* Handle errors from both the initial `POST` and a job with `status: "failed"`.
* Download or re-host outputs as soon as the job completes. Job status is kept for up to **24 hours**, and output URLs expire independently of job status. See [retention](/async-jobs#retention).
* Update handling for `429` responses. V2 can return `rate_limit_error` when your organization's job queue is full.
* Keep logging the `x-request-id` response header. See [Debugging Requests](/debugging).
* Test each endpoint your integration uses before switching production traffic.

See [Async Jobs](/async-jobs) for the complete job lifecycle and [Error Handling](/errors) for retry guidance and error types.