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
curl "https://edycode.vercel.app/api/v1/media/models?type=video"
TypeScript
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();
Python
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
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
  }'
TypeScript
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()));
Python
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.

JSON first frame
{
  "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"
}
JSON style references
{
  "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 file upload
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"
TypeScript file upload
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();
Python file upload
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
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
curl "https://edycode.vercel.app/api/v1/videos/cmf123.../content?index=0" \
  -H "Authorization: Bearer $EDY_API_KEY" \
  --output result.mp4

The 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.