A small Python client for Runway Gen-4.5, start to finish
Updated 2026-10-02
This tutorial builds a small, reusable Python client for Runway Gen-4.5, from a first request to a saved MP4. It assumes you have a VideoRouter API key and Python with requests installed. For request shapes and host behaviour in prose, see the Gen-4.5 API guide; this page is the code walkthrough.
Step 1: the first request
The model id is runway/gen-4.5. Video is asynchronous, so the create call returns a job and not a file:
import requests
BASE = "https://videorouter.sh/api/v1"
HEADERS = {"Authorization": "Bearer llmr_sk_live_...", "Content-Type": "application/json"}
r = requests.post(f"{BASE}/videos", headers=HEADERS, json={
"model": "runway/gen-4.5",
"prompt": "a lighthouse in a storm, waves breaking over the rocks, slow aerial orbit",
"duration_secs": 5,
"aspect_ratio": "16:9",
})
r.raise_for_status()
job = r.json()
print(job["id"], job["status"], job.get("provider"))
You should see queued and the name of the host that accepted the job. The cost is charged here, once, from the requested duration after snapping. Polling afterwards is free.
Step 2: understand duration before you pick one
Gen-4.5 is hosted by only a couple of providers, and their duration rules can differ. Because duration_secs is snapped to the nearest value the serving host supports, a request for 7 seconds may become 5 or 10, and billing follows that snapped length. If you send no duration, the platform default is 4 seconds, which also gets snapped. Practical rule: use 5 or 10 unless you have confirmed the host serving you accepts other values, and read the job back to see what was applied.
Step 3: poll safely
import time
def wait(job_id, timeout=900, interval=5):
deadline = time.time() + timeout
while True:
j = requests.get(f"{BASE}/videos/{job_id}", headers=HEADERS).json()
if j["status"] in ("completed", "failed"):
return j
if time.time() > deadline:
raise TimeoutError(f"{job_id} still {j['status']}; keep polling, do not resubmit")
time.sleep(interval)
Status moves queued, in_progress, then completed or failed. The timeout raises instead of resubmitting, deliberately. Re-creating a slow job is a second charge, and the original may still finish. Store the job id before you start polling so a restart can resume it.
Step 4: save the output
def save(job, path):
if job["status"] == "failed":
raise RuntimeError(job["error"])
url = job["data"][0]["url"]
with requests.get(url, stream=True) as resp:
resp.raise_for_status()
with open(path, "wb") as f:
for chunk in resp.iter_content(1 << 20):
f.write(chunk)
return path
Download promptly and keep your own copy; treat the returned URL as delivery, not archival storage.
Step 5: put it together with retry handling
def create(payload, attempts=3):
for _ in range(attempts):
r = requests.post(f"{BASE}/videos", headers=HEADERS, json=payload)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 5)))
continue
if r.status_code >= 500:
time.sleep(3)
continue
r.raise_for_status()
return r.json()
raise RuntimeError("could not create job")
def gen45(prompt, out, **fields):
job = create({"model": "runway/gen-4.5", "prompt": prompt, **fields})
return save(wait(job["id"]), out)
gen45("a paper boat drifting down a rain-soaked street", "boat.mp4", duration_secs=5)
Retrying a 5xx on the creation call is safe: it means every candidate host failed for that request, and failed attempts are not billed. Errors share the OpenAI-style envelope {"error": {"message", "type", "code"}}. A 400 means fix the request, 401 a bad key, 402 a spent cap or balance, 429 a rate limit with a Retry-After header. Do not retry 400s.
Step 6: image-to-video
Add start_image_url, either a public https:// URL or a data:image/...;base64,... URI:
gen45("the camera pushes in slowly as the character turns toward us",
"turn.mp4",
start_image_url="https://example.com/frame.jpg",
duration_secs=5)
For a local file, POST /v1/uploads returns a presigned URL that expires in 30 minutes; use it immediately. Expired or unreachable image URLs are the usual reason an image-to-video job fails. Keep the prompt about motion and camera rather than re-describing the frame.
Hosts and pinning
With few hosts, the price gap and the failover headroom are both smaller than on widely resold models. Unpinned requests go to the cheapest healthy host and fall through on rejection. To prefer a host, suffix the id as runway/gen-4.5/<host>; the suffix is a soft preference, and provider: {"only": [...], "allow_fallbacks": false} gives a hard pin. A 2% platform fee applies to image and video usage. Check the live comparison:
No matching models in the live catalog right now.
Logging what you send
Reproducing a good result later depends on knowing exactly what produced it. Write one line per job to a file or table: the job id, the full request body, the host that served it, the snapped duration you read back, and your verdict. When a prompt works, that record is the recipe; when a job fails, it is the evidence you need to tell a bad request from a host problem. Because the create call is the only one that charges, this log also doubles as a ledger you can reconcile against your balance.
Test and harden
- An unsupported
resolutionoraspect_ratiocombination is ignored, not rejected. Verify output dimensions withffprobein a test. - Log the request body next to the job id so any good result can be reproduced.
- Set a monthly spend cap on the key you use for tests.
- Keep a second model in config as a fallback, since failover headroom on Gen-4.5 is limited.
Next: the model page for hosts, the quickstart, or create a key to run this code. For editing existing footage, see Aleph 2.
Frequently asked questions
What is the model id for Runway Gen-4.5?
It is runway/gen-4.5. You call it with a VideoRouter key through POST /videos; no Runway account is needed.
What durations does Gen-4.5 accept?
duration_secs is snapped to what the serving host supports. Check the model page for the host you use; billing follows the snapped value.
Why should I not resubmit a slow job?
Billing happens at creation, so a second submission is a second charge. Polling is free; keep polling the same job id.
Can I do image-to-video with Gen-4.5?
Yes, by adding start_image_url as a public URL or base64 data URI to the same request.
Keep reading
- Editing Video with Runway Aleph 2 over API — Request Format and Limits
- Runway Gen-4.5 API Guide: Text & Image-to-Video Examples
- Runway vs Kling vs Veo API: How to Choose Between Them
- Video-to-Video Editing API Options: Aleph 2 and Alternatives
VideoRouter puts it next to dozens of other video and image models behind one API key, so you can compare providers, prices and fail over automatically. Compare providers on VideoRouter →