Sume SDK error.retryable: server flag first, status only as fallback

SumeApiError.retryable uses the error envelope's retryable flag when present and falls back to 408, 429 or 5xx otherwise. A 409 is never retried by status.

5 min readSume
All posts

When a Sume SDK call fails, can you trust error.retryable? Yes, and it is computed in a fixed order. In @sume-com/sdk@0.2.0, toSumeApiError uses the retryable value from the server's error envelope when the server sent one. Only when the envelope has no such field does it fall back to the HTTP status: 408, 429 and any 5xx are retryable, and so is a failure with no response at all, such as a dropped connection. A 409 is never retryable by status, because repeating the call returns the same answer.

The envelope fields come from the errors and credits reference: code, message, request_id, plus retryable, retry_after_seconds and next_action where they apply. The SDK copies them to code, requestId, retryable, retryAfterSeconds and nextAction on the thrown error.

Why the server's flag wins

A status code cannot tell a transient 503 from one that says do not retry aggressively, and Sume uses both. The documented backpressure errors, such as provider_capacity_exceeded, say to retry later with the same idempotency key, while provider_not_configured says not to retry aggressively. If a client retried on status alone it would hammer the second kind. Reading the flag first lets the server steer.

The order also explains why a 409 sits outside the status fallback. On this API a 409 means an idempotency or run-state conflict. idempotency_conflict says you reused a key with a different payload, which no number of retries will fix, and job_not_completed on a result read says the job has not finished, which is a signal to poll the status instead.

How the SDK decides retryable (SDK source and Sume docs, read 2026-10-04)
Situationretryable
Envelope carries retryable: truetrue
Envelope carries retryable: false on a 503false
No envelope, status 429true
No envelope, status 409false
No response (transport failure)true

Using it in your own retry loop

The client already retries GETs, and POSTs that carry an Idempotency-Key, a couple of times. Your outer loop is for the cases that survive that. The helper below retries only when the typed error says so, waits the server's retryAfterSeconds when given, and keeps one idempotency key for every attempt so a retry can never create a second paid job.

import { createSumeClient, generateImageV1, SumeApiError } from "@sume-com/sdk";

const client = createSumeClient({ apiKey: process.env.SUME_API_KEY ?? "", maxRetries: 0 });

export async function submitWithKey(prompt: string, attempts = 4) {
  const key = crypto.randomUUID();
  for (let i = 0; ; i += 1) {
    const res = await generateImageV1({
      client,
      headers: { "idempotency-key": key },
      body: { prompt, mode: "async" },
    });
    if (!res.error && res.data) return res.data.data.request_id;
    const err = res.error as { error?: { retryable?: boolean; retry_after_seconds?: number; code?: string } };
    const info = err?.error;
    const status = res.response?.status;
    const retryable = info?.retryable ?? (status === 408 || status === 429 || (status ?? 0) >= 500);
    if (!retryable || i + 1 >= attempts) {
      throw new Error(`submit failed: ${info?.code ?? status}`);
    }
    await new Promise((r) => setTimeout(r, (info?.retry_after_seconds ?? 2 ** i) * 1000));
  }
}

Checks before you ship it

  • Log code and request_id on every failure so support can find the call.
  • Treat a 402 insufficient_credits as a stop, not a retry. It maps to SumeInsufficientCreditsError and topping up is the fix.
  • Cap attempts. A server that keeps saying retry later still deserves a deadline on your side.
  • Never mint a new idempotency key inside the loop.

Related posts

More in Developers

All Developers posts

Written by Sume