Which Sume API calls are safe to retry blindly, and which need a key?

Reads, cancels and redelivers retry safely; paid submits retry only under the same Idempotency-Key. A call-by-call table, plus the codes that mean wait or stop.

5 min readSume
All posts

Every read is safe to retry blindly. Cancel is safe because it is idempotent. A paid submit is safe to retry only when you resend the same Idempotency-Key with the same body, because without a key a replay starts and bills a second job. That one rule sorts almost every call on the Sume API, and the table below applies it endpoint by endpoint.

The table

"Safe" here means that sending the request twice cannot create or bill a second piece of work. It does not mean the second call succeeds: some of these return a typed conflict you should read rather than loop on.

Retry safety by call (read 2026-10-07)
CallRetry blindly?Why, and what to watch
GET /v1/jobs/{id}, /status, /result, /events, GET /v1/jobsYesReads. A 429 on a read means back off; the job keeps running. /result answers 409 job_not_completed until the job is done.
POST /v1/jobs/{id}/cancelYesCancel of an already-canceled job returns the same canceled job. After generation starts it returns 409 job_generation_already_started.
Paid submit, for example POST /v1/image-1.0/generateOnly with the same Idempotency-KeySame key and same body returns the original job. Same key and a different body is 409 idempotency_conflict.
POST /v1/formats/{handle}/{slug}/runsOnly with the same keyReplay returns 200 with idempotency_hit: true. The key's scope is one Format, so the same key on two Formats starts two runs.
POST .../webhook/redeliverYes, but your endpoint receives another copyRe-POSTs the real terminal event with a fresh timestamp and signature. Dedupe on the id.
MCP write or paid toolsOnly with the same idempotency_keyRequired on writes and paid calls. It is transport dedup, not a human approval.

What each status code asks of you

Branch on the HTTP status first, then on error.code. A 4xx at create means nothing ran and nothing was charged, so correct the call instead of retrying it. The most expensive habit is retrying 403 insufficient_scope in a loop: the key's scopes are fixed when the key is created, so the retry cannot succeed.

  • 429 rate_limited: wait for retry-after, then resend with the same key.
  • 429 queue_full: the workspace has no accepted capacity left. Wait for a job to finish or cancel queued ones, then retry with the same key.
  • 503 provider_capacity_exceeded: retry later with the same key.
  • 409 idempotency_key_in_use: another request with that key is in flight. It is retryable; wait about a second and send again to get the original.
  • 409 idempotency_conflict: you changed the body under a used key. Do not retry. Fix how your keys are derived.
  • 402 insufficient_credits: retrying changes nothing until the wallet is funded.

A submit loop that follows the table

The shell loop below sends one key for all attempts. It sleeps longer on 429 and 503, sleeps one second on a 409 (a concurrent copy of the same request), and stops on any other 4xx instead of looping. Because the body never changes inside the loop, a 409 here can only be the in-flight case, not a conflict.

KEY="order-8823-hero-v1"
for attempt in 1 2 3; do
  code=$(curl -s -o resp.json -w '%{http_code}' -X POST https://api.sume.com/v1/image-1.0/generate \
    -H "Authorization: Bearer $SUME_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $KEY" \
    -d '{"prompt":"Product hero shot of a matte black bottle on marble","mode":"async"}')
  case "$code" in
    2*) break ;;                      # job created, or the original job returned
    429|503) sleep $((attempt * 5)) ;; # rate limited or capacity: same key, same body
    409) sleep 1 ;;                   # idempotency_key_in_use: another copy is in flight
    *) break ;;                       # 4xx: fix the request, do not loop
  esac
done
cat resp.json

Where the key comes from

Derive the key from the thing the job makes, such as your order id plus a version you bump only when you want a re-run. A fresh random UUID per attempt defeats the header, because each retry then looks like a new request. The key can be up to 255 characters on Format runs.

After a create that failed (402, 503 and similar), Sume released the key, so you can correct the cause and retry with the same one. After a timeout where you do not know whether the submit landed, resend the same key and read the returned job id rather than submitting a fresh request.

What the SDK already does

createSumeClient in the TypeScript SDK retries 408, 429, 5xx and transport failures, with two retries by default, exponential backoff with jitter, and retry-after honored. It retries a POST only when the request carries an Idempotency-Key. subscribeFormatRun generates a key for you unless you pass your own. If you write your own client, copy that rule: reads retry freely, writes retry only under a stable key.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume