Responses & errors
The single response envelope every endpoint returns, pagination, and the full error-code catalogue.
Every endpoint — success or failure — returns the same JSON envelope.
The envelope
{
"data": { "...": "the payload (or null)" },
"meta": {
"success": true,
"message": "optional human-readable message",
"errorCode": null,
"timestamp": "2026-06-04T10:00:00Z"
},
"pagination": null
}| Field | Meaning |
|---|---|
data | The payload on success. For list endpoints, this is the page's items. |
meta.success | Boolean outcome flag. |
meta.message | Optional narrative (e.g. "Resource created" or an error description). |
meta.errorCode | Stable machine-readable code on failure (see below); null on success. |
meta.timestamp | Server UTC instant. |
pagination | Present only on list endpoints. |
Branch on meta.errorCode, not on meta.message — messages are for humans and may
change; codes are part of the contract and never change meaning.
Pagination
List endpoints put the items in data and the page info in pagination:
{
"data": ["...items..."],
"meta": { "success": true, "timestamp": "2026-06-04T10:00:00Z" },
"pagination": { "page": 0, "size": 20, "totalElements": 137, "totalPages": 7 }
}Pages are zero-indexed. For the full set of query parameters — page, size,
sortBy, order (ASC/DESC), and per-endpoint filters — and how pagination
is computed, see Pagination & filtering.
Error codes
Errors return the envelope with meta.success = false and a meta.errorCode. The
HTTP status reflects the category.
errorCode | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | Request failed validation; data holds a field → message map. |
UNAUTHORIZED | 401 | Missing/invalid credentials. |
TOKEN_EXPIRED / SESSION_EXPIRED | 401 | Admin session no longer valid. |
REAUTH_REQUIRED | 403 | Admin must re-authenticate before a sensitive action. |
INVALID_CREDENTIALS | 401 | Bad email/password (admin login). |
FORBIDDEN | 403 | Authenticated but not permitted. |
RESOURCE_NOT_FOUND | 404 | No such resource in your scope. |
RESOURCE_ALREADY_EXISTS | 409 | Uniqueness conflict. |
INVALID_OPERATION_STATE | 422 | The resource isn't in a state that allows the action. |
IDEMPOTENCY_KEY_CONFLICT | 409 | Same idempotency key, different body. |
IDEMPOTENCY_IN_PROGRESS | 409 | Same idempotency key still processing. |
LAST_SUPER_ADMIN_PROTECTED | 409 | Cannot disable the last Super Admin. |
LEDGER_ALREADY_REVERSED | 422 | A ledger entry was asked to be reversed twice. |
ACCOUNT_LOCKED | 423 | Admin account is locked. |
ACCOUNT_DISABLED | 403 | Admin account is disabled. |
WEAK_PASSWORD | 400 | Password does not meet policy. |
PAYMENT_GATEWAY_ERROR | 502 | The gateway was unreachable or rejected the operation. |
LEDGER_UNBALANCED | 422 | Ledger invariant breach (debits ≠ credits). |
INTERNAL_ERROR | 500 | Unexpected server error. |
Validation errors
A 400 VALIDATION_ERROR puts the per-field messages in data:
{
"data": { "amountMinor": "must be greater than 0" },
"meta": { "success": false, "errorCode": "VALIDATION_ERROR",
"message": "Validation failed", "timestamp": "…" }
}Malformed or oversized JSON bodies
A request body that is not parseable as JSON — syntactically broken, oversized, or
too deeply nested (for example a metadata payload that trips the hardened JSON
read limits) — never reaches validation. It returns 400 VALIDATION_ERROR with a
generic message and no per-field map (data is null):
{
"data": null,
"meta": { "success": false, "errorCode": "VALIDATION_ERROR",
"message": "Malformed JSON request body", "timestamp": "…" }
}Branch on meta.errorCode to tell the two 400 cases apart: a field-level
rejection carries a data map of field → message, while an unparseable body
has data: null. The raw parser detail is never echoed back — it could leak
fragments of your body — so the message stays generic.
Uniqueness and integrity conflicts
A database integrity violation — for example two requests racing to create the
same unique resource, or any insert/update that breaks a unique constraint —
surfaces as 409 RESOURCE_ALREADY_EXISTS with a generic message:
{
"data": null,
"meta": { "success": false, "errorCode": "RESOURCE_ALREADY_EXISTS",
"message": "Resource conflict", "timestamp": "…" }
}This is the same code a deliberate uniqueness check returns, so treat
RESOURCE_ALREADY_EXISTS as "this resource already exists" regardless of which
layer caught it. For repeated create-style mutations, prefer an
idempotency key so a retry replays the original
response instead of racing into a conflict.
Pagination & filtering
How list and search endpoints page, sort, and filter — the pagination block, app-API Pageable params, admin sortBy/order/paginate params, and unpaginated lists.
Catalog & subscription data model
How products, prices, benefits, coupons, subscriptions, one-time purchases, and entitlements relate — every entity scoped to an app and mode.