Errors
Every failure returns JSON with a stable error code and a human message. Branch on the code; the message may be reworded.
{"error": "unsupported_format","message": "Only PNG and JPEG can be rewritten server-side."}
Success is binary, failure is JSONA successful scrub returns image bytes, not JSON. Checking the response content type — or simply
res.ok — is more reliable than trying to parse every response as JSON.Error codes
400bad_requestThe body was not multipart form data, or had no
file field. Retrying unchanged will fail identically.401unauthorizedKey missing, unknown, revoked, or on a disabled account — one message for all four on purpose. Do not retry; fix the credential.
403planValid key, account not on Agency. Response includes the
feature that was gated. Upgrade rather than retry.413payload_too_largeOver the 25 MB limit. Rejected on
Content-Length where possible, so an oversized upload is refused before it is buffered.415unsupported_formatNot a PNG or JPEG. The body carries
format and tagsFound, so you can still tell the user what was detected.429rate_limitedRate limit or monthly quota. Honour
Retry-After. See Rate limits & quotas.429quotaMonthly or daily allowance exhausted. Retrying before the window resets will not help.
503not_configuredThe service is not accepting API traffic. Transient — retry with backoff.
Which errors are worth retrying
Retry with backoff: 429 (after Retry-After), 503, and network-level failures.
Never retry unchanged: 400, 401, 403, 413, 415. These describe the request, and the request will not become valid by being sent again.
const RETRYABLE = new Set([429, 503]);async function scrub(file, attempt = 0) {const res = await send(file);if (res.ok) return res;if (RETRYABLE.has(res.status) && attempt < 4) {// Prefer the server's own figure; fall back to exponential backoff.const retryAfter = Number(res.headers.get('Retry-After'));const waitMs = Number.isFinite(retryAfter) && retryAfter > 0? retryAfter * 1000: 2 ** attempt * 1000;await new Promise((r) => setTimeout(r, waitMs));return scrub(file, attempt + 1);}const { error, message } = await res.json();throw new Error(`${error}: ${message}`);}
A 401 mid-run usually means a rotationIf calls were succeeding and suddenly return
401, the key was almost certainly rotated or revoked. Retrying will not recover it — issue a new key and redeploy.