OpenRouter video unsigned_urls need an API key: Sume too
OpenRouter's unsigned_urls require your API key in the Authorization header, and so does Sume's content endpoint. Why a browser video tag fails and a safe fix.

A completed OpenRouter video job returns unsigned_urls, and OpenRouter's guide says downloading them needs your API key in the Authorization header. Sume copies that wire: unsigned_urls[0] on api.sume.com is /v1/videos/{jobId}/content?index=0, and it also needs Authorization: Bearer $SUME_API_KEY. Paste either URL into a <video src> tag and the browser will not send the header, so the player fails.
Facts about OpenRouter come from its video generation guide, read on 2026-10-03. Facts about Sume come from Sume's video generation docs.
Why is the URL called unsigned?
Because it carries no signature or token in the query string. The only credential is the header. That keeps the link safe to log, but it also means the URL is useless to anyone who does not hold a key, including your end user's browser.
Sume's docs show the same two ways to fetch the file: read unsigned_urls[0] from the poll response, or call the content endpoint directly, where index defaults to 0 and exists for models that return more than one output.
What does the response look like on both sides?
The fields line up, which is why a client written for one mostly works against the other after a base-URL and key change.
| Item | OpenRouter | Sume |
|---|---|---|
| Base path | https://openrouter.ai/api/v1/videos | https://api.sume.com/v1/videos |
| Download | GET /api/v1/videos/{jobId}/content?index=0 | GET /v1/videos/{jobId}/content?index=0 |
| Credential | API key in Authorization header | SUME_API_KEY as Bearer in Authorization header |
| Result field | unsigned_urls | unsigned_urls |
| Cost field | usage.cost | usage.cost, the Sume billable amount |
How do you serve the video to a browser without leaking the key?
Do not put the API key in front-end code. Download the file on your server with the header, store it where you control access, and give the browser your own URL. A short script does the download:
import os, sys, requests
BASE = "https://api.sume.com/v1"
KEY = os.environ.get("SUME_API_KEY", "")
if not KEY:
sys.exit("set SUME_API_KEY")
def save(job_id: str, path: str) -> None:
r = requests.get(
f"{BASE}/videos/{job_id}/content",
params={"index": 0},
headers={"Authorization": f"Bearer {KEY}"},
stream=True,
timeout=120,
)
r.raise_for_status()
with open(path, "wb") as f:
for chunk in r.iter_content(1 << 20):
f.write(chunk)
if __name__ == "__main__":
save(sys.argv[1], sys.argv[2])What does Sume not do differently?
Sume's docs do not describe a signed, expiring public link on this route, and the only documented download credential is the bearer header. If you need a link a customer can open without logging in, host the file yourself after you download it. Sume's agent surfaces refer to media.sume.com URLs in reports, but the /v1/videos contract documented above is the header-authenticated one.
Before you build on this, check the job status first. Only a completed job has unsigned_urls; failed and cancelled jobs do not.
What errors should the download step expect?
Sume's common error table applies to this route. A missing or wrong key returns 401 unauthorized. A job id from another workspace, or one your key's member did not create, returns 404 not_found, because Sume's job reads are scoped to the member whose key created the job. A 503 means a runtime dependency is unavailable or at capacity, and is worth a retry with backoff.
Treat a 401 on the download as a configuration bug, not a transient failure: retrying with the same header will not change the answer. Treat a 404 as a sign you are using the wrong key for that job, for example a teammate's key against your job id.
What should you store after the download?
Keep three things: the job id, the model id, and the usage.cost figure from the poll response. On Sume, usage.cost is the Sume billable amount, and billing is reserved on submit at provider list price times 1.25, so the number in the response is the one to reconcile against your balance.
Then serve the file from storage you control. A short-lived link from your own CDN, or an authenticated route in your app, gives the browser something it can load without ever seeing your Sume key. Rotate the key if it ever lands in a front-end bundle or a log.
- Download on the server, never in the browser.
- Check
statusiscompletedbefore you readunsigned_urls. - Store job id, model id, and cost next to the file.
- Serve your own URL to viewers.
What about very large files?
Stream the response to disk, as the script above does, instead of reading it into memory. A 30-second 1080p clip is large enough to matter on a small worker, and Sume's seedance-2.5 accepts up to 30 seconds. Set a generous read timeout, and retry only on network errors or 503, with the same URL; the download is a read and is safe to repeat.
Sources
Related posts
More in Comparisons
- Pocket TTS voice cloning: a wav in, and what Sume does instead
Pocket TTS clones from a wav file you pass to --voice, with consent rules in its model card. Sume's API takes voice ids, not audio. Here is the difference.
- Replicate MCP discovery via server.json vs Sume's MCP URL
Replicate publishes /.well-known/mcp/server.json for the official MCP Registry. Sume documents one hosted MCP URL and OAuth metadata. How each client connects.
- Replicate allow_fallback_model: Nano Banana Pro vs Sume
Replicate can fall back from Nano Banana Pro to Seedream 5.0 lite and bills the fallback. Sume's allow_fallbacks is accepted but has no effect. What to do.
- Replicate predictions source=web filter vs Sume jobs list
Replicate lets you list only web-created predictions, limited to 14 days. Sume's GET /v1/jobs lists only jobs your key's member created. How to scope a list.
Written by Sume