Skip to main content

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 codeHTTPMeaningWhat to do
UNAUTHORIZED401Credentials missing, or a credential type not permitted for this roleCheck both headers are present and correct
API_KEY_INVALID401Unknown client ID, or the secret does not matchUsually a rotated secret — check for a credentials email
USER_INACTIVE403The API client account is deactivatedContact the merchant administrator
FORBIDDEN403Authenticated, but this endpoint is not open to an API clientThe operation belongs to the dashboard — see Authentication
TOKEN_EXPIRED400Applies to dashboard sessions, not client credentialsNot reachable with your credentials

Request validation

Error codeHTTPMeaningWhat to do
BAD_REQUEST400Malformed JSON, a failed validation rule, an unknown field, a non-positive amount, a debit below the template minimum, or a refund with no matching debitFix the request — never retry unchanged
TOO_MANY_REQUESTS429Rate limit exceededBack off and retry
REQUEST_TIMEOUT408The request exceeded the 30-second limitRetry 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 codeHTTPMeaningWhat to do
IDEMPOTENCY_CONFLICT409A narrow internal race on a duplicate keyRetry with the same key
VERSION_MISMATCH409Concurrent modification, or a balance change no longer permittedRetry 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 codeHTTPMeaningWhat to do
PROGRAM_NOT_FOUND404The campaign behind the template or instrument no longer existsEscalate — the configuration is broken
PROGRAM_INACTIVE422The campaign is not ACTIVENot retryable — an administrator must activate or extend it
TEMPLATE_NOT_FOUND404No such template within your merchantCheck the template ID you were given
TEMPLATE_LIMIT_EXCEEDED422The template's count or value cap is reachedNot retryable — an administrator must raise the cap
CAMPAIGN_LIMIT_EXCEEDED422The campaign's value budget or its velocity window is exhaustedNot 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 codeHTTPMeaningWhat to do
GC_INVALID422No card matches this code or IDAsk the customer to re-check the code
GC_INACTIVE422The card is not ACTIVEDecline the card
GC_EXPIRED422Past its expiry date, or status EXPIREDDecline the card
GC_INSUFFICIENT_BALANCE422The requested debit exceeds the balanceDebit the remaining balance instead, and collect the rest by other means
GC_PARTIAL_REDEMPTION_NOT_ALLOWED422A partial debit on a whole-balance-only cardDebit 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 codeHTTPMeaningWhat to do
CV_INVALID422No coupon matches this codeAsk the customer to re-check the code
CV_ALREADY_USED422The coupon is not ACTIVE — used, voided, or it lost a redemption raceDecline
CV_EXPIRED422Past its expiry dateDecline
CV_USAGE_LIMIT_REACHED422The usage limit is exhaustedDecline
CV_NOT_APPLICABLE422The discount could not be computed — no line in scope, or the order is below the coupon's minimum valueRead 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

EndpointCode 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 codeHTTPMeaningWhat to do
INTERNAL_SERVER_ERROR500A platform faultRetry 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

ResponseRetryableHow
Network timeout✅ YesIdentical call, same idempotency key
500 INTERNAL_SERVER_ERROR✅ YesIdentical call, same key
408 REQUEST_TIMEOUT✅ YesIdentical call, same key
429 TOO_MANY_REQUESTS✅ YesBack off first
409 VERSION_MISMATCH✅ YesIdentical call, same key
409 IDEMPOTENCY_CONFLICT✅ YesIdentical call, same key
4xx business error❌ NoThe operation was rejected on its merits
401 / 403❌ NoFix 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");
}