Error Codes
Every error in the HummyTummy API is returned through a single, standard
error envelope. On the client, branch on the stable errorCode field —
not on the human-readable message.
Error envelope
Every error response follows this shape:
{
"statusCode": 409,
"message": "User with email '[email protected]' already exists",
"error": "RESOURCE_ALREADY_EXISTS",
"errorCode": "RESOURCE_ALREADY_EXISTS",
"timestamp": "2026-06-23T08:14:05.123Z",
"path": "/api/v1/auth/register",
"requestId": "1750665245123-a1b2c3d4e"
}| Field | Type | Description |
|---|---|---|
statusCode | number | HTTP status code. |
message | string | string[] | User-facing message. May be an array for validation errors. |
error | string | A human/category label (sometimes localized, or class-validator’s "Bad Request"). Do not branch on this. |
errorCode | string? | The stable, machine-readable code. The client should branch only on this. Present only when the thrown exception attaches one. |
timestamp | string | Request timestamp (ISO 8601). |
path | string | Request path. |
requestId | string? | Tracking id — include it in support requests. |
actionable | object? | Structured remediation payload. Today it carries the entitlement denial: requirement, offer, licenseRequired, reason. |
details | any? | Present only in the development environment. |
stack | string? | Present only in the development environment. |
errorCode carries no PII, so it is surfaced in every environment
(production included). details and stack, by contrast, appear only in the
development environment.
The error field may be localized or fall back to a framework-generated
label. Always branch behaviour on errorCode; when errorCode is
absent, fall back to statusCode.
errorCode values
The following codes come from the ErrorCode enum and are attached to business
logic exceptions (BusinessException).
Some gates (the entitlement guard, the purchasability gate, the stock gate)
throw their own code in a code field; the global error filter
standardises it onto errorCode, so the client still branches on a single
field. Those codes are listed under
Entitlements and purchasing.
Authentication
errorCode | Meaning |
|---|---|
INVALID_CREDENTIALS | Email or password is wrong. |
TOKEN_EXPIRED | The token has expired. |
TOKEN_INVALID | The token is invalid/malformed. |
UNAUTHORIZED | Authentication is required. |
Authorization
errorCode | Meaning |
|---|---|
FORBIDDEN | Access denied. |
INSUFFICIENT_PERMISSIONS | The role/permission is insufficient for this action. |
Resource
errorCode | Meaning |
|---|---|
RESOURCE_NOT_FOUND | The requested record does not exist. |
RESOURCE_ALREADY_EXISTS | The same record already exists (e.g. duplicate email). |
RESOURCE_CONFLICT | Conflicts with the resource’s current state. |
Validation
errorCode | Meaning |
|---|---|
VALIDATION_ERROR | Generic input validation failed. |
INVALID_INPUT | An input value is invalid. |
MISSING_REQUIRED_FIELD | A required field is missing. |
Business logic
errorCode | Meaning |
|---|---|
INSUFFICIENT_STOCK | Not enough stock for the product. details: { productName, available, requested }. |
ORDER_ALREADY_PAID | The order is already paid. |
TABLE_OCCUPIED | The table is occupied. |
INVALID_ORDER_STATUS | The action isn’t allowed in the order’s current status. |
QUOTA_EXCEEDED | The credit balance is insufficient (HTTP 402). details: { kind, remaining, requested, offerCode } — offerCode is the credit pack the client can deep-link to. |
SUBSCRIPTION_REQUIRED | Dormant. Retired with the plan rail; no code path throws it any more. It stays in the enum only so old clients don’t break. |
FEATURE_NOT_AVAILABLE | Dormant. Replaced by ENTITLEMENT_REQUIRED (HTTP 403), whose envelope carries the missing entitlement and the product that unlocks it, with its price. |
Payment
errorCode | Meaning |
|---|---|
PAYMENT_FAILED | The payment failed. |
PAYMENT_PROCESSING_ERROR | An error occurred while processing the payment. |
INVALID_PAYMENT_METHOD | Invalid payment method. |
System
errorCode | Meaning |
|---|---|
INTERNAL_SERVER_ERROR | Unexpected server error. |
SERVICE_UNAVAILABLE | The service is temporarily unavailable. |
DATABASE_ERROR | A database error. |
EXTERNAL_SERVICE_ERROR | An external service error (e.g. the payment provider). |
Rate limiting
errorCode | Meaning |
|---|---|
TOO_MANY_REQUESTS | Too many requests (HTTP 429). |
Tenant
errorCode | Meaning |
|---|---|
TENANT_NOT_FOUND | Tenant not found. |
INVALID_TENANT | Invalid tenant. |
Entitlements and purchasing
These codes are not in the ErrorCode enum; the gates throw them in a code
field and the global filter moves it onto errorCode, so the client still
branches on a single field.
errorCode | HTTP | Meaning |
|---|---|---|
ENTITLEMENT_REQUIRED | 403 | The route requires an entitlement the tenant does not hold. actionable carries requirement, offer, licenseRequired, reason. |
LICENSE_REQUIRED | 409 | A cart line needs a licence, and there is none owned or in the cart. |
ADDON_ALREADY_OWNED | 409 | The product (or the licence) is already active. |
ADDON_ALREADY_GRANTED | 409 | Everything the product grants is already in the entitlement set. |
ADDON_REQUIRES_DEPENDENCY | 409 | Its dependency is neither owned nor in the cart. |
ADDON_LIMIT_REDUNDANT | 409 | The capacity it adds is already unlimited. |
ADDON_MAX_QUANTITY | 409 | The catalog quantity ceiling would be exceeded. |
HARDWARE_OUT_OF_STOCK | 409 | Not enough real stock for a hardware line. |
CATALOG_INVALID | 400 | A superadmin catalog write violates the product invariants; message carries every problem as an array. |
Every 409 above is returned before any charge, inside
POST /v1/checkout/intent: the rejected line stops the whole cart, no
CheckoutIntent row is written, and PayTR is never called. Details in the
Licensing & Billing API and the
Entitlement Matrix.
Database errors → HTTP
Prisma’s known database errors (PrismaClientKnownRequestError) are translated
to meaningful HTTP status codes in the global exception filter. In these
responses the error field carries the category label; errorCode is not
present here (these are not BusinessExceptions).
| Prisma code | HTTP | error | Meaning |
|---|---|---|---|
P2002 | 409 Conflict | UniqueConstraintViolation | Unique constraint violation (duplicate record). |
P2025 | 404 Not Found | RecordNotFound | Record not found. |
P2003 | 400 Bad Request | ForeignKeyConstraintViolation | Related record missing, or can’t delete due to dependencies. |
P2014 | 400 Bad Request | RequiredRelationViolation | The change would violate a required relation. |
P2016 | 400 Bad Request | InvalidQuery | Invalid query parameters. |
P2021 | 500 Internal Server Error | DatabaseConfigError | Table does not exist (configuration error). |
P2024 | 503 Service Unavailable | DatabaseTimeout | Database connection timeout. |
P2034 | 409 Conflict | ConcurrentUpdate | Serializable transaction conflict (Postgres 40001). The transaction did not commit; the request is safe to retry. |
P2000, P2020 | 400 Bad Request | ValueOutOfRange | A value is out of the allowed range or too long. |
| (other) | 500 Internal Server Error | DatabaseError | An unmapped database error. |
P2034 (ConcurrentUpdate, 409) is the expected “loser” outcome of a
Serializable transaction under contention (checkout settlement, marketplace
purchase, etc.), and your request did not take effect. If you receive it,
you can safely resend the request as-is.
Handling errors on the client
const res = await fetch('/api/v1/display/orders', { method: 'POST', /* … */ });
if (!res.ok) {
const err = await res.json(); // ErrorResponse
switch (err.errorCode) {
case 'ENTITLEMENT_REQUIRED': {
// 403 — err.actionable.offer is the product that unlocks the missing
// entitlement, priced for THIS tenant today.
// reason === 'lapsed' → "Renew", otherwise → "Buy".
const { offer, reason, licenseRequired } = err.actionable ?? {};
showPurchasePrompt({ offer, reason, licenseRequired });
break;
}
case 'QUOTA_EXCEEDED':
// 402 — credits exhausted. Deep-link to err.details.offerCode.
break;
case 'LICENSE_REQUIRED':
case 'ADDON_ALREADY_OWNED':
case 'ADDON_ALREADY_GRANTED':
case 'ADDON_REQUIRES_DEPENDENCY':
case 'ADDON_LIMIT_REDUNDANT':
case 'ADDON_MAX_QUANTITY':
// 409 — the cart needs fixing; nothing was charged.
showToast(err.message);
break;
case undefined:
// No errorCode → branch on statusCode (e.g. 409 ConcurrentUpdate → retry)
break;
default:
showToast(err.message);
}
}