Error Codes
The Incapon API returns 2xx for successful requests, 4xx for client errors and 5xx for server-side problems. Whatever the status code, the error body always has the same shape, so a single error handler covers every tool.
The error body
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your balance is too low for this operation.",
"request_id": "req_8f2a91c4d7"
}
}code is stable and meant for machines; build your logic on it. message is for humans and may change. Write request_id to your logs: when you contact support, this id lets us find the request within seconds.
Status codes
| Status | code | Meaning | Retry |
|---|---|---|---|
| 400 | BAD_REQUEST | The request body is malformed or incomplete. | No |
| 401 | UNAUTHORIZED | The key is missing, invalid or revoked. | No |
| 402 | INSUFFICIENT_CREDITS | The balance does not cover this job. | After topping up |
| 403 | FORBIDDEN | The key's scopes or IP address do not allow this endpoint. | No |
| 404 | NOT_FOUND | The resource does not exist or belongs to another team. | No |
| 409 | CONFLICT | The resource is being changed by another operation. | Yes |
| 422 | VALIDATION_FAILED | Fields are well-formed but semantically wrong. | No |
| 429 | RATE_LIMIT | The limit of 600 requests per minute was exceeded. | Yes |
| 500 | INTERNAL | Unexpected server error. | Yes |
| 503 | UNAVAILABLE | The service is temporarily busy or under maintenance. | Yes |
Retrying with exponential backoff
Do not repeat a retryable request immediately. On a 429, first wait as long as the Retry-After header says; in other cases double the wait on every attempt and add random jitter. After five attempts, pass the error up.
async function withRetry(fn, maxAttempts = 5) {
for (let attempt = 0; ; attempt++) {
const res = await fn();
const retryable = [409, 429, 500, 503].includes(res.status);
if (!retryable || attempt + 1 >= maxAttempts) return res;
// Honour Retry-After if present, otherwise 1s, 2s, 4s, 8s + jitter
const retryAfter = Number(res.headers.get('retry-after'));
const delay = retryAfter ? retryAfter * 1000 : 2 ** attempt * 1000 + Math.random() * 250;
await new Promise((r) => setTimeout(r, delay));
}
}import random
import time
def with_retry(fn, max_attempts=5):
for attempt in range(max_attempts):
res = fn()
if res.status_code not in (409, 429, 500, 503) or attempt + 1 == max_attempts:
return res
# Honour Retry-After if present, otherwise 1s, 2s, 4s, 8s + jitter
retry_after = res.headers.get("Retry-After")
delay = int(retry_after) if retry_after else 2 ** attempt + random.random() / 4
time.sleep(delay)Send an Idempotency-Key header with POST requests. A repeat with the same key returns the result of the first request, so a retry never creates two monitors or a job that is charged twice. The official SDKs do this, and the backoff, on their own.
When you get a 402, check the balance instead of retrying; Auto Top-Up all but eliminates this error in production.