Skip to main content

Create Transaction

Move value on a gift card. This is the redemption endpoint — a DEBIT spends balance, REFUND returns it, and VOID / EXPIRE change the card's status.

Every transaction is recorded in an immutable ledger with the balance before and after, and the balance mutation and the ledger entry commit in a single database transaction — money can never move without its ledger entry.

Endpoint

POST /v1/giftCards/transactions

Headers

HeaderTypeDescriptionRequired
X-Client-IdStringYour client ID✅ Yes
X-Client-SecretStringYour client secret✅ Yes
Content-TypeStringapplication/json✅ Yes

Request Body

FieldTypeDescriptionRequired
typeStringDEBIT, REFUND, VOID or EXPIRE✅ Yes
codeStringThe customer's digital code✅ For DEBIT
giftCardIdString (UUID)Internal card ID✅ For all other types
amountNumberValue to move. Must be > 0 for DEBIT and REFUND; ignored for VOID and EXPIRE✅ Yes
referenceIdStringYour own reference. For a REFUND this must be the referenceId of the original debit✅ Yes
idempotencyKeyStringMakes this call safe to retry✅ Yes

The identifier depends on the type

TypeIdentifierRationale
DEBITcodeThe holder is present and initiating the spend
REFUNDgiftCardIdA back-office reversal — no customer is presenting a card
VOIDgiftCardIdAn administrative cancellation
EXPIREgiftCardIdSystem-initiated

Sending the wrong one fails validation with 400 BAD_REQUEST. The design keeps the plaintext code confined to the one operation a customer actually initiates.

Example Request — Redeem

curl --location 'https://stage-platform-exlr8-incentives.exlr8now.com/v1/giftCards/transactions' \
--header 'X-Client-Id: YOUR_CLIENT_ID' \
--header 'X-Client-Secret: YOUR_CLIENT_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"code": "GC-7F3A-9K2M-4XQ8",
"type": "DEBIT",
"amount": 150,
"referenceId": "order-20260825-0042",
"idempotencyKey": "order-20260825-0042-debit-01"
}'

Response

Successful Response — 201 Created

{
"success": true,
"data": {
"transactionId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"merchantId": "b1946ac9-2c3d-4e5f-8a7b-6c5d4e3f2a1b",
"giftCardId": "9b2c4d6e-8f0a-4b1c-9d3e-5f7a1b2c3d4e",
"type": "DEBIT",
"amount": 150,
"balanceBefore": 500,
"balanceAfter": 350,
"referenceId": "order-20260825-0042",
"idempotencyKey": "order-20260825-0042-debit-01",
"createdAt": "2026-08-25T11:42:07Z"
}
}

Response Fields

FieldTypeDescription
transactionIdStringLedger entry ID — store it against your order
giftCardIdStringThe card that was resolved, even when you sent a code
typeStringThe transaction type performed
amountNumberValue moved, rounded to 2 decimal places
balanceBeforeNumberBalance immediately before
balanceAfterNumberBalance immediately after — authoritative
referenceIdStringYour reference, echoed
idempotencyKeyStringThe key it was committed under
createdAtStringRFC 3339, UTC

:::tip A DEBIT tells you the card ID Even when you identify a card by code, the response returns giftCardId. Capture it — you cannot issue a refund without it. :::

Debit rules

A debit is checked in this order. All of these are read from the card's template, and you can see them ahead of time via Gift Card Inquiry.

CheckFailure
Card expired, or status EXPIREDGC_EXPIRED
Card status not ACTIVEGC_INACTIVE
amount is zero or negativeBAD_REQUEST
amount below the template's minRedemptionAmountBAD_REQUEST
amount exceeds remainingBalanceGC_INSUFFICIENT_BALANCE
Partial redemption disabled and amount ≠ the full remaining balanceGC_PARTIAL_REDEMPTION_NOT_ALLOWED

Note that a below-minimum amount comes back as a plain BAD_REQUEST, not a gift-card-specific code — check minRedemptionAmount yourself rather than relying on the error to distinguish it.

Idempotency

idempotencyKey is required on every call, and it is what makes a retry safe.

First call → debits 150 → balanceAfter: 350
Retry, same key → debits nothing → returns the original transaction, balanceAfter: 350

The guarantee holds even when a retry arrives while the first call is still in flight: the duplicate is caught by a unique index, the second attempt rolls back entirely, and the original outcome is replayed.

:::danger A reused key silently ignores the new parameters The key alone identifies the operation. If you reuse a key with a different amount or a different card, the platform does not compare the parameters and does not raise a conflict — it returns the original transaction, and the new amount is never applied.

This makes an incorrectly-derived key fail silently: a second, genuinely different debit that happens to reuse a key appears to succeed while moving no money. Derive keys per operation:

{orderId}-{operation}-{sequence} e.g. order-20260825-0042-debit-01

Deterministic, so a retry after a crash reproduces it — never uuid() generated inline, which would debit twice. And never reuse an order's debit key for its refund, or the refund will be swallowed as a replay of the debit. :::

IDEMPOTENCY_CONFLICT is returned only in a narrow race where the duplicate record cannot be re-read afterwards. It is rare, and safe to retry.

Refunds

A refund credits value back. Its rules are stricter than most integrators expect, because REFUND must not be usable to mint value out of nothing.

The referenceId must be the one used for the original debit. The platform looks for prior DEBIT transactions on that card under exactly that referenceId, and refuses the refund if it finds none. A refund total can never exceed what was debited under that reference.

{
"giftCardId": "9b2c4d6e-8f0a-4b1c-9d3e-5f7a1b2c3d4e",
"type": "REFUND",
"amount": 150,
"referenceId": "order-20260825-0042",
"idempotencyKey": "order-20260825-0042-refund-01"
}

Note referenceId matches the debit exactly, while idempotencyKey differs. Getting this backwards is the most common failure:

MistakeResult
A new referenceId such as order-…-refundBAD_REQUEST — no matching debit found
Refunding more than was debited under that referenceBAD_REQUEST — refund exceeds the debited amount
Reusing the debit's idempotencyKeyThe debit is replayed; no refund happens

The card must also still be ACTIVE and unexpired — you cannot refund onto an expired card.

Void and expire

VOID and EXPIRE change the card's status rather than its balance, and ignore amount entirely.

TypeResulting status
VOIDINACTIVE
EXPIREEXPIRED
{
"giftCardId": "9b2c4d6e-8f0a-4b1c-9d3e-5f7a1b2c3d4e",
"type": "VOID",
"amount": 0,
"referenceId": "fraud-review-8871",
"idempotencyKey": "fraud-review-8871-void-01"
}

Expiry also runs automatically: an hourly job records an EXPIRE transaction against every active card past its expiry date, so the ledger stays the authoritative record of the change.

Concurrency

Balance mutations use optimistic concurrency — a change commits only if the card's version has not moved since it was read. Two simultaneous debits cannot both spend the same balance. Reads during a transaction go to the primary database, never a replica, so a debit is never computed against a stale balance.

When the compare-and-set fails, the response is 409 VERSION_MISMATCH. It covers concurrent modification, and also a change that is no longer permitted against the current balance. Retry it once with the same idempotency key; if it repeats, re-inquire and re-evaluate rather than looping.

Error Responses

Error codeHTTPCause
BAD_REQUEST400Missing field, wrong identifier for the type, non-positive amount, below the minimum redemption amount, or a refund that has no matching debit / exceeds it
VERSION_MISMATCH409Concurrent modification, or the change is not permitted against the current balance
IDEMPOTENCY_CONFLICT409Rare race on a duplicate key — retryable
TEMPLATE_NOT_FOUND404The card's template no longer exists
GC_INVALID422No card matches the code or ID
GC_INACTIVE422The card is not ACTIVE
GC_EXPIRED422Past its expiry date, or status EXPIRED
GC_INSUFFICIENT_BALANCE422The requested amount exceeds the balance
GC_PARTIAL_REDEMPTION_NOT_ALLOWED422Partial debit on a whole-balance-only card
INTERNAL_SERVER_ERROR500Platform fault — retryable with the same key
{
"success": false,
"errCode": "GC_INSUFFICIENT_BALANCE",
"message": "Insufficient gift card balance: insufficient balance"
}

Retry policy

Timeout or 5xx → retry the identical call, same key
409 VERSION_MISMATCH → retry once, same key
409 IDEMPOTENCY_CONFLICT → retry, same key
4xx business error → do not retry; the operation was rejected

A timeout is what idempotency exists for: you cannot know whether the debit committed before the connection dropped, so retry and let the key settle it.