A video takes several minutes. Starting one returns a job right away; you then poll the job and download the video once it is finished. The endpoints follow the OpenAI video API.
| Method | Path | Purpose |
|---|---|---|
POST | /v1/videos | Start a video |
GET | / | Check on a video |
GET | / | Download the video |
GET | /v1/videos | List your videos |
A job goes through the statuses queued, in_progress, then completed or failed. It belongs to the workspace of the key that started it: any key from that workspace can check on it and download it.
Full exampleLink to the Full example section
# 1. Start the video
curl https://api.realmoon.ai/v1/videos \
-H "Authorization: Bearer $MOONAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "alibaba/wan-2.6", "prompt": "A wave at sunset", "seconds": "5"}'
# => {"id": "video_3c9e…", "status": "queued", …}
# 2. Poll the job (about every 10 seconds)
curl https://api.realmoon.ai/v1/videos/video_3c9e... \
-H "Authorization: Bearer $MOONAI_API_KEY"
# 3. Download the video once it is "completed"
curl https://api.realmoon.ai/v1/videos/video_3c9e.../content \
-H "Authorization: Bearer $MOONAI_API_KEY" -o wave.mp4import os
import time
from openai import OpenAI
client = OpenAI(base_url="https://api.realmoon.ai/v1", api_key=os.environ["MOONAI_API_KEY"])
job = client.videos.create(
model="alibaba/wan-2.6",
prompt="A wave at sunset",
seconds="5",
extra_body={"resolution": "720p", "aspect_ratio": "16:9"},
)
while job.status in ("queued", "in_progress"):
time.sleep(10)
job = client.videos.retrieve(job.id)
if job.status == "completed":
client.videos.download_content(job.id).write_to_file("wave.mp4")
else:
print("Failed:", job.error)import fs from 'node:fs';
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'https://api.realmoon.ai/v1', apiKey: process.env.MOONAI_API_KEY });
let job = await client.videos.create({
model: 'alibaba/wan-2.6',
prompt: 'A wave at sunset',
seconds: '5',
resolution: '720p', // Moon AI-specific parameter, sent as is
});
while (job.status === 'queued' || job.status === 'in_progress') {
await new Promise((r) => setTimeout(r, 10_000));
job = await client.videos.retrieve(job.id);
}
if (job.status === 'completed') {
const content = await client.videos.downloadContent(job.id);
fs.writeFileSync('wave.mp4', Buffer.from(await content.arrayBuffer()));
} else {
console.error('Failed:', job.error);
}Start a videoLink to the Start a video section
POST https://api.realmoon.ai/v1/videos- Authentication: an
sk-moon-…key with theinferencescope (see Authentication). - Body: JSON, or
multipart/(with theform-data Content-Lengthheader) to send the first and last frames as files. 60 MB at most. - Response: the job’s video object, with status
queued, and theX-Moon-Request-Idheader, which you use to read the cost from/.v1/ generation
ParametersLink to the Parameters section
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | yes | A video model from GET /. |
prompt | string | yes | Description of the video, 4,000 characters at most. |
seconds (or duration) | integer or string | no | Duration in seconds, one of the model’s durations. Default: the first duration of at least 5 seconds, otherwise the longest. |
resolution | string | no | One of the model’s resolutions (480p, 720p, 1080p, 4K…). Default: 720p when the model offers it, otherwise its first resolution. |
size | string | no | WIDTHxHEIGHT, for example 1280x720: the closest resolution and aspect ratio are derived from it, if you don’t give them. |
aspect_ratio | string | no | One of the model’s aspect_ratios (16:9, 9:16…). Default: the first one in the list. |
generate_audio | boolean | no | With or without sound, for models that produce sound (audio: true). Default: with sound if the model produces it. true on a silent model: 400 . |
seed | integer | no | Seed, from 0 to 2,147,483,647. |
input_reference, image or first_frame | image | no | First frame: an https:// URL, a base64 data: URL, bare base64, or a multipart file. For models where first_frame is true. |
last_frame | image | no | Last frame, together with a first frame, for models where last_frame is true. |
user | string | no | Accepted, with no effect. |
Any other parameter is rejected with 400 . An image is 20 MB at most (PNG, JPEG, WebP or GIF). An image at an https:// URL is fetched by the model provider, not by Moon AI.
ExampleLink to the Example section
curl https://api.realmoon.ai/v1/videos \
-H "Authorization: Bearer $MOONAI_API_KEY" \
-F model=kwaivgi/kling-v3.0-std \
-F prompt="The photo comes to life: the clouds drift by" \
-F seconds=5 \
-F generate_audio=false \
-F first_frame=@landscape.pngjob = client.videos.create(
model="kwaivgi/kling-v3.0-std",
prompt="The photo comes to life: the clouds drift by",
seconds="5",
input_reference=open("landscape.png", "rb"),
extra_body={"generate_audio": False},
)
print(job.id, job.status)const job = await client.videos.create({
model: 'kwaivgi/kling-v3.0-std',
prompt: 'The photo comes to life: the clouds drift by',
seconds: '5',
input_reference: fs.createReadStream('landscape.png'),
generate_audio: false, // Moon AI-specific parameter, sent as is
});
console.log(job.id, job.status);The video objectLink to the The video object section
POST /, GET / and GET / return video objects.
{
"id": "video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b",
"object": "video",
"model": "alibaba/wan-2.6",
"status": "completed",
"progress": 100,
"created_at": 1793577600,
"completed_at": 1793577780,
"seconds": "5",
"size": "1280x720",
"resolution": "720p",
"aspect_ratio": "16:9",
"generate_audio": true,
"remixed_from_video_id": null,
"error": null,
"content_url": "/v1/videos/video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b/content",
"expires_at": 1793581380,
"usage": { "seconds": 5, "credits": 2800 }
}| Field | Type | Description |
|---|---|---|
id | string | The job ID: video_ followed by 32 characters. |
object | string | Always video. |
model | string | The model you called. |
status | string | queued, in_progress, completed or failed. |
progress | integer | Progress from 0 to 100, when the provider reports it; 100 once finished. |
created_at, completed_at | integer | Timestamps in Unix seconds. completed_at is null until the job finishes. |
seconds | string | The duration requested from the model. |
size | string | The dimensions that match the resolution and aspect ratio, for example 1280x720. |
resolution, aspect_ratio, generate_audio | string, string, boolean | What was requested from the model. |
remixed_from_video_id | null | Always null, kept for compatibility. |
content_url | string | Once finished: the download URL. |
expires_at | integer | Once finished: the time until which the video stays available (about one hour after it finishes). |
error | object or null | On failure: {"code", . |
usage | object | Once the job is settled: seconds (duration) and credits (credits charged). |
error.code values for a failed job, which is never charged:
code | Cause |
|---|---|
generation_failed | The model could not make the video. |
content_policy_violation | The model’s safety system rejected the request. |
timeout | The video was not finished after 6 hours and was abandoned. |
not_found | The provider no longer knows about the job. |
Check on a videoLink to the Check on a video section
GET https://api.realmoon.ai/v1/videos/{id}Returns the up-to-date video object.
- Authentication: an
sk-moon-…key from the workspace that started the video, with theinferencescope. - Path parameter:
id, the job’svideo_…ID. - Poll the job about every 10 seconds: Moon AI does not ask the provider for the status more than once every 5 seconds.
- Reading the job is not billed.
curl https://api.realmoon.ai/v1/videos/video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b \
-H "Authorization: Bearer $MOONAI_API_KEY"job = client.videos.retrieve("video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b")
print(job.status, job.progress)const job = await client.videos.retrieve('video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b');
console.log(job.status, job.progress);Download the videoLink to the Download the video section
GET https://api.realmoon.ai/v1/videos/{id}/contentSends the video file, usually video/mp4, with a Content-Disposition header that suggests the name <id>.mp4.
- Authentication: an
sk-moon-…key from the workspace that started the video, with theinferencescope. - Before the job finishes:
409.video_not_ready - Moon AI does not keep the video: it is read from the provider at download time. Download it within an hour of completion and keep your own copy. When the provider no longer has it:
410.video_expired - At most 4 downloads at once per workspace (beyond that:
429).too_many_concurrent - Downloading is not billed.
curl https://api.realmoon.ai/v1/videos/video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b/content \
-H "Authorization: Bearer $MOONAI_API_KEY" -o wave.mp4client.videos.download_content("video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b").write_to_file("wave.mp4")const content = await client.videos.downloadContent('video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b');
fs.writeFileSync('wave.mp4', Buffer.from(await content.arrayBuffer()));List your videosLink to the List your videos section
GET https://api.realmoon.ai/v1/videosLists the workspace’s videos, most recent first.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | no | Number of videos per page, clamped between 1 and 100. 20 by default. |
after | string | no | The ID of a video in the workspace: the page starts after it. |
curl "https://api.realmoon.ai/v1/videos?limit=10" \
-H "Authorization: Bearer $MOONAI_API_KEY"# The SDK fetches the next pages as you iterate.
for video in client.videos.list(limit=10):
print(video.id, video.status)// The SDK fetches the next pages as you iterate.
for await (const video of client.videos.list({ limit: 10 })) {
console.log(video.id, video.status);
}{
"object": "list",
"data": [{ "id": "video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b", "object": "video", "status": "completed" }],
"first_id": "video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b",
"last_id": "video_3c9e2f6a8b1d4e7f9a0b2c4d6e8f1a3b",
"has_more": false
}| Field | Type | Description |
|---|---|---|
object | string | Always list. |
data | array | The page’s video objects (shortened in the example). |
first_id, last_id | string or null | First and last ID on the page. |
has_more | boolean | true if there are more videos: pass last_id in after to get the next page. |
ModelsLink to the Models section
List as of November 5, 2026; the current list, with prices per second, comes from GET /.
| Model | Durations (s) | Resolutions | Aspect ratios | Sound | First / last frame |
|---|---|---|---|---|---|
alibaba/ | 5, 10 | 480p, 720p, 1080p | 16:9, 9:16 | yes | yes / no |
alibaba/ | 2 to 10 | 720p, 1080p | 16:9, 9:16, 1:1, 4:3, 3:4 | yes | yes / yes |
alibaba/ | 2 to 15 | 480p, 720p, 1080p | 16:9, 9:16, 1:1, 4:3, 3:4 | yes | yes / yes |
kwaivgi/, kling-v3.0-pro | 3 to 15 | 720p, 1080p | 16:9, 9:16, 1:1 | yes, or without (cheaper) | yes / yes |
google/ | 4, 6, 8 | 720p, 1080p | 16:9, 9:16 | yes, or without (cheaper) | yes / yes |
google/, google/ | 4, 6, 8 | 720p, 1080p, 4K | 16:9, 9:16 | yes, or without (cheaper) | yes / yes |
minimax/ | 6, 10 | 1080p | 16:9 | no | yes / no |
x-ai/ | 1 to 15 | 480p, 720p | 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3 | no | yes / no |
runway/ | 2 to 10 | 720p | 16:9, 9:16 | no | yes / no |
CostsLink to the Costs section
- When it starts, the job holds the price of the requested duration, with a small margin, plus the first and last frames for models that charge for them.
- A finished video is charged at the actual cost reported by the provider (or, failing that, the list price), and the rest of the hold is released.
- A video that fails, is refused, is abandoned after 6 hours, or whose start was cut off by our timeout costs nothing.
- The hold of a job in progress counts against the balance and the spend caps until the job ends.
See Costs and wallet.
LimitsLink to the Limits section
- At most 20 videos in progress at once per workspace (beyond that:
429,too_many_concurrent Retry-After:).30 - Starting a video responds within about one minute at most.
- Moon AI keeps the job’s state (status, duration, resolution, aspect ratio, cost), not your description or the video.
ErrorsLink to the Errors section
| HTTP | code | Endpoint | Meaning |
|---|---|---|---|
| 400 | missing_required_parameter | start | model or prompt is missing. |
| 400 | unknown_parameter | start | Unknown parameter. |
| 400 | unsupported_value | start | Duration, resolution, aspect ratio, size or generate_audio outside what the model accepts. |
| 400 | unsupported_parameter | start | A first or last frame on a model that does not accept it, or a parameter the model rejects (with a hint). |
| 400 | string_above_max_length | start | Description longer than 4,000 characters. |
| 400 | invalid_image | start | Unreadable image, wrong type, or too large. |
| 400 | content_policy_violation | start | The model’s safety system rejected the request. |
| 400 | null | start, list | Invalid seed or generate_audio, last frame without a first frame, invalid base64, unknown after. |
| 403 | org_policy | start | Your organization’s rules refuse this model. |
| 404 | model_not_found | start | Unknown model, or one that does not serve this endpoint. |
| 404 | video_not_found | check, download | No video in this workspace has this ID. |
| 409 | video_not_ready | download | The video is not finished yet. |
| 410 | video_expired | download | The video is no longer available at the provider. |
| 411 | length_required | start | Multipart upload without a Content-Length header. |
| 413 | request_too_large, file_too_large | start | Body or image too large. |
| 429 | too_many_concurrent | start, download | Too many videos in progress, or downloads, in the workspace. |
| 429 | upstream_rate_limited | start | The model is overloaded: retry after Retry-After (10 seconds). |
| 502 | upstream_error | start, download | The model failed, or the video could not be read: try again in a moment. |
| 503 | job_not_recorded | start | The video was started but could not be recorded: contact support. |
| 504 | upstream_timeout | start | The model did not respond in time when starting. Nothing is charged. |
Key, balance and spend cap errors are in Errors.