Errors & retries

One error shape, a small set of status codes, and a simple rule: retry confirmed no-charge failures, but inspect an unknown outcome before sending it again.

The error envelope

Every non-2xx response from the API is a Stripe-style envelope. type is the stable category, code is the machine-readable reason, message is for humans, and param names the offending field when there is one:

402 Payment Required
{
  "error": {
    "type": "api_error",
    "code": "insufficient_credits",
    "message": "Insufficient credits: need 4, have 1. Purchase credits to continue."
  }
}

type is one of: invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, rate_limit_error, api_error, platform_error.

/v1/data/* endpoints keep their own envelope on failure too, so success and failure parse the same way. An unavailable data action returns 501:

501 on /v1/data/*
{
  "status": "ERROR",
  "request_id": "01K3ZB...",
  "error": { "code": "not_implemented", "message": "This data action is currently unavailable." }
}

Status codes

StatusCodesWhat to do
400bad_request, parameter_invalid, capability_not_supportedFix the request. The message says what is wrong; don't retry unchanged.
401api_key_invalid, unauthenticatedThe key is missing, revoked or mistyped. Check the prefix and the header.
402insufficient_credits, spend_cap_exceededOut of credits, or a spend cap stopped the call. Top up or raise the cap — the call was not executed and nothing was charged.
403permission_deniedThe key is valid but lacks the scope (e.g. a read-only key on a mutation).
404resource_missingGenuinely missing OR outside your tenant's scope — we never leak existence across tenants, so both look identical.
409conflictState conflict (e.g. redriving a delivery that isn't dead).
422parameter_invalidSchema validation failed. param points at the field.
429rate_limitedSlow down. A Retry-After header tells you exactly how many seconds to wait.
500internal_errorOur bug. Quote the request_id and inspect the outcome before retrying.
501not_implementedThis data action is not currently available. Retrying won't help — contact support if you expected coverage.
502platform_errorThe data service was temporarily unavailable. Never charged. Retry with backoff.

Retry policy

  • 429 — wait Retry-After seconds, then retry. That header is authoritative; don't invent your own interval.
  • 502 — exponential backoff with a cap (1s → 2s → 4s → …, max ~60s, ~5 attempts). A confirmed 502 is never charged; an unknown timeout or 500 needs inspection.
  • 4xx (except 429) — do not retry. Fix the request, the key, or the balance first.
  • POSTs — persisted Idempotency-Key replay is available for bulk-job submissions and subscription Checkout. Synchronous data and workflow requests need receipt inspection before retrying an unknown outcome.
Reference retry loop (Python)
import time
import requests

def call_with_retry(url, *, headers, json=None, max_attempts=5):
    """429/502 honor backoff; unknown 5xx outcomes stop for inspection."""
    delay = 1.0
    for attempt in range(max_attempts):
        resp = requests.post(url, headers=headers, json=json, timeout=60)

        if resp.status_code == 429:
            retry_after = resp.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else delay)
            delay = min(delay * 2, 60)
            continue

        if resp.status_code == 502:
            time.sleep(delay)  # confirmed service failures are never charged
            delay = min(delay * 2, 60)
            continue

        if resp.status_code >= 500:
            raise RuntimeError("unknown server outcome; inspect request_id before retrying")

        resp.raise_for_status()  # 4xx: fix the request, don't retry
        return resp.json()

    raise RuntimeError("exhausted retries")

Talking to support? Every response — success or error — carries a request_id. Quote it; it is the primary key to the call in our ledger. You can also replay any call yourself from the playground.