Errors
Every error response from /api/v1/* carries a stable, machine-readable code field. Partners filter on code, not on HTTP status (statuses are shared across multiple codes) and not on the message (which is localized). The code space is append-only: a code that ships never gets renamed, renumbered, or removed.
Envelope
A typical 400 response:
{ "code": "VALIDATION_FAILED", "message": "Name is required.", "errors": { "name": ["Name is required."] }}code: one of the values in the catalog below. Filter on this.message: a human-readable description, localized to the caller’sAccept-Language(defaults tohr). Do not parse this programmatically.errors: present onVALIDATION_FAILEDresponses. A dictionary keyed by field name with an array of localized messages per field.
Server errors (HTTP 500) omit the errors dictionary and carry a generic message. Retry with back-off; if the error persists, contact support with the request id (visible in the response X-Request-Id header where present).
Catalog
| Code | HTTP | When it fires | Partner response |
|---|---|---|---|
VALIDATION_FAILED | 400 | Request body failed validation, or a required header is missing. | Inspect the errors dictionary. Surface field-level messages to the end user. |
UNAUTHORIZED | 401 | The API key is missing, malformed, or revoked. | Check the Authorization header. Re-issue the key if it was revoked. |
API_ACCESS_NOT_ENABLED | 402 | The target company does not have the ApiAccess subscription feature. | Upgrade the company to Enterprise, or add the ApiAccess add-on, in the billing settings. |
FORBIDDEN | 403 | Authenticated, but the key’s issuing user lacks permission for the operation in this scope. | Check the acting user’s role in the target company. Re-issue the key as an admin. |
NOT_FOUND | 404 | The target resource does not exist or has been deleted. | Verify the id. The resource may have been removed by another actor. |
CONFLICT | 409 | Duplicate resource, stale precondition, or an idempotency-key replay with a different body. | Reconcile state. Retry may succeed with a different payload, or may require no-op. |
RATE_LIMIT_EXCEEDED | 429 | Per-key rate limit window (per-second, per-hour, or per-day) was exceeded. | Back off per the Retry-After header. See Rate limits. |
KEY_REVOKED | 401 | Reserved for a future custom challenge handler. Not currently emitted. | Pre-wire your handler. Today the revoked-key flow returns a bare HTTP 401. |
INTERNAL_ERROR | 500 | An unhandled server error. | Retry with exponential back-off. If the error persists, contact support. |
Append-only guarantee
Once shipped, a code stays in the catalog forever, even if its emitter is deleted. Partners can rely on their code switch statements never silently breaking because locco renamed a value. If behavior around a code changes (different HTTP status, new constraint) it is called out in the changelog; the code string itself remains stable.
Partners are encouraged to handle unknown codes defensively: if a new code ships before your client is updated, treat it as a generic failure (probably INTERNAL_ERROR-shaped) and log the raw response for later review rather than crashing.