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.

5 min readSume
All posts

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.

Download wire, OpenRouter vs Sume (read 2026-10-03)
ItemOpenRouterSume
Base pathhttps://openrouter.ai/api/v1/videoshttps://api.sume.com/v1/videos
DownloadGET /api/v1/videos/{jobId}/content?index=0GET /v1/videos/{jobId}/content?index=0
CredentialAPI key in Authorization headerSUME_API_KEY as Bearer in Authorization header
Result fieldunsigned_urlsunsigned_urls
Cost fieldusage.costusage.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 status is completed before you read unsigned_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

All Comparisons posts

Written by Sume