Developers
Video API
One POST turns a sentence into a short film with native audio. Billed by the second of finished video — a flat 40 credits a second, and nothing else.
Three steps
1
Get a key
Sign in, open your account page, create an API key. It is shown once — save it. The same key works for the Director API.
2
Top up
Credits are prepaid on the same page. US$1 buys 1,000 credits, valid for 12 months.
3
Call it
POST a prompt, get a job_id back immediately, poll until it is done. Rendering takes minutes — write it as a job, not as a request.
What it costs
A flat 40 credits per second of finished video, at any hour. No per-request fee, no resolution tiers, no charge for a failed render. The day and night rates on the pricing page are for the website only; the API does not use them, and it does not use free seconds.
| Length | Credits | Render time |
|---|---|---|
| 4 s | 160 | about 3 min |
| 8 s | 320 | about 8 min |
| 10 s | 400 | about 10 min |
| 15 s | 600 | about 17 min |
All three modes cost the same, and so does every aspect ratio. Credits are held when you submit and become the charge on success. A failed or cancelled job is refunded in full, same minute. Render times are estimates, not promises — the number that matters is deliver_by in the response.
Three ways to make a clip
Same model, same price, same endpoint — mode picks which one you get.
| mode | What it does | What you send |
|---|---|---|
text | Words in, film out. The default; omit mode and you get this. | prompt |
animate | A photo starts moving. Give a first frame; add a last frame and it moves between the two. | first_frame, optional last_frame |
cast | Put a specific person, object, look or voice in the shot. Point at them in the prompt as <Picture 1>, <Audio 1>. | ref_images and/or ref_audios |
GET /v1/models tells you which modes are open right now. We turn them on one at a time; a mode that is not open returns mode_unavailable rather than pretending.
Lip sync
Give cast mode a reference audio and the person in your clip speaks it — mouth movement follows the recording.
- · Send the recording in ref_audios and refer to it in the prompt as <Audio 1>. MP3, WAV or M4A, up to three of them.
- · Pair it with a reference image of the person in ref_images and you get that face saying that line.
- · Match the lengths. The clip is exactly duration_sec long, so a 12-second recording in an 8-second clip gets cut off part way — which reads as bad lip sync even though nothing failed. We warn you on submit when they disagree; the warning is in the job under warnings.
- · Write the prompt as a speaking shot — who is talking, to whom, how they are framed. A prompt with no person in it has no mouth to move.
Portrait, landscape and square
Pass ratio. Every shape costs the same and takes the same time, so pick by where the video is going, not by budget.
| ratio | What it is for |
|---|---|
16:9 | Landscape. The default — omit ratio and you get this. |
9:16 | Vertical, for phones — TikTok, Reels, Shorts. |
1:1 | Square, for feeds. |
How long it takes, honestly
- · Jobs run in queue order. Paid plans render ahead of lower ones — but never at the cost of a delivery time already given to someone else: a job only moves up where every job already queued still finishes inside the deliver_by it was promised.
- · Cost in time grows faster than length: a 15-second clip is not four 4-second clips, it is about ten of them. This is the model, not our queue.
- · Every response carries deliver_by — a timestamp that already includes the queue ahead of you. Poll every 15–30 seconds; polling faster changes nothing.
- · If the queue is deeper than six hours we refuse the submission with capacity_deadline_infeasible rather than accept it and miss the date.
Submit a job
curl -X POST https://spaceskills.shop/v1/videos \
-H "Authorization: Bearer $SPACESKILLS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-clip-0001" \
-d '{
"prompt": "A red paper boat drifting on a calm pond at sunrise, soft golden light, gentle ripples on the water, slow push-in, cinematic",
"duration_sec": 5
}'HTTP/1.1 202 Accepted
{
"job_id": "vid_8Qk2ZrX1c0Nn",
"status": "queued",
"model": "h3/768p",
"duration_sec": 5,
"credits_held": 200,
"balance": 9800,
"queue_position": 1,
"est_render_sec": 259,
"deliver_by": "2026-09-09T14:31:07.000Z",
"poll": "/v1/videos/vid_8Qk2ZrX1c0Nn"
}Returns 202 with a job_id and the credits held. Send an Idempotency-Key and a retried POST returns the same job instead of charging twice.
curl https://spaceskills.shop/v1/videos/vid_8Qk2ZrX1c0Nn \ -H "Authorization: Bearer $SPACESKILLS_API_KEY"
{
"job_id": "vid_8Qk2ZrX1c0Nn",
"status": "succeeded",
"duration_sec": 5,
"seed": 774512900183,
"credits_held": 0,
"credits_charged": 200,
"output": {
"url": "https://media.spaceskills.shop/relay/vid_8Qk2ZrX1c0Nn.mp4",
"stored": true
},
"error": null,
"created_at": "2026-09-09T14:21:44.910Z",
"updated_at": "2026-09-09T14:25:12.338Z"
}status is one of queued, running, succeeded, failed, cancelled. On succeeded, output.url is the finished MP4 with audio muxed in.
Sending images and audio
Same shape as the Director API: each file is inline base64 or an https URL we fetch.
- · A file is { "name": "hero.jpg", "data_base64": "…" } or { "name": "hero.jpg", "url": "https://…" }. URLs must be https and must not resolve to a private address.
- · 8 MB per file, 16 MB per request, 24 MB request body. Images are JPEG, PNG or WebP; audio is MP3, WAV or M4A. We check the actual bytes, not the name or the declared type.
- · Your images are stored on our own server, never in the public bucket the finished clips live in, and the bytes are deleted the moment the job finishes or fails. The job keeps a manifest — slot, size and sha256 — so you can always check what we received.
- · In cast mode the prompt has to refer to the pictures. "A woman walks through rain" ignores your references; "The woman from <Picture 1> walks through rain" does not.
# cast: put a specific face, object or look in the shot
curl -X POST https://spaceskills.shop/v1/videos \
-H "Authorization: Bearer $SPACESKILLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "cast",
"prompt": "The woman from <Picture 1> walks slowly through a rainy neon street at night, reflections on wet asphalt, rain and distant traffic, slow tracking shot",
"duration_sec": 6,
"ref_images": [
{ "name": "her.jpg", "url": "https://example.com/her.jpg" }
]
}'
# animate: start from a photo
curl -X POST https://spaceskills.shop/v1/videos \
-H "Authorization: Bearer $SPACESKILLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "animate",
"prompt": "The lake ripples as the sun sinks lower, drifting clouds, distant birdsong, slow push-in",
"duration_sec": 5,
"first_frame": { "name": "lake.jpg", "url": "https://example.com/lake.jpg" }
}'Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/models | Catalogue: length range, price per second, current queue depth. Free, no key needed. |
POST /v1/videos | Submit a prompt. Holds credits, returns job_id. |
GET /v1/videos/{id} | Status, and the file once it is ready. |
GET /v1/videos | Your last 30 jobs and your balance. Useful for reconciliation. |
POST /v1/videos/{id}/cancel | Cancel while still queued. Full refund. Once it is running the GPU time is already spent, so it cannot be cancelled. |
Request fields
| Field | Type | Notes |
|---|---|---|
prompt | string | What the shot should show. 8 to 20000 characters. Subject, motion, setting, light, camera — in that order works well. |
duration_sec | integer | 4 to 15. This is the pricing basis. Defaults to 5. |
mode | string | text, animate or cast. Defaults to text. |
first_frame | file | animate only, required. The frame the clip starts on. |
last_frame | file | animate only, optional. The frame it ends on. |
ref_images | file[] | cast only, up to 4. Refer to them in the prompt as <Picture 1> onwards, in the order you sent them. |
ref_audios | file[] | cast only, up to 3. The voice the person speaks — this is what drives lip sync. Refer to them as <Audio 1>. |
ratio | string | 16:9 (default), 9:16 or 1:1. Same price, same render time. GET /v1/models is authoritative — the list can change. |
model | string | Defaults to h3/768p, currently the only active model. |
seed | integer | Optional. Same seed, prompt and images reproduce the same clip. Omit it and one is drawn and echoed back on the job. |
Headers: Authorization: Bearer sk_live_… and, recommended, Idempotency-Key: <any string you generate>. A key is remembered per account, so a network retry can never buy the same clip twice.
What comes back
The same job object from submit, poll and list — only the fields that are known yet are filled in.
- · output.url is an MP4 with stereo audio already muxed in, in the aspect ratio you asked for. There is no separate audio file to fetch.
- · output.stored: true means it sits on our own storage and the link is stable. On the rare false, the render machine is serving it directly and the link dies when that machine is recycled — download it promptly.
- · credits_held becomes credits_charged on success, and drops to zero on failure with a matching refund in your ledger.
- · The finished clip can land up to a third of a second short of the length you asked for. You are billed on the length you asked for.
Writing a prompt that works
The model reads plain prose, English or Chinese. It rewards specificity and punishes lists of adjectives.
- · Name the subject, then what it does, then where, then the light, then the camera. One clause each.
- · One camera move per clip. Two moves in four seconds looks like a mistake, not like style.
- · Say what should be heard. Native audio is generated from the prompt too — "wind and distant traffic" gets you wind and distant traffic.
- · Changing everything at once tells you nothing. Fix the seed and rewrite one clause at a time.
When things go wrong
| Code | Means |
|---|---|
unauthorized | Missing or wrong key. Use Authorization: Bearer sk_live_… |
insufficient_credits | Balance below the price. The response tells you both numbers. |
bad_params | No prompt, prompt too long, or duration_sec outside 4–15. |
unknown_model | No such active model. Check GET /v1/models. |
mode_unavailable | That mode is not open yet. The response lists the ones that are. |
bad_material | An image was too large, not a real JPEG/PNG/WebP, unreachable, or not https. The message says which slot. |
payload_too_large | The request body is over 24 MB. Send images as https URLs instead of inline base64. |
model_unavailable | No render backend right now — usually a machine swap. Retry in a few minutes; nothing was charged. |
capacity_deadline_infeasible | The queue is deeper than six hours. Retry later; nothing was charged. |
idempotency_conflict | That Idempotency-Key is in use by a different request body. |
not_cancellable | The job is already running or finished. |
rate_limited | Too many requests. Retry-After says how long to wait. |
Limits
- · 30 submissions per 10 minutes per IP; 120 catalogue reads in the same window. Polling a job is not rate limited.
- · One job renders at a time across the whole service — that is why deliver_by exists.
- · Five API keys per account. Revoking is immediate.
- · Finished clips stay on our storage; keep your own copy of anything you intend to ship.
Want the director to plan and render the whole film for you? See the Director Agent
Questions, or something behaving oddly? Tell us