Poll /v1/jobs/{id}/status, then /result, in Node with backoff
A 25-line Node function that polls a Sume job on its own next_poll_after_seconds, reads /result only when the job completed, and keeps the id on timeout.

Poll GET /v1/jobs/{id}/status until terminal is true, then call GET /v1/jobs/{id}/result once, and only when the status is completed. The status payload tells you how long to wait in next_poll_after_seconds; a fixed sleep ignores that hint, and a result call on an unfinished job returns 409 job_not_completed.
Jobs and results documents the same loop in pseudocode. Below is a working version for Node 18 and later.
The function
Every public job read wraps its object in data. The id comes from the submit response of /v1/videos (id).
const API = "https://api.sume.com";
const H = { Authorization: "Bearer " + process.env.SUME_API_KEY };
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));
async function get(path) {
const res = await fetch(API + path, { headers: H });
if (!res.ok) throw new Error(path + " -> " + res.status);
return (await res.json()).data;
}
export async function waitForResult(jobId, deadlineMs = 20 * 60_000) {
const stop = Date.now() + deadlineMs;
let backoff = 2;
while (Date.now() < stop) {
const s = await get("/v1/jobs/" + jobId + "/status");
if (s.terminal) {
if (s.sume_status !== "completed") throw new Error("job " + s.sume_status);
return (await get("/v1/jobs/" + jobId + "/result")).result.artifacts;
}
await sleep(s.next_poll_after_seconds ?? backoff);
backoff = Math.min(backoff * 2, 30);
}
throw new Error("deadline reached; job " + jobId + " still runs and still bills");
}Why it is built this way
terminalandresult_readyare booleans on the status payload;sume_statuscarries the lowercase state.- A client deadline stops your wait, not the job. The error message keeps the id so you can read it again or cancel it.
queuedis a normal state: with the workspace at its concurrency cap, accepted jobs wait for a seat.- Failed and canceled jobs answer
/resultwith409 job_not_completed, so read the failure fromGET /v1/jobs/{id}instead.
Where this fits
Polling is the fallback even when you use webhooks. A delivery that exhausts its attempts does not change the job's real state, and the status URL still has the answer.
Adapting it
The function returns the artifact list from the result. Each artifact has an id, a url under media.sume.com, a type and a content_type, so a caller can pick the first video entry and stream it. Store the Sume URL; raw provider URLs are not part of the public contract.
- Add an
AbortSignalparameter if your worker can be shut down mid-wait, and pass it tofetch. - Add jitter to the sleep when you run many pollers in one process, so they do not hit the read limit as a group.
- Treat a 429 on a status read as a reason to wait longer, not as a job failure; the job is unaffected.
- Log the job id on every poll error so a restart can resume by reading the same id.
The 20-minute default is a client choice, not a Sume limit. The docs call a deadline of about that length reasonable for video, and the deadline lives in your client precisely because no HTTP request should stay open that long.
Sources
Related posts
More in Developers
- Polling Omni jobs can't 429 your submits on Sume
Sume gives each API key separate read and write budgets: 120 writes and 4,800 reads a minute on Free, 1,200 and 48,000 on Scale. The math for an Omni batch.
- Port an OpenRouter video client to Sume: base URL, key, model ids
Sume's /v1/videos follows the OpenRouter video wire. Three edits move a client: base URL, API key, bare model id. The six differences that still bite.
- PowerShell: Invoke-RestMethod for a 30-second Wan 3.0 clip
Windows PowerShell script that posts a 30-second wan-3.0 job, loops until it completes and saves the MP4 with Invoke-WebRequest. $3.75 at 720p.
- Pre-flight an Omni Flash request against the Sume model catalog
A short Python check that reads supported durations, resolutions and aspect ratios for gemini-omni-flash-1.1 via /v1/videos/models.
Written by Sume