Error Handling
Anatomy of an error
All errors follow the response envelope:
{ "success": false, "message": "Validation failed", "data": null, "errorCode": "VALIDATION_ERROR", "errors": [ { "field": "msisdn", "message": "msisdn must be at least 10 characters" } ]}The errorCode string is stable across releases. Parse it in code — never parse message,
which may change or be localized.
Retry strategy
| Status | Retry? | Strategy |
|---|---|---|
4xx (except 408, 429) | ❌ Never | Fix the request payload or auth. |
408, 429 | ✅ Yes | Exponential backoff; respect Retry-After header. |
5xx | ✅ Yes | Up to 3 retries, jittered backoff. |
async function withRetry(fn, { max = 3 } = {}) { for (let attempt = 1; attempt <= max; attempt++) { const res = await fn(); if (res.ok) return res; if (res.status < 500 && res.status !== 408 && res.status !== 429) return res; const retryAfter = Number(res.headers.get('retry-after')) || 2 ** attempt; await new Promise(r => setTimeout(r, retryAfter * 1000)); }}Checkout-specific errors
409 Out-of-stock during order creation
When a product runs out of stock between the customer viewing it and hitting Pay, the order endpoint returns:
{ "success": false, "errorCode": "CONFLICT", "message": "Insufficient stock for variant 22402", "data": { "type": "OutOfStockError", "variant_id": 22402 }}Render a friendly “Sorry, this item sold out” message and re-fetch the product to show updated stock. Do not retry the order without showing the user updated availability.
400 Fulfillment input errors
| Message | Cause | Fix |
|---|---|---|
"only one recipient identifier is allowed" | Two identifier keys in the same item | Remove the extra key |
"this product expects email but received uid" | Wrong identifier for this product | Use GET /orders/fulfillment-fields/:variantId to get the right key |
"Missing required fulfillment input: player_id" | A required field was omitted | Add the missing key |
400 Discount code errors
These errors are returned by both GET /api/v1/discounts/validate and POST /api/v1/orders
when a discount_code is supplied.
| Message | Cause | Fix |
|---|---|---|
"Discount code "X" is not valid or has been deactivated." | Code doesn’t exist or is_active=false. | Show “invalid code” to the user. |
"Discount code "X" is not active yet." | Current date is before start_date. | Inform the user when the code becomes valid. |
"Discount code "X" has expired." | Current date is after end_date. | Ask the user to try a different code. |
"Discount code "X" has reached its usage limit." | All redemptions used up. | Ask the user to try a different code. |
"Your subtotal (X BDT) is below the minimum purchase (Y BDT)…" | Cart total too small. | Show the minimum purchase amount and ask the user to add more items. |
Always validate the code with GET /api/v1/discounts/validate before allowing the user to
submit the order, so the checkout form can show real-time feedback without waiting for an
order creation error.
Preventing duplicate order submissions
If your checkout UI retries POST /api/v1/orders, use a client-side submission
lock (disable the Pay button until the first request resolves) and reconcile
using the returned order_number / transactionId before attempting a second
create call. This is especially important around payment redirects and slow
mobile connections.
See the full error code catalogue for codes you should special-case.