HTTP Error Codes Reference
The HTTP status codes, error response shapes, and per-service error codes you'll see calling the Intempt API, plus how they differ by service.
Overview
Every Intempt platform service sits behind a single gateway at api.intempt.com/v1, but the services behind it don't all return errors in the same shape. This reference covers the status codes you'll see, the two error response formats in use across the platform, and the named error codes each service documents, so you can write one error handler that branches correctly instead of assuming a single format everywhere.
📘 Good to know
See Authentication for how requests are authenticated before they reach a service, and Project Limits for the requests-per-minute cap tied to your organization's tier.
Standard HTTP status codes
| Status | Meaning | Where you'll see it |
|---|---|---|
200 | Success | All services |
201 | Resource created | Authentication, Data Hub |
202 | Accepted, processing async | audit (exports) |
204 | Success, no content | Authentication, Billing, Data Hub (typically DELETE) |
303 | See Other (redirect) | auth (OAuth/SSO callback flows) |
307 | Temporary redirect | auth (OAuth/SSO callback flows) |
400 | Invalid request body or parameters | All services |
401 | Missing or invalid authentication | All services |
402 | Payment required (insufficient balance, spend cap exceeded) | payments only |
403 | Insufficient permissions, or a policy blocks the action | Authentication, Audit, Data Hub, Analytics, Billing, Audiences |
404 | Resource not found | Authentication, Billing, Audit, Data Hub, Analytics, Audiences |
409 | Conflict (duplicate operation, blocked state) | auth, payments |
422 | Validation error (field constraints) | payments |
429 | Rate limited | Gateway (all services), and payments at the application level |
500 | Internal server error | Billing, Data Hub, Analytics |
501 | Not implemented (intentional stub) | auth (two SCIM group endpoints only, to avoid IdP error loops) |
503 | Dependent service unavailable | payments (Stripe or metric service down) |
Error response formats
Two different error body shapes exist across the platform. Which one you get depends on which service handled the request, not on the status code.
Format A: { error, message, details }
Used by payments. Defines an ErrorResponse schema in its OpenAPI spec: a machine-readable error string, a human-readable message, and an optional details object.
{
"error": "insufficient_balance",
"message": "Wallet balance is too low for this operation",
"details": {
"required": 500,
"available": 120
}
}Format B: { errors: [{ message }] }
Used by the Authentication, Audit, Data Hub, Analytics and Audiences endpoints. These return a list of messages rather than a single machine-readable code:
{
"errors": [
{ "message": "Brand abc-123 does not exist" }
]
}errors is always an array, since a single request can fail more than one validation check at once:
{
"errors": [
{ "message": "Name is required" },
{ "message": "Color must be a hex value" }
]
}There's no error code or details field in this format. Don't parse or switch on the message string; it's meant for display, not for branching logic.
Data Hub endpoints have one more variant, for DELETE requests blocked by existing relations:
{
"description": "Deletion cannot be performed because the entity has relations",
"relations": [
{ "type": "JOURNEY", "id": "j_123", "name": "Welcome flow" }
]
}Empty-body errors
Some 401 and 403 responses carry no body at all. On Data Hub and Analytics endpoints, some 401 and 403 responses return an empty body, not the errors array shown above. Check the status code first; don't assume a body is always present.
Rate limiting (429)
Rate limiting happens at two different layers, with two different response shapes.
At the gateway, requests to routes marked rate-limited (a majority of route groups, including Authentication, Billing, Analytics, Data Hub, integrations and several others) are checked against a token bucket before they ever reach the upstream service. If the bucket is empty, the gateway itself returns:
HTTP/1.1 429 Too Many Requests
(empty body)with these response headers:
X-RateLimit-Replenish-Rate: 30
X-RateLimit-Burst-Capacity: 50
X-RateLimit-Requested-Tokens: 1The upstream service never sees the request in this case, so there's no error/message body to parse, only the status and headers.
Inside payments, a 429 can also come from the service itself (not the gateway), and in that case it uses payments' own Format A error body like any other payments error.
Your organization's plan tier sets the requests-per-minute cap the gateway enforces; see Project Limits for the exact numbers per tier.
Per-service error codes
Auth
The auth service's OAuth endpoints (/oauth/token, /oauth/revoke) are a documented exception: on a 400, they return a narrower OAuthErrorResponse shape, { error, error_description }, matching the OAuth 2.0 spec rather than the platform's general format.
| Code | Status | Context |
|---|---|---|
authorization_pending, access_denied, expired_token, invalid_grant, invalid_request, unsupported_grant_type | 400 | OAuth device-flow and token-exchange errors, returned as OAuthErrorResponse |
Every other auth error (missing auth, forbidden, not-found, validation) uses Format B — a plain errors[].message string, not a fixed code list. For example:
// API key not found
{ "errors": [{ "message": "API key not found" }] }// Missing required field
{ "errors": [{ "message": "Name is required" }] }Audit
Uses Format B — no fixed error code list, each message is generated per failure. For example:
// Missing from/to
{ "errors": [{ "message": "Export requires 'from' and 'to' date range" }] }// Nonexistent export job ID
{ "errors": [{ "message": "Export job not found: <id>" }] }A nonexistent export job ID returns 400, not 404.
Payments
Wallet errors
| Code | Status | Description |
|---|---|---|
insufficient_balance | 402 | Wallet balance is too low for the requested operation |
subscription_paused | 409 | Cannot deduct while the subscription is paused |
SPEND_CAP_EXCEEDED | 402 | Org-level or member-level monthly spend cap would be exceeded |
attribute_budget_exceeded | 402 | Monthly budget for this AI attribute is exhausted |
attribute_budget_paused | 402 | This AI attribute has been paused (budget exceeded) |
already_refunded | 409 | The transaction has already been refunded |
transaction_not_found | 404 | No transaction found with the given ID |
operation_not_found | 404 | The operationKey does not exist in the catalog |
Subscription errors
| Code | Status | Description |
|---|---|---|
no_active_subscription | 404 | The org does not have an active subscription |
subscription_not_found | 404 | No subscription cache found for the org |
invalid_seat_type | 400 | The seat type is not valid for the current tier |
minimum_seats | 400 | Cannot remove the last full seat |
feature_disabled | 404 | The requested feature flag is not enabled |
Data Hub, Analytics, Audiences
These three services don't define a fixed error code list. They return the errors[].message array shown under Format B above, with the message text generated per failure rather than drawn from an enum.
Retry guidance
- 4xx errors aren't retryable. Fix the request instead.
- 429, gateway-level: back off and retry, using the
X-RateLimit-*headers to pace your next request. - 503 (payments): the circuit breaker for a dependent service (Stripe or metric) is open. Retry after 30 seconds.
- 500: retry with exponential backoff, up to 3 attempts.
Use cases
- Writing one API client wrapper that checks the response shape (
error/message/detailsvs.errors[]) before deciding how to surface an error to a user, since the field names differ by service. - Detecting gateway-level rate limiting by checking for a
429with an empty body and theX-RateLimit-*headers, versus an application-level429from payments that has a JSON body. - Handling
402 insufficient_balancefrom payments by prompting the org to top up credits, using thedetails.requiredanddetails.availablefields to show exactly how much is missing. - Handling a
400deletion-blocked response from a Data Hub endpoint by listing the blockingrelationsto the user instead of just showing the generic description string. - Retrying a payments call after a
503, waiting the documented 30 seconds before the next attempt instead of retrying immediately. - Parsing an OAuth
400from the auth service's token or revoke endpoints usingerror/error_description, not the platform's generalerror/message/detailsshape. - Distinguishing a payments
422validation error (field constraints) from a400(malformed request) when deciding whether to show a form-field error or a generic failure message. - Treating a bodyless
401/403from Data Hub or Analytics as a pure status-code check, since there's nomessagefield to log or display in that case.
Where to go next
- Authentication for how API keys and JWTs are validated before a request reaches any service.
- Project Limits for the per-tier requests-per-minute cap enforced at the gateway.
- Track Data for a worked example of the
200/400responses on a specific endpoint.
Migration Guide: Segment, Mixpanel, and Amplitude
How to move an existing Segment, Mixpanel, or Amplitude tracking setup to Intempt: the method mapping, connecting a source, and what to do with historical data.
Server-Side API Integration
Send tracking events to Intempt directly over HTTP from your backend, with no SDK installed on either end.
