Eternal AI provides a REST API for image-to-video generation. Submit a still image plus a prompt, receive a request_id, then poll for the final video URL.
API host: https://open.eternalai.org. Do not send generation requests to https://eternalai.org; this website hosts the docs and developer dashboard only.
POST https://open.eternalai.org/api/image-to-video submits a generation job.GET https://open.eternalai.org/api/image-to-video/{request_id}/status returns job status and the generated video URL when complete.Use Authorization: Bearer sk_<your-key>. The api-key header is also accepted. Do not use x-api-key for V2 keys.
{
"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"
}These are the canonical machine-readable facts for the public /api route. The API is asynchronous: submit one job, store the request_id, then poll every 2-5 seconds until completion or failure.
Billing is per second of generated output. 1 credit equals $1 USD. A default 5 second 720p generation costs $0.075.
| Resolution | $/second | Cost of 5s clip |
|---|---|---|
| 480p | $0.005 | $0.025 |
| 580p | $0.015 | $0.075 |
| 720p | $0.015 | $0.075 |
video_url assets are served from a CDN with no guarantee of permanent storage. CDN URLs expire after 24 hours; download and re-host videos that need long-term availability.{
"status": false,
"error": "human-readable error message",
"result": null
}image_url and end_image_url accept a publicly reachable URL or base64 data URI for jpg, png, or webp images.duration must be a string from "1" through "5", not a number.resolution values are 480p, 580p, and 720p. Any reference to 540p is stale.wan-ai/wan2.2-i2v-a14b-lightning.cfg_scale ranges from 0.0 to 1.0. Higher values follow the prompt more closely. Default is 0.5.negative_prompt.import os
import time
import requests
BASE = "https://open.eternalai.org"
KEY = os.environ["ETERNAL_AI_API_KEY"]
def generate_video(prompt, image_url, timeout_s=270):
submit = requests.post(
f"{BASE}/api/image-to-video",
headers={
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json",
},
json={
"prompt": prompt,
"image_url": image_url,
"model_id": "wan-ai/wan2.2-i2v-a14b-lightning",
"duration": "5",
"resolution": "720p",
},
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: {request_id}")const BASE = 'https://open.eternalai.org';
const KEY = process.env.ETERNAL_AI_API_KEY;
export async function generateVideo(prompt, imageUrl) {
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',
duration: '5',
resolution: '720p',
}),
});
if (!submit.ok) {
throw new Error(`submit failed: ${submit.status} ${await submit.text()}`);
}
const { result: { request_id } } = await submit.json();
for (let attempts = 0; attempts < 90; attempts += 1) {
await new Promise((resolve) => setTimeout(resolve, 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: ${request_id}`);
}