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}`);
}| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | required | Text description of the video. Stay concise for best results. |
image_url | string | required | Starting still (jpg/png/webp). Accepts a publicly reachable URL or a base64-encoded data URI (e.g. "data:image/jpeg;base64,…"). Maximum image file size is 15 MB. |
model_id | string | required | Generation model. Must be in the server allowlist. Current: "wan-ai/wan2.2-i2v-a14b-lightning". |
end_image_url | string | optional | End frame for two-image conditioning. Accepts a public URL or a base64-encoded data URI. Default: null. |
duration | string | optional | Output length in seconds. Supported values: "1"–"5". Default: "5". |
aspect_ratio | string | optional | "auto","16:9", "9:16", "1:1", "4:3", "3:4". Default: "auto". |
resolution | string | optional | Output resolution. Supported values: "480p", "580p", "720p". Default: "480p". |
negative_prompt | string | optional | Things to avoid. Default: "blur, distort, and low quality". |
cfg_scale | number | optional | CFG scale [0.0, 1.0]. Higher = closer to prompt. Default: 0.5. |
seed | number | optional | Deterministic seed. Default: random. |
{
"prompt": "A cat slowly turning its head toward the camera, soft natural light",
"image_url": "https://example.com/cat-start.jpg",
"model_id": "wan-ai/wan2.2-i2v-a14b-lightning",
"end_image_url": "https://example.com/cat-end.jpg",
"duration": "5",
"aspect_ratio": "16:9",
"resolution": "720p",
"negative_prompt": "blur, low quality, watermark",
"cfg_scale": 0.5,
"seed": 12345
}{
"status": true,
"error": null,
"result": {
"request_id": "9a4f1c84-2bb3-4d29-a4e1-7c0d3b8f5a91"
}
}{
"status": true,
"error": null,
"result": {
"request_id": "9a4f1c84-2bb3-4d29-a4e1-7c0d3b8f5a91",
"status": "completed",
"progress": 100,
"video_url": "https://cdn.eternalai.org/video-webhook/9a4f1c84.mp4",
"created_at": "2026-05-25T07:15:07Z"
}
}pending ──► processing ──► completed (video_url set)
└► failed (error set, no video_url)| Resolution | 1 second | 5 seconds |
|---|---|---|
| 480p | 0.005 credits | 0.025 credits |
| 580p | 0.015 credits | 0.075 credits |
| 720p | 0.015 credits | 0.075 credits |
400 Missing required field, invalid URL, bad duration: invalid request body: …400 model_id not in the allowlist: model_id "…" is not supported for image-to-video401 Missing / invalid / inactive API key: invalid api key402 Insufficient credit balance: insufficient API credit: need 0.025 credits500 Persisting the request failed: failed to persist request: …502 Generation backend rejected (credit refunded): ai submit failed: …400 Missing request_id in path: request_id is required401 Missing / invalid / inactive API key: invalid api key403 request_id belongs to a different user: request does not belong to this api key404 No such request_id: request not foundTreat them like passwords; do not commit them to source control or expose them client-side.
Use whichever fits your HTTP client.
2–5 seconds is recommended. There is no separate billing for status calls.
If the backend rejects a submission, we refund automatically. If you cancel client-side after the job is accepted, we do NOT refund — the generation is already in flight.
Submitting the same prompt+image twice creates two independent jobs and is billed twice. Use your own request deduplication if you want one-shot semantics.
Generated video_urls are served from a CDN and expire about 24 hours after generation. Download and re-host the result if you need long-term access.