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 | auth, single-metadata |
202 | Accepted, processing async | audit (exports) |
204 | Success, no content | auth, payments, single-metadata (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 | auth, audit, single-metadata, metric, payments, audience-service |
404 | Resource not found | auth, payments, audit, single-metadata, metric, audience-service |
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 | payments, single-metadata, metric (unhandled exceptions) |
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 auth, audit, and payments. All three define the same ErrorResponse schema in their OpenAPI specs: a machine-readable error string, a human-readable message, and an optional details object.
{
"error": "invalid_cursor",
"message": "The cursor value is malformed or expired.",
"details": null
}A payments example, showing details populated:
{
"error": "insufficient_balance",
"message": "Wallet balance is too low for this operation",
"details": {
"required": 500,
"available": 120
}
}The auth service's OAuth endpoints (/oauth/token, /oauth/revoke) are the one exception inside this group: on a 400, they return a narrower OAuthErrorResponse instead, { error, error_description }, matching the OAuth 2.0 spec's own error shape rather than the platform's general one.
Format B: { errors: [{ message }] }
Used by single-metadata, metric, and audience-service. All three throw a shared IntemptException type that carries a list of messages, not 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.
single-metadata has 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. In single-metadata and metric, a 401 (from Spring Security's JWT validation) and a 403 (from a legacy ForbiddenException, kept for backward compatibility rather than used in new code) both 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 auth, payments, metric, single-metadata, integration, and several others) are checked against a Redis-backed 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
| 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 |
Other auth errors (missing auth, forbidden, conflict) use Format A but the OpenAPI spec documents them by status and description rather than a fixed code list, since error is service-generated per failure.
Audit
| Code | Status | Description |
|---|---|---|
invalid_parameter | 400 | A query parameter is out of range, e.g. limit above 200 |
unauthorized | 401 | Missing or invalid Bearer token |
forbidden | 403 | Missing the required permission on the audit_log resource |
event_not_found | 404 | No audit event exists with the given ID |
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 |
single-metadata, metric, audience-service
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 single-metadata 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 single-metadata or metric 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.
