Media API
Generate videos
Video generation runs asynchronously. Submit a job, poll its Edy URL until it reaches a terminal status, then download the result through the authenticated content URL.
1. Find a video model
curl "https://edycode.vercel.app/api/v1/media/models?type=video"const response = await fetch("https://edycode.vercel.app/api/v1/media/models?type=video");
if (!response.ok) throw new Error(await response.text());
const { data: videoModels } = await response.json();import requests
response = requests.get("https://edycode.vercel.app/api/v1/media/models?type=video", timeout=30)
response.raise_for_status()
video_models = response.json()["data"]The response identifies the public slug, provider, default status, capabilities, and MWK price per rendered second. Omitting model uses the configured default video model.
2. Submit a video job
curl -X POST https://edycode.vercel.app/api/v1/videos/generations \
-H "Authorization: Bearer $EDY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "edy-hailuo",
"prompt": "A fishing boat crossing Lake Malawi at sunrise, slow aerial camera",
"duration": 6,
"resolution": "480p",
"aspect_ratio": "16:9",
"generate_audio": false
}'import { writeFile } from "node:fs/promises";
const baseUrl = "https://edycode.vercel.app";
const headers = {
Authorization: "Bearer " + process.env.EDY_API_KEY,
"Content-Type": "application/json",
};
const submitted = await fetch(baseUrl + "/api/v1/videos/generations", {
method: "POST",
headers,
body: JSON.stringify({
model: "edy-hailuo",
prompt: "A fishing boat crossing Lake Malawi at sunrise, slow aerial camera",
duration: 6,
resolution: "480p",
aspect_ratio: "16:9",
generate_audio: false,
}),
});
if (!submitted.ok) throw new Error(await submitted.text());
let job = await submitted.json();
while (job.status === "pending" || job.status === "in_progress") {
await new Promise((resolve) => setTimeout(resolve, 30_000));
const polled = await fetch(baseUrl + job.polling_url, { headers });
if (!polled.ok) throw new Error(await polled.text());
job = await polled.json();
}
if (job.status !== "completed") throw new Error("Video job " + job.status);
const content = await fetch(baseUrl + job.data[0].url, { headers });
if (!content.ok) throw new Error(await content.text());
await writeFile("result.mp4", Buffer.from(await content.arrayBuffer()));import os
import time
import requests
base_url = "https://edycode.vercel.app"
headers = {
"Authorization": f"Bearer {os.environ['EDY_API_KEY']}",
"Content-Type": "application/json",
}
response = requests.post(
base_url + "/api/v1/videos/generations",
headers=headers,
json={
"model": "edy-hailuo",
"prompt": "A fishing boat crossing Lake Malawi at sunrise, slow aerial camera",
"duration": 6,
"resolution": "480p",
"aspect_ratio": "16:9",
"generate_audio": False,
},
timeout=300,
)
response.raise_for_status()
job = response.json()
while job["status"] in ("pending", "in_progress"):
time.sleep(30)
response = requests.get(base_url + job["polling_url"], headers=headers, timeout=60)
response.raise_for_status()
job = response.json()
if job["status"] != "completed":
raise RuntimeError(f"Video job {job['status']}")
content = requests.get(base_url + job["data"][0]["url"], headers=headers, timeout=300)
content.raise_for_status()
with open("result.mp4", "wb") as video_file:
video_file.write(content.content)model — Optional public video model slug.
prompt — Required for text-to-video and optional when image is supplied.
duration — Rendered duration from 1–30 seconds. The selected model may support a smaller set.
resolution — Optional provider-supported resolution such as 480p or 720p.
size — Optional exact WIDTHxHEIGHT dimensions. Use this instead of resolution plus aspect_ratio when supported.
aspect_ratio — Optional output ratio such as 16:9, 9:16, or 1:1.
generate_audio — Optional boolean for models that support generated audio.
seed — Optional non-negative integer for repeatable output where supported.
frame_images — Optional one or two exact frames labelled first_frame or last_frame.
input_references — Optional visual style/content references for compatible OpenRouter models.
Image-to-video and reference-to-video
Use frame_images when a picture must be the exact first or last frame. Use input_references when pictures should guide style or content without becoming exact frames. If both are supplied, Edy rejects the request so intent stays unambiguous.
{
"model": "edy-hailuo",
"frame_images": [{
"type": "image_url",
"image_url": { "url": "https://example.com/frame.png" },
"frame_type": "first_frame"
}],
"prompt": "Gentle waves move while the camera slowly pushes forward",
"duration": 6,
"resolution": "720p"
}{
"model": "edy-wan-video",
"input_references": [
{ "type": "image_url", "image_url": { "url": "https://example.com/style.png" } }
],
"prompt": "A solar flare beside a planet in this visual style",
"duration": 6
}For local files, use multipart upload fields first_frame,last_frame, or repeated input_references.
curl -X POST https://edycode.vercel.app/api/v1/videos/generations \
-H "Authorization: Bearer $EDY_API_KEY" \
-F "model=edy-hailuo" \
-F "prompt=Gentle waves move while the camera pushes forward" \
-F "duration=6" \
-F "first_frame=@./lake.png"import { readFile } from "node:fs/promises";
const form = new FormData();
form.set("model", "edy-hailuo");
form.set("prompt", "Gentle waves move while the camera pushes forward");
form.set("duration", "6");
form.append("first_frame", new Blob([await readFile("./lake.png")], { type: "image/png" }), "lake.png");
const response = await fetch("https://edycode.vercel.app/api/v1/videos/generations", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.EDY_API_KEY },
body: form,
});
if (!response.ok) throw new Error(await response.text());
const job = await response.json();import os
import requests
with open("./lake.png", "rb") as image_file:
response = requests.post(
"https://edycode.vercel.app/api/v1/videos/generations",
headers={"Authorization": f"Bearer {os.environ['EDY_API_KEY']}"},
data={"model": "edy-hailuo", "prompt": "Animate the lake", "duration": "6"},
files={"first_frame": ("lake.png", image_file, "image/png")},
timeout=300,
)
response.raise_for_status()
job = response.json()Uploaded images follow the same 10 MB per-file and 30 MB combined limits. OpenRouter models may accept first and last frames or style references. Grokified video accepts one first-frame image.
3. Poll the Edy job
{
"id": "cmf123...",
"object": "video.generation",
"status": "pending",
"model": "grok-imagine-1-0",
"polling_url": "/api/v1/videos/cmf123..."
}curl https://edycode.vercel.app/api/v1/videos/cmf123... \
-H "Authorization: Bearer $EDY_API_KEY"Poll about every 30 seconds. Status is pending, in_progress, completed, failed, cancelled, or expired.
4. Download the completed video
{
"id": "cmf123...",
"object": "video.generation",
"status": "completed",
"model": "grok-imagine-1-0",
"data": [{ "url": "/api/v1/videos/cmf123.../content?index=0" }],
"usage": { "credits": 30 }
}curl "https://edycode.vercel.app/api/v1/videos/cmf123.../content?index=0" \
-H "Authorization: Bearer $EDY_API_KEY" \
--output result.mp4The Edy content endpoint verifies that the job belongs to the authenticated user.
Billing and retries
Video is charged using the model's price per second multiplied by requested duration. Polling never creates a second charge. Failed, cancelled, and expired jobs release the reservation.
See the error reference for authentication, balance, and upstream errors.