Handle every Sume API error with one switch on next_action

Sume errors share one envelope. Branch on next_action, retryable and retry_after_seconds, and your client handles new codes without a code change. JS sample.

5 min readSume
All posts

Read error.next_action from the Sume error envelope and switch on it. It has seven values (authenticate, fix_input, add_funds, retry_later, poll_status, inspect_events, contact_support), and together with retryable and retry_after_seconds it tells a client what to do without knowing every error.code. Keep the code for logs and for the few cases where you want custom behavior.

What the envelope carries

Every error on the API has the same shape: an error object with a stable lowercase code, a human message, a request_id, and the fields below. The docs say to match on code and never on message, because the text can change.

The HTTP status still matters, and the order is status first, then code. A 4xx at create means nothing ran and nothing was charged, so you correct the call instead of retrying it. next_action is the third layer: it turns the pair into an instruction.

Envelope fields and what to use them for (read 2026-10-07)
FieldUse it forNotes
codeBranching on a known causeMatches ^[a-z0-9_]+$; never a sentence
next_actionThe default responseOne of seven tokens
retryableWhether the same request can succeed laterCan be false on a 5xx whose cause is your input
retry_after_secondsHow long to wait firstCan be absent or null
request_idSupport and logsAlso sent as the x-sume-request-id header
detailsCode-specific dataFor example scope, required_scope, violations[]

Why next_action beats the status code

Status codes lie about the cause often enough that a status-only handler goes wrong. attachment_fetch_failed returns a 502, which looks like an outage, but its next_action is fix_input, because the cause is a URL that Sume could not fetch. A status-only client would retry a request that can never succeed. A 403 insufficient_scope is the opposite: nothing about the status says the retry is pointless, and the docs call retrying it in a loop the most frequent and most expensive mistake.

next_action also gives you one place to handle codes that do not exist yet. When Sume adds an error code, an old client that switches on the action still stops on fix_input and still waits on retry_later.

A handler you can paste

The function maps a response body to a step, a retry flag, a delay and the identifiers to log. Unknown or missing actions fall back to the status class: retry for 429 and 5xx, stop for everything else.

const ACTIONS = {
  authenticate: "stop: fix the key, do not retry",
  fix_input: "stop: correct the request body, do not retry",
  add_funds: "stop: fund the wallet, then resend the same key",
  retry_later: "retry with the same key after the delay",
  poll_status: "do not resubmit: poll status_url",
  inspect_events: "read /events, then decide",
  contact_support: "stop: send request_id to support",
};

export function decide(status, body) {
  const e = body.error ?? {};
  const delay = e.retry_after_seconds ?? 5;
  const step = ACTIONS[e.next_action] ?? (status >= 500 || status === 429 ? ACTIONS.retry_later : ACTIONS.fix_input);
  return { step, retryable: e.retryable === true, delay, requestId: e.request_id ?? null, code: e.code ?? "unknown" };
}

console.log(decide(402, { error: { code: "insufficient_credits", next_action: "add_funds", retryable: false, request_id: "req_1" } }));
console.log(decide(503, { error: { code: "provider_capacity_exceeded", retryable: true, retry_after_seconds: 20 } }));

What each action should do in your code

authenticate and fix_input are stops. Page a human or return a 4xx to your own caller, and do not loop. add_funds is also a stop, with one extra property: retrying before the wallet is funded gives the same answer. After funding, resend with the same Idempotency-Key, because a failed create releases its key.

retry_later is the only action that means resend, and you should wait retry_after_seconds when it is present. poll_status and inspect_events mean the job exists: read status_url or /events and never resubmit a paid request. contact_support should log request_id and stop.

Log the fields that make the next incident short

Whatever the action, a retry of a create should reuse the same Idempotency-Key. The same key with the same body returns the original job, the same key with a different body is 409 idempotency_conflict, and a key held by an in-flight request is 409 idempotency_key_in_use, which is retryable after about a second. Store the key next to the intent in your database, not in a variable, so it survives a restart.

Log the same four fields every time

On every non-2xx response, write the HTTP status, error.code, error.next_action and request_id in one structured log line. The request id is also the x-sume-request-id response header, and it is the single identifier support needs to find the call. Redact API keys, signed URLs and raw media URLs before the line is written.

With those four fields you can chart errors by action instead of by status. A rising fix_input count points at your own validation, a rising retry_later count points at capacity or limits, and a single contact_support deserves a ticket. That is a better alert than a count of all 5xx, because it separates your bugs from Sume's and from your wallet.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume