OpenRouter video client on Sume: cancelled is terminal, callback_url
Moving an OpenRouter video client to Sume: add cancelled to your terminal statuses, send callback_url on each request, and keep unknown statuses non-terminal.

A client written from OpenRouter's video guide treats pending and in_progress as running and completed and failed as final. Sume's /v1/videos has a fifth status, cancelled, so a client that only knows four will poll a cancelled job forever. Add cancelled to the terminal set and treat any status you do not know as not final. For webhooks, send callback_url on each request; Sume's /v1/videos docs describe the per-request field, not workspace defaults.
Statuses and callbacks side by side
OpenRouter's guide, read on 2026-10-09, lists the statuses pending, in_progress, completed and failed, and says webhooks can come from a per-request callback_url or from workspace defaults, are sent on terminal states, and are HMAC-SHA256 signed. Sume's page lists the same four plus cancelled, takes callback_url on the request body, and sends its own job envelope.
| Topic | OpenRouter | Sume /v1/videos |
|---|---|---|
| Statuses | pending, in_progress, completed, failed | pending, in_progress, completed, failed, cancelled |
| Webhook target | callback_url or workspace defaults | callback_url on the request; must be HTTPS |
| Signature header | X-OpenRouter-Signature | x-sume-webhook-signature (sume-v1=...) |
| Event shape | video.generation.* events | Sume job envelope: job.completed, job.failed, job.canceled |
| Job routes | Polling URL only | Also GET /v1/jobs/{id}/status and /result |
The helper
withCallback validates the URL before the request leaves your machine: it must parse, use https, and not be localhost, 127.0.0.1 or [::1]. Sume rejects such URLs too, but a local check gives a clear message without a round trip. submit posts the body with the Idempotency-Key and expects a 202.
isTerminal holds the three final statuses and returns false for anything else. That is deliberate: if Sume or a later API version adds a status, your loop keeps polling and your deadline ends it, instead of treating an unknown word as success.
export function withCallback(body, url = process.env.SUME_CALLBACK_URL) {
if (!url) throw new Error("set SUME_CALLBACK_URL: Sume takes the callback per request");
const u = new URL(url);
const local = ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname);
if (u.protocol !== "https:" || local) throw new Error(`callback_url must be public HTTPS, got ${url}`);
return { ...body, callback_url: url };
}
export async function submit(body, key) {
const res = await fetch("https://api.sume.com/v1/videos", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify(withCallback(body)),
});
if (res.status !== 202) throw new Error(`submit ${res.status}: ${await res.text()}`);
return res.json();
}
const TERMINAL = new Set(["completed", "failed", "cancelled"]);
export const isTerminal = (status) => TERMINAL.has(status); // unknown spellings stay non-terminal
if (process.argv[2] === "demo") {
console.log(withCallback({ model: "wan-3.0", prompt: "x" }, "https://hooks.example.com/sume"));
console.log(isTerminal("expired"), isTerminal("cancelled"));
}Two spellings to watch
The two job vocabularies in Sume do not match. /v1/videos uses cancelled with two l letters, while the generic job routes use queued, processing, completed, failed and canceled with one l. If your code reads both, normalize the word at the edge.
Webhooks are an optimization. The Sume docs say delivery is never the only recovery path, so keep a slow poll on the polling_url for the events that never arrive, as with any callback.
A last check is cheap to run. Cancel a queued job from a test workspace, then poll it with your client. A client that knows cancelled stops and reports it; a client that only knows completed and failed times out at its deadline. That test finds the bug in a minute and costs nothing. Document the two spellings in your client's types, so a later reader does not add canceled to a list that already has cancelled.
- Replace the OpenRouter signature check with the Sume one before you switch webhooks.
- Run the demo: node file.mjs demo prints the callback body and two terminal checks.
- Never put localhost in callback_url, even in tests.
Sources
Related posts
More in Developers
- Org workspace concurrency floor of 10: the queue and wave that follow
Sume gives org workspaces a processing floor of 10. With the default queue formula that is 50 queued, 60 accepted, and a wave hint of 45. Read your own fields.
- Pick a Sume video model in code: audio refs, 1080p, 20 seconds
Filter GET /v1/video-router/models on reference_audios, resolutions and duration_seconds, then submit the survivor with reference_audio_urls (up to five).
- Pick the Sume video model in code: seconds, ratio, edit or swap
A 12-line Python function turns duration, aspect ratio, edit and swap needs into the models that can run the job. A 4 s 1:1 clip has none; a 12 s one has two.
- Poll every 2 s or 30 s? Reads per 20-minute video job on Free and Pro
Polling a Sume video job every 2 s is 600 reads in 20 minutes; every 30 s is 40. Share of the read budget on Free and Pro, with a Python check.
Written by Sume