Errors & retries
HTTP status codes, retry pattern, and when to retry vs. when not to.
Status codes
| Code | Meaning | What to do |
|---|---|---|
200 | OK | The response body holds the requested resource. |
400 | Bad Request | The request is invalid (missing parameter, wrong body shape). Fix the call and resend. Don't retry. |
401 | Unauthorized | Credentials are wrong or missing. Check that
user|company:password is shaped correctly and
base64-encoded. Don't retry until credentials are fixed. |
403 | Forbidden | Credentials work, but the user lacks the required privilege. Ask an
IntelliTracer admin to grant the right Api: ... role.
Don't retry. |
404 | Not Found | The resource doesn't exist, or not within your tenant. Check the
Id and the path. Don't retry. |
429 | Too Many Requests | You hit the rate limit. Wait the number of seconds in the
Retry-After header and try again. See
Rate limits. |
500 – 504 | Server-side error | Transient. Retry with exponential backoff (see below). If the failure persists past 5 attempts, escalate to support. |
Retry pattern
For transient failures use exponential backoff. Start at a 1-second delay, double after each failure, cap at 30 seconds. At most 5 retries per request. Add jitter (a random component between 0 and the delay) so retries don't land in lockstep.
| Attempt | Wait |
|---|---|
| 1 | immediate |
| 2 | 1s (+ jitter) |
| 3 | 2s (+ jitter) |
| 4 | 4s (+ jitter) |
| 5 | 8s (+ jitter) |
| 6 | 16s (+ jitter) |
| after 6 | give up, escalate |
When to retry, when not to
- Retry:
5xx(server-side) and429(rate limit, respectingRetry-After). - Don't retry:
4xxother than429. Those are caller bugs, not transient failures. Resending changes nothing and eats into your rate limit. - Don't retry: network timeouts on non-idempotent requests (writes) without server confirmation. Prefer a follow-up read to check whether the write landed. The API is read-only in V1, so this doesn't apply yet, but keep the rule in mind for future write work.
Example: retry wrapper
async function withRetry<T>(
fn: () => Promise<Response>,
opts: { maxRetries?: number; baseDelayMs?: number; capMs?: number } = {},
): Promise<Response> {
const max = opts.maxRetries ?? 5;
const base = opts.baseDelayMs ?? 1000;
const cap = opts.capMs ?? 30000;
for (let attempt = 0; attempt <= max; attempt++) {
const res = await fn();
if (res.ok) return res;
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get("retry-after") ?? "1", 10);
await sleep(retryAfter * 1000);
continue;
}
if (res.status >= 500) {
if (attempt === max) return res;
const delay = Math.min(base * 2 ** attempt, cap);
const jitter = Math.random() * delay;
await sleep(delay + jitter);
continue;
}
return res; // 4xx other than 429: don't retry
}
throw new Error("retry budget exhausted");
}
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); import random, time, requests
def with_retry(fn, max_retries=5, base_delay=1.0, cap=30.0):
for attempt in range(max_retries + 1):
res = fn()
if res.ok:
return res
if res.status_code == 429:
retry_after = int(res.headers.get("Retry-After", "1"))
time.sleep(retry_after)
continue
if 500 <= res.status_code < 600:
if attempt == max_retries:
return res
delay = min(base_delay * (2 ** attempt), cap)
time.sleep(delay + random.random() * delay)
continue
return res # 4xx other than 429: don't retry
raise RuntimeError("retry budget exhausted") Idempotency
All GET calls are idempotent: running them again gives the
same result and has no side effects. Retrying after a 5xx or
429 is therefore safe.
Writes (POST, PUT, DELETE) are not
generally available in V1. When writes ship, every write endpoint gets its
own idempotency strategy documented here. Until then: treat write timeouts
as ambiguous and check status via a follow-up GET.
More
- Rate limits for the exact budgets and throttle window.
- Authentication for resolving
401and403.