Error Codes
This page describes the response envelope used by every Secutix API endpoint and the
registry of status codes you can switch on in your client.
Response envelope
Every response — success and error, on every endpoint, at every HTTP status — uses the same JSON envelope with exactly three fields:
{
"status": "<code>",
"details": "<human-readable English message, null on success>",
"data": "<payload, null on errors>"
}
statusis the only field a client should switch on. It's a stable, machine-readable code, lower-casesnake_case.detailsis meant for logs and developer-facing UIs. The wording can change without notice — do not parse it.datacarries the payload on2xxand isnullon every4xx/5xx.
The same envelope is returned by the API Gateway (e.g. 401 for a missing token), the authorizer (403), and the backend services — so a single parser handles all cases.
Success
Every 2xx response uses the same status value:
{ "status": "success", "details": null, "data": { ... } }
Error codes
The status field on 4xx / 5xx responses is drawn from the two registers below.
The registry is intentionally open: new codes are added as new failure modes appear,
under a documented naming convention. Treat unknown codes the same way you treat the
generic code for the HTTP status you received.
Generic codes — one per HTTP family meaning
| HTTP | status | When |
|---|---|---|
| 400 | bad_request | Generic bad input (malformed payload, missing/invalid required field) |
| 400 | validation_error | Bean-validation constraint violation |
| 400 | missing_parameter | A required parameter is absent |
| 400 | invalid_parameter | A parameter has the wrong type, format, or enum value |
| 401 | unauthorized | Missing or expired authentication token |
| 403 | forbidden | Valid token but insufficient permission for the resource |
| 404 | not_found | Endpoint or resource id does not exist |
| 405 | method_not_allowed | HTTP method not supported on this path |
| 409 | conflict | The request conflicts with the current state of the resource |
| 412 | precondition_failed | Pre-condition (e.g. order state) not satisfied |
| 422 | validation_error | Semantic validation failed (well-formed but unprocessable) |
| 501 | not_implemented | Endpoint is declared but not yet wired |
| 502 | bad_gateway | Upstream system returned an error we propagate |
| 503 | service_unavailable | Service temporarily unavailable (maintenance, circuit breaker) |
| 500 | internal_error | Uncaught server-side failure |
Domain-specific codes
A code earns its own slot in this table when a client can take a different action based on it than for a sibling error at the same HTTP status. Otherwise the generic code above is enough.
Conventions for new codes:
- lower-case
snake_case; - prefixed with the domain (
cart_,contact_,payment_,shipment_, …); - the matching English message goes in
details, never instatus.
The seeded domain codes are tracked in
tools/backlog/domain-specific-error-codes.md and added to the registry as they ship.
Contact
| HTTP | status | When |
|---|---|---|
| 409 | contact_duplicate_email | POST /contacts/individuals or /contacts/structures rejected because another contact already carries this email (trim + case-insensitive match). The error envelope's data field carries the full existing contact. Pass ?allowDuplicateEmail=true to bypass (family / B2B shared-inbox cases). |
Parsing recipe
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
const body = await res.json();
if (body.status === "success") {
return body.data;
}
// Switch on the status code. Fall back to the HTTP status if the code is unknown.
switch (body.status) {
case "validation_error":
return showFormErrors(body.details);
case "not_found":
return show404();
case "unauthorized":
return redirectToLogin();
default:
return showGenericError(res.status, body.details);
}