Errors
The error envelope, HTTP status codes, the full error code catalog, and safe retry patterns
The Invitebase API uses conventional HTTP status codes and returns a consistent JSON error envelope on every failure. Errors are designed to be actionable: the type tells you the class of problem, the code tells you the exact problem, and param points at the offending field.
The error envelope
Every non-2xx response has this shape:
{
"error": {
"type": "invalid_request_error",
"code": "parameter_missing",
"message": "Missing required parameter: referred_user_id.",
"param": "referred_user_id",
"doc_url": "https://docs.invitebase.com/implementation/platform/errors#parameter_missing"
}
}| Field | Description |
|---|---|
type | One of invalid_request_error, authentication_error, rate_limit_error, api_error. Always present. |
message | Human-readable explanation. Log it; do not parse it — branch on type and code. |
code | Machine-readable identifier from the catalog below. |
param | The request parameter the error relates to, when applicable. |
doc_url | Link to the relevant docs section. |
HTTP status codes
| Status | Meaning | Error type |
|---|---|---|
200 | Request succeeded. | — |
201 | Resource created. | — |
202 | Accepted for async processing (event ingestion). | — |
400 | Malformed or invalid request — bad JSON, missing or invalid parameters, invalid qualification gate. | invalid_request_error |
401 | Missing, invalid, or revoked API key. | authentication_error |
402 | The action needs funds your prefunded balance does not have. | invalid_request_error |
403 | The key is valid but not allowed to do this — publishable key on a secret-only endpoint, or wrong mode. | authentication_error |
404 | The resource does not exist — or exists in the other mode or another org. | invalid_request_error |
409 | Conflict — usually a concurrent retry with the same Idempotency-Key still in flight. | invalid_request_error |
422 | The request is well-formed but semantically unusable — e.g. an idempotency key reused with a different payload. | invalid_request_error |
429 | Rate limit exceeded. See Rate limits. | rate_limit_error |
5xx | Something failed on Invitebase's side. Rare; safe to retry with backoff. | api_error |
404 is deliberately indistinguishable between "never existed", "wrong mode", and "different org" — the API never confirms the existence of another tenant's objects.
Error code catalog
| Code | Status | Meaning | Retry? |
|---|---|---|---|
parameter_missing | 400 | A required parameter was not provided (param names it). | No — fix the request. |
parameter_invalid | 400 | A parameter has the wrong type, format, or an out-of-range value. | No — fix the request. |
api_key_missing | 401 | No Authorization or X-Publishable-Key header. | No. |
api_key_invalid | 401 | The key does not exist or is malformed. | No. |
api_key_revoked | 401 | The key was revoked or its roll grace period ended. | No — use the replacement key. |
key_type_not_allowed | 403 | A publishable key called a secret-only endpoint. | No — call from your server with a secret key. |
mode_mismatch | 403 | The request references an object from the other mode (e.g. a test campaign with a live key). | No. |
resource_missing | 404 | No such object in this org and mode. | No. |
balance_insufficient | 402 | The prefunded balance cannot cover the configured payout. Payouts queue rather than fail; top up to resume. | Yes — after topping up. |
idempotency_key_in_use | 409 | A request with this Idempotency-Key is still being processed. | Yes — after a short delay, same key. |
idempotency_payload_mismatch | 422 | This Idempotency-Key was already used with a different request body. | No — use a new key for a new request. |
rate_limit_exceeded | 429 | Too many requests for this key. | Yes — honor Retry-After, back off. |
internal_error | 500 | Unexpected failure on Invitebase's side. | Yes — with backoff and the same Idempotency-Key. |
service_unavailable | 503 | Temporary unavailability (deploys, upstream issues). | Yes — with backoff and the same Idempotency-Key. |
Retrying safely
Two rules make retries safe:
- Only retry retryable errors:
429,5xx, network failures/timeouts, and409 idempotency_key_in_use. Retrying a400will fail forever. - Reuse the same
Idempotency-Keyon every attempt of the same logical request, so a retry of a request that actually succeeded returns the original result instead of creating a duplicate.
Every POST endpoint accepts the Idempotency-Key header (any unique string up to 255 characters): the first request with a given key is processed normally and its response recorded, and any later request with the same key replays that response without re-executing anything. Generate the key once, before the first attempt, and reuse it on every retry — a fresh key per attempt defeats the mechanism. Keys are scoped per endpoint, per organization, per mode, and retained for 24 hours, so keep your retry horizon inside that window; reusing a key with a different body returns 422 idempotency_payload_mismatch (see the table above). The SDKs manage keys automatically for the events they send — you only handle them on direct API calls.
import { randomUUID } from "node:crypto";
async function postWithRetry(path: string, body: unknown, maxAttempts = 5) {
const idempotencyKey = randomUUID(); // one key for all attempts
for (let attempt = 1; ; attempt++) {
const res = await fetch(`https://api.invitebase.com${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INVITEBASE_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
}).catch(() => null); // network error → retryable
if (res && res.status < 400) return res.json();
const retryable =
!res || res.status === 429 || res.status === 409 || res.status >= 500;
if (!retryable || attempt >= maxAttempts) {
throw new Error(`Invitebase request failed: ${res?.status ?? "network"}`);
}
const retryAfter = Number(res?.headers.get("Retry-After")) || 0;
const backoff = Math.min(2 ** attempt * 500, 30_000); // 1s, 2s, 4s… cap 30s
const jitter = Math.random() * 250;
await new Promise((r) => setTimeout(r, Math.max(retryAfter * 1000, backoff) + jitter));
}
}
await postWithRetry("/v1/events", {
name: "subscription_started",
referred_user_id: "usr_123",
properties: { plan: "paid" },
});Errors in test mode
Test and live mode share the same error surface — a request that returns qualification_gate_invalid in test mode returns it in live mode too. Build your error handling against test keys and it carries over unchanged. Every API error is also visible in the dashboard's API log under Developer tools, with the full request and response.