REST API
/api/v1/videos/generations
Start a video job with a text prompt (and, for image-to-video models, a start image). The call returns at once with a job id; the finished video comes from GET /api/v1/jobs/{id}. Videos take from about a minute to several minutes.
- URL
- https://railwail.com/api/v1/videos/generations
- Key scope
- video
- Returns
- 202 + job id
- Billing
- held at start, refunded on failure
How it works
- 1
POST the job
The API checks key, scope and balance, holds the price of the run and hands the job to the provider. The answer is HTTP 202 withjob_idand aLocationheader. - 2
Poll the job
GET /api/v1/jobs/{job_id}every 5–10 seconds. Each poll asks the provider for news and counts toward your key's rate limit, so polling faster only uses up requests. - 3
Download the result
Whenstatusiscompleted,output_urlholds the video. It can be a temporary provider link, so store the file yourself. Onfailedorcancelledthe held credits are refunded.
Request body
JSON. Unknown fields are rejected with 400 validation_failed. Which optional fields a model actually uses depends on the model; its page lists its inputs.
modelrequiredpromptrequirednegative_promptduration_secaspect_ratioresolutionfpsseedimage_url / start_imageend_imageuserExample
There is no video method in the OpenAI SDKs or the railwail npm SDK; use plain HTTP. Put your key in RAILWAIL_API_KEY (it needs the video scope).
# 1. Start the job
curl https://railwail.com/api/v1/videos/generations \
-H "Authorization: Bearer $RAILWAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kling-v2-1",
"prompt": "A slow drone shot over a misty pine forest at sunrise"
}'
# 2. Poll until status is completed (replace JOB_ID)
curl https://railwail.com/api/v1/jobs/JOB_ID \
-H "Authorization: Bearer $RAILWAIL_API_KEY"# pip install requests
import os, time, requests
BASE = "https://railwail.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['RAILWAIL_API_KEY']}"}
job = requests.post(
f"{BASE}/videos/generations",
headers=HEADERS,
json={"model": "kling-v2-1", "prompt": "A slow drone shot over a misty pine forest at sunrise"},
timeout=60,
)
job.raise_for_status()
job_id = job.json()["job_id"]
deadline = time.time() + 15 * 60 # give up after 15 minutes
while time.time() < deadline:
status = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=60).json()
if status["status"] == "completed":
print(status["output_url"])
break
if status["status"] in ("failed", "cancelled"):
raise RuntimeError(status.get("error") or status["status"])
time.sleep(8)// Node 18+ (global fetch). ESM: save as video.mjs
const BASE = "https://railwail.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.RAILWAIL_API_KEY}`,
"Content-Type": "application/json",
};
const res = await fetch(`${BASE}/videos/generations`, {
method: "POST",
headers,
body: JSON.stringify({
model: "kling-v2-1",
prompt: "A slow drone shot over a misty pine forest at sunrise",
}),
});
if (!res.ok) throw new Error((await res.json()).error?.message);
const { job_id } = await res.json();
const deadline = Date.now() + 15 * 60_000; // give up after 15 minutes
while (Date.now() < deadline) {
const job = await (await fetch(`${BASE}/jobs/${job_id}`, { headers })).json();
if (job.status === "completed") {
console.log(job.output_url);
break;
}
if (job.status === "failed" || job.status === "cancelled") throw new Error(job.error ?? job.status);
await new Promise((r) => setTimeout(r, 8000));
}Response (202)
Shape of the answer; values are placeholders.
{
"job_id": "<uuid>",
"id": "<uuid>",
"object": "video.generation",
"status": "queued",
"model": "kling-v2-1",
"provider_job_id": "<provider id>",
"estimated_duration_seconds": <number>,
"created_at": <unix seconds>,
"cost": { "credits": <number>, "currency": "credits" }
}estimated_duration_seconds is a rough guide for when to start polling, not a promise. cost.credits is the amount held (1 credit = USD 0.01). The finished job is described on the Jobs API page.
Models you can call here
Live from the catalog: video models this endpoint can run right now, with the price as charged.
- Google Veo 2
google-veo-20,60 US$/s - Google Veo 3 (Replicate)
veo-3-replicate0,48 US$/s - Google Veo 3.1
veo-3-10,48 US$/s - HunyuanVideo
hunyuan-video≈ 3,06 US$/ejecución - Kling v2.1
kling-v2-10,060 US$/s - Kling v2.1 Master
kling-v2-1-master0,336 US$/s - Kling v3
kling-v30,2688 US$/s - Kling v3 Omni
kling-v3-omni0,2688 US$/s
Errors and limits
| Status | Code | What to do |
|---|---|---|
| 400 | validation_failed | A field is unknown or out of range; details lists which. |
| 400 | unsupported_image_input | An image field the model has no input for. Nothing was charged. |
| 400 | end_image_without_start_image | end_image without image_url or start_image. Nothing was charged. |
| 402 | insufficient_credits | Top up on the billing page. |
| 402 | monthly_limit_exceeded | Your monthly spending limit is reached (chat uses spending_limit_reached for the same case). |
| 403 | insufficient_scope | The key lacks the video scope. |
| 404 | model_not_found | Check the slug in the list above. |
| 429 | trial_limit | Trial accounts may reserve at most 2 credits per run; most videos cost more. Top up to lift it. |
| 503 | model_unavailable | The model has no verified price right now; pick another. |
Rate limit headers
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. See Rate limits and Error codes.Try a model in the browser first: every video model page has a playground and shows the price of a default run.