Run an OpenRouter video script on Sume: four changes (Python)
Sume's /v1/videos copies OpenRouter's video wire. Port a polling script by changing the base URL and model id, then dropping seed, size and provider options.

OpenRouter's video guide describes an asynchronous flow: POST /api/v1/videos returns an id, a polling_url and a pending status, you poll until the status is completed or failed, and finished jobs expose unsigned_urls that you download with your key. Sume's /v1/videos route was written to match that wire, so a script built for OpenRouter ports with four small edits rather than a rewrite.
The edits matter because Sume deliberately differs in a few places. Where the Sume contract and OpenRouter's guide disagree, the difference is listed in Sume's own contract document, and the items below are the ones a ported script usually trips on.
The four changes
| Change | OpenRouter | Sume |
|---|---|---|
| Base URL | https://openrouter.ai/api/v1 | https://api.sume.com/v1 (no /api segment) |
| Model id | vendor/model, such as google/veo-3.1 | Bare catalog id, such as h3-max-recast |
| Unsupported fields | seed and size are fields in the request schema | seed and size return 400 in v1 |
| Provider options | provider.options is a field in the request schema | Must be empty or omitted |
Response shapes need no change. The submit response (202) and the poll response are bare OpenRouter-style objects and are not wrapped in Sume's usual data envelope. Statuses are pending, in_progress, completed, failed and cancelled, and a finished job adds unsigned_urls and a usage.cost.
A ported script
This script uses only the standard library. It submits a Recast job with one source video and one host photo in input_references, then follows polling_url until the job leaves pending and in_progress. Sume accepts Authorization: Bearer or x-api-key, and the sample uses the first, which is the form an OpenRouter client already sends. Send only one of them, since both together is a 401.
import json, os, time, urllib.request
BASE = "https://api.sume.com/v1" # OpenRouter: https://openrouter.ai/api/v1
HEAD = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json"}
def call(url: str, body: dict | None = None) -> dict:
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(url, data, HEAD)
with urllib.request.urlopen(req, timeout=60) as r:
return json.load(r)
job = call(BASE + "/videos", {
"model": "h3-max-recast", # bare id, not "vendor/model"
"resolution": "768p",
"input_references": [
{"type": "video_url", "video_url": {"url": "https://example.com/source.mp4"}},
{"type": "image_url", "image_url": {"url": "https://example.com/host.jpg"}},
],
})
print(job["id"], job["status"])
while job["status"] in ("pending", "in_progress"):
time.sleep(float(os.environ.get("POLL", "10")))
job = call(job["polling_url"])
print(job["status"], job.get("unsigned_urls"))After it completes
unsigned_urls[0]is a Sume content URL that redirects to the file. Fetch it with your key, as OpenRouter's guide also says to.- The job id doubles as a Sume job id, so
GET /v1/jobs/{id}/statusworks too, and it addsterminalandnext_poll_after_seconds. - Add an
Idempotency-Keyheader to the submit call before you put the script on a retry loop. - Poll every 10 to 30 seconds. OpenRouter suggests around 30 seconds for video, and Sume's status endpoint states its own preferred interval.
- Check the model's own limits first.
GET /v1/videos/modelslists supported resolutions, durations and reference types for each id.
The OpenRouter wire is described in their video guide. The Sume model list and rules are in the videos docs, and header rules are in authentication.
Sources
Related posts
More in Developers
- Sume run webhook 3xx redirect: a failed attempt, not a delivery
Sume does not follow redirects on run webhooks, so a trailing-slash 301 fails every attempt. How to find it with a no-follow probe and register the final URL.
- reqwest timeout versus read_timeout for Sume polls and downloads
reqwest has no timeout by default. timeout() is a total deadline including the body; read_timeout() resets per read. Which to use for Sume status polls.
- Same narrator every week: pin the voice, language and speed
Keep one narrator across a weekly series on Sume TTS: pin avatar_handle with voice.id, language, speed and format in one config, with one key per episode.
- Scalar API Reference for the Sume OpenAPI JSON, with a Try It key
Scalar renders an OpenAPI document as an interactive reference with a test client. Point it at the Sume spec, and keep the Bearer key out of the page source.
Written by Sume