PurifyAI

Errors

Every failure returns JSON with a stable error code and a human message. Branch on the code; the message may be reworded.

error shape
{
"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

400
bad_request
The body was not multipart form data, or had no file field. Retrying unchanged will fail identically.
401
unauthorized
Key missing, unknown, revoked, or on a disabled account — one message for all four on purpose. Do not retry; fix the credential.
403
plan
Valid key, account not on Agency. Response includes the feature that was gated. Upgrade rather than retry.
413
payload_too_large
Over the 25 MB limit. Rejected on Content-Length where possible, so an oversized upload is refused before it is buffered.
415
unsupported_format
Not a PNG or JPEG. The body carries format and tagsFound, so you can still tell the user what was detected.
429
rate_limited
Rate limit or monthly quota. Honour Retry-After. See Rate limits & quotas.
429
quota
Monthly or daily allowance exhausted. Retrying before the window resets will not help.
503
not_configured
The 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.

Retry with backoff
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.
PreviousGET /v1/healthNext Rate limits & quotas