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
- GET /v1/jobs/{id} returns 404 for a job your teammate created
A Sume API key reads only jobs its own member created. Jobs made by a teammate or in another workspace return 404 not_found. Why, and how to read them.
- 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 Auto video: which model ran? Not disclosed, so log this
sume/auto never names the family that served a request. Keep a provenance record from the request, job id and cost, and pin an id when you must know.
- 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.
Written by Sume