---
name: eternal-ai-image-to-video
description: Use when the user wants to generate, animate, or produce a short video from a still image - including phrasings like "turn this image into a video", "animate this photo", "make a video clip from a picture", "image-to-video", "i2v", or any request that references the Eternal AI video API. Submits a job to Eternal AI's Image-to-Video endpoint and polls until the video is ready.
---

# Eternal AI - Image-to-Video

Generate a short video from a still image using Eternal AI's API. The flow is asynchronous: submit a job, then poll for status until a `video_url` is returned.

## Before you start

You need an Eternal AI API key (format: `sk_...`).

1. Look for the key in this order:
   - environment variable `ETERNAL_AI_API_KEY`
   - a `.env` or `.env.local` file in the project
2. If no key is found, ask the user for one. Do **not** invent a key or proceed without it.

You also need a publicly reachable URL for the starting image. If the user gives you a local file path, tell them they need a public URL (jpg/png/webp) - the API cannot read local files.

## API reference

**Base URL:** `https://open.eternalai.org`

**Auth header (every request):**

```
Authorization: Bearer sk_<API_KEY>
```

(Alternative for clients that can't set `Authorization`: `api-key: sk_<API_KEY>`.)

**Only supported model:** `wan-ai/wan2.2-i2v-a14b-lightning`

### 1. Submit - `POST /api/image-to-video`

Request body:

| Field             | Type    | Required | Notes                                                                                                                                                                                                                  |
| ----------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt`          | string  | yes      | Text description of the video. Keep it concise.                                                                                                                                                                        |
| `image_url`       | string  | yes      | Public URL or base64 data URI of the starting still (jpg/png/webp). Maximum image file size is 15 MB.                                                                                                                  |
| `model_id`        | string  | yes      | Use `"wan-ai/wan2.2-i2v-a14b-lightning"`.                                                                                                                                                                              |
| `end_image_url`   | string  | no       | Optional end frame for two-image conditioning.                                                                                                                                                                         |
| `duration`        | string  | no       | Seconds. Default `"5"`. **Must be a string, not a number.** Sending `3` returns `"invalid JSON body"` even though the JSON parses fine — the failure is schema-type validation. Always `String(value)` before sending. |
| `aspect_ratio`    | string  | no       | `"16:9"`, `"9:16"`, `"1:1"`, `"4:3"`, `"3:4"`.                                                                                                                                                                         |
| `resolution`      | string  | no       | `"480p"`, `"580p"`, `"720p"`. Affects pricing.                                                                                                                                                                         |
| `negative_prompt` | string  | no       | Things to avoid.                                                                                                                                                                                                       |
| `cfg_scale`       | number  | no       | 0.0 - 1.0. Higher = closer to prompt. Default 0.5.                                                                                                                                                                     |
| `seed`            | integer | no       | Deterministic seed. Omit for random.                                                                                                                                                                                   |

200 OK response:

```json
{
  "status": true,
  "error": null,
  "result": { "request_id": "9a4f1c84-2bb3-4d29-a4e1-7c0d3b8f5a91" }
}
```

Keep `result.request_id` - you need it for the poll step.

### 2. Poll - `GET /api/image-to-video/{request_id}/status`

Poll every **2-5 seconds** (default to 3). Stop when `result.status` is `"completed"` or `"failed"`.

200 OK response (completed):

```json
{
  "status": true,
  "error": null,
  "result": {
    "request_id": "9a4f1c84-2bb3-4d29-a4e1-7c0d3b8f5a91",
    "status": "completed",
    "progress": 100,
    "video_url": "https://cdn.example.com/results/9a4f1c84.mp4",
    "duration": "5",
    "aspect_ratio": "16:9",
    "resolution": "720p"
  }
}
```

Status lifecycle: `pending` -> `processing` -> `completed` (with `video_url`) or `failed` (with `error`).

## Pricing (so you can warn the user before submitting)

- 480p: 0.005 credits/second (5s clip = 0.025 credits)
- 580p: 0.015 credits/second (5s clip = 0.075 credits)
- 720p: 0.015 credits/second (5s clip = 0.075 credits)

1 credit = $1 USD. Credit is deducted before dispatch and auto-refunded if the backend rejects the job. There is no refund if the user cancels client-side after the job is accepted.

## Workflow

When the user asks you to generate or animate a video:

1. **Collect inputs**
   - `prompt` - what should happen in the video
   - `image_url` - public URL of the starting frame
   - `resolution` and `duration` if specified, otherwise default to `720p` and `5`
2. **Confirm cost** if the user hasn't given explicit go-ahead (e.g. "This will cost ~0.075 credits / $0.075. Proceed?"). Skip the confirmation if the user already said "go" or specified the resolution.
3. **Submit** the job. Capture `request_id`.
4. **Poll** every 3 seconds. Cap at ~90 polls (~4.5 minutes) before reporting timeout - generations normally complete in 30-90 seconds.
5. **Return** the `video_url`. Mention that the CDN URL expires about 24 hours after generation - the user should download or re-host if they need long-term access.

## Error handling

| HTTP | Meaning                                       | What to do                                                    |
| ---- | --------------------------------------------- | ------------------------------------------------------------- |
| 400  | Invalid request body / unsupported `model_id` | Show the server's `error` message, fix the request, retry.    |
| 401  | Missing / invalid / inactive API key          | Stop. Ask the user to check the key. Do not retry.            |
| 402  | Insufficient credits                          | Stop. Tell the user to top up at `/api/top-up`. Do not retry. |
| 403  | `request_id` belongs to a different key       | Stop. The user is polling with the wrong key.                 |
| 404  | No such `request_id`                          | Stop. The id is wrong or expired.                             |
| 500  | Persistence failure                           | Retry once after a short delay.                               |
| 502  | Backend rejected (credit auto-refunded)       | Retry once. If it fails again, surface the error.             |

On polling: if the status endpoint returns 200 but the backend is upstream-down, it still returns the last cached status, so it is safe to keep polling.

## Reference curl commands

Submit:

```bash
curl -X POST https://open.eternalai.org/api/image-to-video \
  -H "Authorization: Bearer $ETERNAL_AI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A cat slowly turning its head toward the camera",
    "image_url": "https://example.com/cat-start.jpg",
    "model_id": "wan-ai/wan2.2-i2v-a14b-lightning",
    "duration": "5",
    "resolution": "720p"
  }'
```

Poll:

```bash
curl -X GET "https://open.eternalai.org/api/image-to-video/<REQUEST_ID>/status" \
  -H "Authorization: Bearer $ETERNAL_AI_API_KEY"
```

## Reference Python (synchronous, full e2e)

```python
import os
import time
import requests

BASE = "https://open.eternalai.org"
KEY = os.environ["ETERNAL_AI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}

def generate_video(prompt: str, image_url: str, *, resolution: str = "720p", duration: str = "5", timeout_s: int = 270) -> str:
    submit = requests.post(
        f"{BASE}/api/image-to-video",
        json={
            "prompt": prompt,
            "image_url": image_url,
            "model_id": "wan-ai/wan2.2-i2v-a14b-lightning",
            "resolution": resolution,
            "duration": duration,
        },
        headers=HEADERS,
        timeout=30,
    )
    submit.raise_for_status()
    request_id = submit.json()["result"]["request_id"]

    deadline = time.time() + timeout_s
    while time.time() < deadline:
        time.sleep(3)
        poll = requests.get(
            f"{BASE}/api/image-to-video/{request_id}/status",
            headers={"Authorization": f"Bearer {KEY}"},
            timeout=30,
        )
        poll.raise_for_status()
        result = poll.json()["result"]
        if result["status"] == "completed":
            return result["video_url"]
        if result["status"] == "failed":
            raise RuntimeError(result.get("error") or "generation failed")
    raise TimeoutError(f"video not ready after {timeout_s}s (request_id={request_id})")
```

## Reference JavaScript (Node 18+ / browser, full e2e)

```javascript
const BASE = 'https://open.eternalai.org';
const KEY = process.env.ETERNAL_AI_API_KEY;

export async function generateVideo({
  prompt,
  imageUrl,
  resolution = '720p',
  duration = '5',
  timeoutMs = 270_000,
}) {
  const submit = await fetch(`${BASE}/api/image-to-video`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      prompt,
      image_url: imageUrl,
      model_id: 'wan-ai/wan2.2-i2v-a14b-lightning',
      resolution,
      duration,
    }),
  });
  if (!submit.ok)
    throw new Error(`submit failed: ${submit.status} ${await submit.text()}`);
  const {
    result: { request_id },
  } = await submit.json();

  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, 3000));
    const poll = await fetch(
      `${BASE}/api/image-to-video/${request_id}/status`,
      { headers: { Authorization: `Bearer ${KEY}` } },
    );
    if (!poll.ok)
      throw new Error(`poll failed: ${poll.status} ${await poll.text()}`);
    const { result } = await poll.json();
    if (result.status === 'completed') return result.video_url;
    if (result.status === 'failed')
      throw new Error(result.error || 'generation failed');
  }
  throw new Error(
    `video not ready in ${timeoutMs}ms (request_id=${request_id})`,
  );
}
```

## Resources

- Human-readable docs: https://eternalai.org/api/docs
- OpenAPI spec: https://eternalai.org/.well-known/openapi.json
- AI agent integration page: https://eternalai.org/ai-agent-api.html
- Top up credits: https://eternalai.org/api/top-up
- Manage API keys: https://eternalai.org/api/keys
