Stop a Seedance 2.5 batch on SumeInsufficientCreditsError
Submit a batch with createVideoGeneration and one idempotency key per item. On a 402, stop the loop, top up, and rerun the same script without paying twice.

Give every item in the batch its own Idempotency-Key, check each submit for a 402, and break out of the loop when the SDK type says the workspace cannot fund the request. After a top-up, run the same script: items that were already accepted come back as the original jobs, and only the rest are new spend.
The generated SDK functions return a result object instead of throwing, so the typed error is one call away.
What does a 402 look like to the SDK?
The Sume client maps statuses to classes. For this batch, two matter, and the docs describe what to do about each.
| Class | HTTP status | What the loop does |
|---|---|---|
| SumeInsufficientCreditsError | 402 | Stop; topping up is the only fix |
| SumeRateLimitError | 429 | The client already retried a keyed POST; slow down |
| SumeConflictError | 409 | Same key with a changed body; fix the key |
| SumeAuthenticationError | 401 | Stop; the key is wrong |
How do I write the loop?
The index is part of the key, so the loop is replay-safe. toSumeApiError turns the status and body into the typed class.
import {
createSumeClient, createVideoGeneration, toSumeApiError,
SumeInsufficientCreditsError,
} from "@sume-com/sdk";
const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });
const prompts = (process.env.PROMPTS ?? "").split("|").filter(Boolean);
const ids: string[] = [];
for (const [i, prompt] of prompts.entries()) {
const { data, error, response } = await createVideoGeneration({
client,
headers: { "idempotency-key": `${process.env.BATCH}-${i}` },
body: { model: "seedance-2.5", prompt,
duration: 8, resolution: "720p" },
});
if (error || !data) {
const err = toSumeApiError(response?.status, error, "submit failed");
if (err instanceof SumeInsufficientCreditsError) {
console.error(`out of credits after ${ids.length} jobs`);
break;
}
throw err;
}
ids.push(data.id);
}
console.log(ids);Why a break and not a retry?
The docs say a funding failure has one fix, and a retry cannot supply it. A 402 loop with backoff only burns time and, on the later items, writes noise into your logs. Break, save the printed ids, and let a person or a billing job top up.
How do I know the rerun reused the first jobs?
Print the job ids on both runs. For items that were accepted the first time, the ids match, because the same key with the same body returns the original job. Change a prompt between runs and that one item fails with a 409 instead, which is the signal that your key scheme is working.
Sources
Related posts
More in Developers
- Store the Sume job webhook, then answer 204: SQLite insert-or-ignore
Commit the event keyed on job_id before you return 2xx, so a retry or a redeliver is harmless and a crash never loses it. Runnable Python and sqlite3 example.
- Stream a Sume video download in Python: .part file, then rename
Download /v1/videos/{id}/content with requests stream=True, write a .part file and rename on success. A lost 30 s clip costs $1.875 to $17.334 to re-buy.
- Sume STT: omit duration_seconds and it reserves 1 minute, not 9
A 9-minute file is $0.09 of Sume STT, but without duration_seconds the reserve is 1 minute, $0.01. Send 540. The field takes 1 to 600 seconds.
- Sume 415 unsupported_media_type: read details.received_content_type
A 415 from the Sume API means the body was not sent as application/json. Which client defaults cause it, how to read the details field, and the one-line fix.
Written by Sume