Move an OpenRouter-style video client to Sume: the 400s to expect

Sume /v1/videos matches the OpenRouter video shape, but size, provider.options and seed return 400. The differences table and a Python asyncio polling client.

5 min readSume
All posts

A client written for the OpenRouter video generation API works against Sume after you change the base URL and the key, with a few exceptions that fail with a 400. Sume's size returns unsupported_parameter because every v1 model reports supported_sizes: null. A non-empty provider.options returns 400 unsupported_parameter. No v1 model accepts seed. Model ids are bare, such as seedance-2, not org/slug.

This is Sume's own statement on its Video generation page, read 2026-10-09. This post makes no claim about the other side beyond what that page says.

The differences that break a migration

The page lists the differences in one table. These are the rows that change your requests or your handling.

Sume /v1/videos differences, as of 2026-10-09 (Video generation).
AreaOn Sume
Base pathhttps://api.sume.com/v1/videos, no /api segment
AuthAuthorization: Bearer $SUME_API_KEY
Model idsBare catalog ids; sume/auto lets Sume pick the family
size400 unsupported_parameter; use resolution plus aspect_ratio
provider.optionsNon-empty returns 400 unsupported_parameter
seedNo v1 model accepts it; the field is rejected
WebhooksSume's job envelope and x-sume-webhook-signature; send callback_url, HTTPS only
IdempotencySend Idempotency-Key; a replay returns the original job
Job lifecycleThe same job is also at /v1/jobs/{id}/status and /result

A client that runs

The snippet submits, then polls the polling_url from the submit response. It sends a fresh Idempotency-Key once per logical request, outside any retry loop, so a retried submit cannot create a second job. The poll gap is 30 seconds, as the page suggests.

import asyncio
import os
import uuid

import httpx


async def main():
    headers = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
    body = {"model": "seedance-2", "prompt": "A kettle steaming on a stove, 9:16",
            "aspect_ratio": "9:16", "resolution": "720p", "duration": 4}
    key = {"Idempotency-Key": str(uuid.uuid4())}
    async with httpx.AsyncClient(headers=headers, timeout=60) as c:
        sub = await c.post("https://api.sume.com/v1/videos", json=body, headers=key)
        sub.raise_for_status()
        url = sub.json()["polling_url"]
        while True:
            job = (await c.get(url)).json()
            if job["status"] in ("completed", "failed", "cancelled"):
                break
            await asyncio.sleep(30)
        print(job["status"], job.get("unsigned_urls"), job.get("error"))


asyncio.run(main())

Check the model before you submit

Limits differ per model, so ask first. GET /v1/videos/models returns supported_resolutions, supported_aspect_ratios, supported_durations, and whether a model accepts frame images and reference types. For example, the page says seedance-2.5 accepts 4 to 30 seconds and minimax-h3 accepts 5 to 15 seconds at 480p or 768p.

Two last details. The status spelling is cancelled on this surface, not canceled. And usage.cost on a poll is the Sume billable amount: at submit, Sume reserves the provider list price times 1.25. If you have a billing check that compares against another price table, expect a difference.

Run a small contract test before you cut over. Send one request with size, one with a non-empty provider.options, and one with seed, and assert that each comes back as a 400 in your own error handling. Then send one valid request twice with the same Idempotency-Key and confirm that you get one job id, not two. These four checks cover most of the difference table.

If you want the Sume envelope instead of the OpenRouter shape, the older /v1/video-router/generate still works with the same model ids, but the page recommends /v1/videos for new integrations.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume