Error Codes
Every non-2xx response carries a stable errorCode string. Handle these in
code instead of parsing message (which is localized / may change).
Standard codes
| HTTP | errorCode | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body/query failed DTO validation. |
| 400 | BAD_REQUEST | Generic client error. |
| 401 | UNAUTHORIZED | Missing, expired, or malformed bearer token. |
| 403 | FORBIDDEN | Authenticated, but lacks the required permission/role. |
| 404 | NOT_FOUND | Resource does not exist (or you can’t see it). |
| 409 | CONFLICT | Duplicate / state conflict (e.g. email already registered). |
| 422 | UNPROCESSABLE_ENTITY | Semantic validation error beyond DTO checks. |
| 429 | RATE_LIMITED | Too many requests; retry after the header duration. |
| 500 | INTERNAL_ERROR | Unexpected server failure. Please report with X-Request-ID. |
Domain-specific codes (selection)
errorCode | Meaning |
|---|---|
INVALID_CREDENTIALS | Sign-in failed — wrong MSISDN or password. |
OTP_EXPIRED | One-time password no longer valid. |
OTP_MISMATCH | Submitted OTP doesn’t match. |
PRODUCT_UNAVAILABLE | Product is out of stock or inactive. |
VOUCHER_ALREADY_REDEEMED | The voucher was already claimed. |
PAYMENT_FAILED | Upstream payment gateway declined. |
INSUFFICIENT_BALANCE | Wallet doesn’t cover the charge. |
OUT_OF_STOCK | All inventory reserved by the time the order attempted to lock stock. Render “sold out” and re-fetch the product. |
DISCOUNT_INVALID | Discount code does not exist, is inactive, has expired, or has hit its usage limit — returned by both the validate endpoint and POST /orders. |
DISCOUNT_NOT_STARTED | Discount code’s start_date has not been reached yet. |
DISCOUNT_EXPIRED | Discount code’s end_date has passed. |
DISCOUNT_USAGE_LIMIT_REACHED | Discount code has been fully consumed. |
DISCOUNT_MIN_PURCHASE_NOT_MET | Cart subtotal is below the code’s min_purchase threshold. |
Admin fulfillment recovery codes
These codes are returned by the /admin/orders/:id/fulfillment/* endpoints.
| HTTP | errorCode | When |
|---|---|---|
| 400 | BAD_REQUEST | Order has no SUCCEEDED payment — refusing to dispatch a voucher. |
| 404 | NOT_FOUND | Order or order item does not exist. |
| 409 | CONFLICT | Item already fulfilled, a successful attempt already exists (idempotency guard), or the voucher code is already assigned to another item. |
Handling errors
const res = await fetch(url, opts);const body = await res.json();if (!body.success) { switch (body.errorCode) { case 'UNAUTHORIZED': return redirectToLogin(); case 'RATE_LIMITED': return retryWithBackoff(); case 'VALIDATION_ERROR': return showFieldErrors(body.errors); default: return toast(body.message); }}Request tracing
Every response includes an X-Request-ID header. Include it when filing
support tickets — it’s the fastest way to look up the corresponding server
log.