Error Codes
Every failure returns a JSON envelope with a stable, machine-readable errCode.
{
"success": false,
"errCode": "GC_INSUFFICIENT_BALANCE",
"message": "Insufficient gift card balance: requested 750.00, available 500.00"
}
:::caution Branch on errCode, never on message
errCode is a stable contract. message is human-readable, may include request-specific detail, and can change without notice. Never parse it or compare against it in code.
:::
Authentication and authorisation
| Error code | HTTP | Meaning | What to do |
|---|---|---|---|
UNAUTHORIZED | 401 | Credentials missing, or a credential type not permitted for this role | Check both headers are present and correct |
API_KEY_INVALID | 401 | Unknown client ID, or the secret does not match | Usually a rotated secret — check for a credentials email |
USER_INACTIVE | 403 | The API client account is deactivated | Contact the merchant administrator |
FORBIDDEN | 403 | Authenticated, but this endpoint is not open to an API client | The operation belongs to the dashboard — see Authentication |
TOKEN_EXPIRED | 400 | Applies to dashboard sessions, not client credentials | Not reachable with your credentials |
Request validation
| Error code | HTTP | Meaning | What to do |
|---|---|---|---|
BAD_REQUEST | 400 | Malformed JSON, a failed validation rule, an unknown field, a non-positive amount, a debit below the template minimum, or a refund with no matching debit | Fix the request — never retry unchanged |
TOO_MANY_REQUESTS | 429 | Rate limit exceeded | Back off and retry |
REQUEST_TIMEOUT | 408 | The request exceeded the 30-second limit | Retry with the same idempotency key |
BAD_REQUEST is deliberately broad on the write paths. Several distinct business failures share it, so read message when you need to explain the rejection to a human — but never branch on it.
An unknown field is also a BAD_REQUEST: strict decoding turns a typo into an immediate failure rather than a silently ignored value.
Idempotency and concurrency
| Error code | HTTP | Meaning | What to do |
|---|---|---|---|
IDEMPOTENCY_CONFLICT | 409 | A narrow internal race on a duplicate key | Retry with the same key |
VERSION_MISMATCH | 409 | Concurrent modification, or a balance change no longer permitted | Retry once with the same key |
:::danger There is no error for a misused key
Reusing an idempotency key with different parameters does not produce an error. The original outcome is replayed and the new parameters are discarded silently. IDEMPOTENCY_CONFLICT will not catch a key-derivation bug for you — see How Redemption Works.
:::
Campaign and template
| Error code | HTTP | Meaning | What to do |
|---|---|---|---|
PROGRAM_NOT_FOUND | 404 | The campaign behind the template or instrument no longer exists | Escalate — the configuration is broken |
PROGRAM_INACTIVE | 422 | The campaign is not ACTIVE | Not retryable — an administrator must activate or extend it |
TEMPLATE_NOT_FOUND | 404 | No such template within your merchant | Check the template ID you were given |
TEMPLATE_LIMIT_EXCEEDED | 422 | The template's count or value cap is reached | Not retryable — an administrator must raise the cap |
CAMPAIGN_LIMIT_EXCEEDED | 422 | The campaign's value budget or its velocity window is exhausted | Not retryable — same |
PROGRAM_INACTIVE is the one to instrument. Campaigns expire automatically at their end date, so a working integration can start failing with no change on your side.
CAMPAIGN_LIMIT_EXCEEDED covers both the lifetime value budget and a rolling velocity window. The code alone does not distinguish them; the message does.
Gift cards
| Error code | HTTP | Meaning | What to do |
|---|---|---|---|
GC_INVALID | 422 | No card matches this code or ID | Ask the customer to re-check the code |
GC_INACTIVE | 422 | The card is not ACTIVE | Decline the card |
GC_EXPIRED | 422 | Past its expiry date, or status EXPIRED | Decline the card |
GC_INSUFFICIENT_BALANCE | 422 | The requested debit exceeds the balance | Debit the remaining balance instead, and collect the rest by other means |
GC_PARTIAL_REDEMPTION_NOT_ALLOWED | 422 | A partial debit on a whole-balance-only card | Debit the full balance, or decline |
GC_INVALID deliberately covers both a non-existent code and one belonging to another merchant — the platform does not disclose which.
Coupons
| Error code | HTTP | Meaning | What to do |
|---|---|---|---|
CV_INVALID | 422 | No coupon matches this code | Ask the customer to re-check the code |
CV_ALREADY_USED | 422 | The coupon is not ACTIVE — used, voided, or it lost a redemption race | Decline |
CV_EXPIRED | 422 | Past its expiry date | Decline |
CV_USAGE_LIMIT_REACHED | 422 | The usage limit is exhausted | Decline |
CV_NOT_APPLICABLE | 422 | The discount could not be computed — no line in scope, or the order is below the coupon's minimum value | Read message to explain it to the customer |
At Validate Coupon these appear as data.code alongside valid: false at HTTP 200, not as errors.
Not-found codes differ by resource
| Endpoint | Code on a missing resource |
|---|---|
GET /v1/giftCards/{giftCardId} | RECORD_NOT_FOUND |
GET /v1/giftCards/transactions/{transactionId} | RECORD_NOT_FOUND |
GET /v1/coupons/{couponId} | INSTRUMENT_NOT_FOUND |
The inconsistency is real — handle both.
Platform
| Error code | HTTP | Meaning | What to do |
|---|---|---|---|
INTERNAL_SERVER_ERROR | 500 | A platform fault | Retry with the same idempotency key; escalate if it persists |
:::note Codes you will not see
The service defines a wider catalogue than it uses. GC_EXHAUSTED, GC_ALREADY_VOIDED, GC_DENOMINATION_INVALID, CV_INACTIVE, CV_MIN_ORDER_VALUE_NOT_MET, CV_NOT_STACKABLE, IDEMPOTENCY_KEY_REQUIRED, MERCHANT_LIMIT_EXCEEDED and USER_LIMIT_EXCEEDED are declared but are not returned by any endpoint you can reach. Do not write branches for them.
:::
Retry decision table
| Response | Retryable | How |
|---|---|---|
| Network timeout | ✅ Yes | Identical call, same idempotency key |
500 INTERNAL_SERVER_ERROR | ✅ Yes | Identical call, same key |
408 REQUEST_TIMEOUT | ✅ Yes | Identical call, same key |
429 TOO_MANY_REQUESTS | ✅ Yes | Back off first |
409 VERSION_MISMATCH | ✅ Yes | Identical call, same key |
409 IDEMPOTENCY_CONFLICT | ✅ Yes | Identical call, same key |
4xx business error | ❌ No | The operation was rejected on its merits |
401 / 403 | ❌ No | Fix credentials or escalate |
The single rule underneath this table: a retry must carry the original idempotency key. A new key on a retry is a second operation, and for a debit that means charging twice.
A worked handler
async function redeemGiftCard(code, amount, orderId) {
const idempotencyKey = `${orderId}-debit-01`; // deterministic, never inline uuid()
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(`${baseUrl}/v1/giftCards/transactions`, {
method: "POST",
headers: {
"X-Client-Id": clientId,
"X-Client-Secret": clientSecret,
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
type: "DEBIT",
amount,
referenceId: orderId,
idempotencyKey, // identical on every attempt
}),
});
const body = await res.json();
if (body.success) return body.data;
switch (body.errCode) {
case "VERSION_MISMATCH":
case "INTERNAL_SERVER_ERROR":
continue; // retry with the same key
case "IDEMPOTENCY_CONFLICT":
continue; // rare internal race — retry with the same key
case "GC_INSUFFICIENT_BALANCE":
case "GC_EXPIRED":
case "GC_INACTIVE":
case "GC_PARTIAL_REDEMPTION_NOT_ALLOWED":
throw new CardDeclined(body.errCode, body.message);
default:
throw new RedemptionFailed(body.errCode, body.message);
}
} catch (err) {
if (err instanceof TypeError && attempt < 2) continue; // network — retry, same key
throw err;
}
}
throw new RedemptionFailed("RETRY_EXHAUSTED", "no outcome after 3 attempts");
}
Related
- How Redemption Works — the reasoning behind the retry rules
- Authentication — credential failures
- API Overview — status code summary