Resume a Sume job wait after SumeJobTimeoutError in TypeScript
A 30-second Seedance 2.5 clip can outlast one waitForJob call. Call it again with the same job id on timeout or a transient read error. Never resubmit.

When waitForJob throws SumeJobTimeoutError or a SumeJobRequestError for a transient read, the job is still running on Sume's side, so call waitForJob again with the same job id. Do not submit the request again, because that is a second paid job.
The wait helper has no built-in allowance for read failures, so a small loop around it is the whole recovery plan.
Which errors mean wait again?
The SDK retries GET reads inside the client first (two retries by default, with backoff), so an error that reaches you has already been tried more than once. Decide by class and status.
| Outcome | Carries | Next step |
|---|---|---|
| Resolves | The job record, for completed, failed or canceled | Read status and error; a failed job is a result, not an exception |
| SumeJobTimeoutError | jobId, lastStatus | Wait again with the same id |
| SumeJobRequestError, status 429 or 5xx | jobId, status, body | Wait again after a pause |
| SumeJobRequestError, status 401, 403 or 404 | jobId, status, body | Stop and fix the key or the id |
What does the loop look like?
Cap the number of rounds so a broken key cannot spin forever. Each round is its own deadline, so ten minutes per round gives a 30-second Seedance 2.5 clip plenty of time.
import {
createSumeClient, waitForJob,
SumeJobTimeoutError, SumeJobRequestError,
} from "@sume-com/sdk";
const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });
async function settle(jobId: string, rounds = 3) {
for (let i = 1; i <= rounds; i++) {
try {
return await waitForJob(jobId, { client, timeout: 10 * 60_000 });
} catch (e) {
const transient =
e instanceof SumeJobTimeoutError ||
(e instanceof SumeJobRequestError &&
(e.status === undefined || e.status === 429 || e.status >= 500));
if (!transient || i === rounds) throw e;
console.warn(`round ${i} ended early; waiting again for ${jobId}`);
}
}
}
const job = await settle(process.env.JOB_ID!);
console.log(job?.status, job?.error ?? "no error");What should I do after the last round?
Keep the job id in your own store and stop. The job record, the status endpoint and the events endpoint all stay readable, so a later run can pick up the same id. If you registered a webhook, the terminal event will also arrive on its own, and it is the better trigger than a long-lived process.
Why not raise the timeout instead?
You can, since the default is 20 minutes and the option takes milliseconds. A loop gives you one more thing, a log line and a decision point between rounds, and it survives a single dropped connection that a long single wait would not, once the client's own retries are used up.
Sources
Related posts
More in Developers
- Resume polling Sume video jobs after a restart with a sqlite ledger
Write the job id and Idempotency-Key to sqlite before anything else can crash, then resume polling open jobs on start. Python standard library only.
- Ruby Net::HTTP: Gemini Omni Flash 1.1 vertical 6 s clip, $0.75
Ruby standard library only: submit a 6-second 9:16 Gemini Omni Flash 1.1 clip to Sume ($0.75 at 720p) and poll to completion. Prices at four resolutions.
- Run receipt micros: 5,000,000 is $5.00 and 240,000 is $0.24
Sume run receipts report spend in USD micros. How to convert the cap and billable fields to dollars, and why GET /v1/usage stays the billing ledger.
- Scheduled run input: 64 properties and 2 MiB, how to pack a brief
A Sume scheduled run takes an input object of at most 64 properties and 2,097,152 bytes. Here is how to pack a brief for an agent without hitting either limit.
Written by Sume