Sume API errors: a 13-line function that says retry or fix
Map a failed Sume response to out-of-credit, wait, back off, fix the key, retry with the same key, or fix the request, using the documented error envelope.

Every failed Sume API response carries the same envelope, { "error": { "code", "message", "request_id", "details" } }, so one function can decide what a caller should do next: flag the balance on 402, wait on queue_full, back off on rate_limited, fix your key on idempotency_conflict, retry with the same key on 5xx, and fix the request for everything else. The 13-line function below does that and returns a small object you can log or branch on.
The split matters because the retry advice differs per code. A blind retry on 400 or 401 never succeeds. A retry on a paid submit without the same Idempotency-Key can bill twice. And two different 429s need different waits.
What the docs define
Retry advice for queue and capacity errors in the docs is to retry with the same idempotency key, and to use retry-after when present.
| Status and code | Meaning | Function returns |
|---|---|---|
| 402 insufficient_credits | Balance cannot reserve the generation | out_of_credit |
| 429 queue_full | Concurrency plus queue capacity is full | wait_for_capacity |
| 429 rate_limited | Too many requests in the window | back_off |
| 409 idempotency_conflict | Key reused for a different payload | fix_key |
| 5xx, provider_capacity_exceeded | Dispatch queue or runtime unavailable | retry_same_key |
| 400, 401, 403, 404, 415 | Request, auth, scope or media type problem | fix_request |
The function
It parses the body defensively, because a proxy can return an HTML 502 with no envelope, and prefers the retry-after header over the body's retry_after_seconds when both exist. Log requestId and nothing else when you contact support; the docs say it is safe to share.
// Decide what to do with a failed Sume response, from the documented error envelope.
export async function classify(res) {
const body = await res.json().catch(() => null); // a proxy 502 may return HTML
const e = body?.error ?? {};
const base = { status: res.status, code: e.code ?? "unknown_error", requestId: e.request_id ?? null };
const retryAfter = Number(res.headers.get("retry-after")) || e.retry_after_seconds || null;
if (res.status === 402 || e.code === "insufficient_credits") return { ...base, action: "out_of_credit" };
if (e.code === "queue_full") return { ...base, action: "wait_for_capacity", retryAfter };
if (res.status === 429) return { ...base, action: "back_off", retryAfter };
if (e.code === "idempotency_conflict") return { ...base, action: "fix_key" }; // same key, different payload
if (res.status >= 500 || e.retryable) return { ...base, action: "retry_same_key", retryAfter };
return { ...base, action: "fix_request" }; // 400, 401, 403, 404, 415: retrying cannot help
}Using it
Wire it in right after the fetch call.
- Call it only when
res.okis false. - For
back_offandwait_for_capacity, sleep forretryAfterseconds or a doubling default, then resend with the sameIdempotency-Key. - For
wait_for_capacity, stop adding work and poll existing jobs first; retrying harder does not open queue slots. - For
out_of_credit, alert a human. Do not loop; the docs point to upgrading the plan or waiting for included Gen$, not a top-up flow.
Limits
I ran it against hand-built responses shaped like the documented envelope (402, both 429s, 409, 503, an HTML 502, 400), not against live failures. retryable and retry_after_seconds appear on job errors and in the SDK's envelope type; whether a given endpoint fills them is not guaranteed, so the function treats them as hints. A 409 can also be job_not_completed or job_generation_already_started, which this function reports as fix_request; handle those at the call site that cancels or fetches results.
Sources
Related posts
More in Developers
- sume/auto for an image series: why to pin a model id instead
sume/auto never tells you which model ran, and job.model stays sume/auto. For a series that must match, send one catalog id such as google/nano-banana-2.
- SUME_API_BASE_URL has /v1, the SDK baseUrl does not: which is right?
The Sume CLI base URL is https://api.sume.com/v1 and it sends x-api-key by default; the SDK baseUrl is https://api.sume.com with no /v1. Both env sets compared.
- SUME_CONFIG_DIR in GitHub Actions: keep Sume CLI config off the runner
Set SUME_API_KEY from a GitHub secret and SUME_CONFIG_DIR to a temp folder so the Sume CLI keeps its config off ~/.sume-com/config.json on a shared runner.
- Format run on_active_run: allow, skip or reject with a 409?
on_active_run sets what a second Format run does while one is in flight: allow runs both, skip records a skipped run, reject answers 409 format_run_in_progress.
Written by Sume