Sell, redeem, and refund gift cards
A gift card's life runs in five steps: it gets value, its code reaches the holder, the holder spends it, a return puts value back, and you reconcile what's outstanding. Each step below is the shortest path through the API. Gift cards has the rules behind every call: limits, statuses, codes, disputes, and cash-outs.
| Step | Calls | Section |
|---|---|---|
| Give a card value | Sell a catalog gift card on an order, or issue one directly | 1. Sell or issue a card |
| Send the code | Recipient email | 2. Deliver the code |
| Spend it | Apply it to an order, or redeem it from your register | 3. Redeem the card |
| Take a return | Refund each tender that paid | 4. Refund a split-tender sale |
| Close the books | Transaction feed, webhooks, liability report | 5. Reconcile |
Before you start#
You need a sandbox API key with the scopes for the steps you use:
commerce.products.writeandcommerce.orders.writeto sell cards from your catalog and to apply cards to orders.commerce.gift_cards.writeto issue cards andcommerce.gift_cards.secrets.writeto email codes.commerce.gift_cards.redemptions.writeto redeem from your own register, andcommerce.gift_cards.readto read cards and the transaction feed.commerce.refunds.writefor refunds andreports.writefor the liability report.
Gift cards are USD only. Amounts are in cents. Replace the example IDs with your own.
1. Sell or issue a card#
Sell cards through the catalog when buyers pay you for them in Flint. Issue them directly when the value comes from somewhere else, such as a register that isn't on Flint or another gift card system.
Sell a catalog gift card#
Create a product with product_type: "gift_card". This one sells at face value, in any amount from $10.00 to $500.00, with $25.00 as the default:
curl -X POST https://api.withflintpay.com/v1/products \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: product-gift-card" \
-H "Content-Type: application/json" \
-d '{
"variant_mode": "simple",
"name": "Gift card",
"product_type": "gift_card",
"default_variant": {
"unit_price_money": { "amount": 2500, "currency": "USD" },
"gift_card_configuration": {
"price_mode": "face_value",
"face_value_money": { "amount": 2500, "currency": "USD" },
"custom_amount_bounds": {
"minimum_money": { "amount": 1000, "currency": "USD" },
"maximum_money": { "amount": 50000, "currency": "USD" }
}
}
}
}'
Gift card variants are never taxed and take no inventory, modifiers, or delivery profile. To sell a card for less than its value, such as $100.00 of value for $90.00, use price_mode: "discounted"; Catalog setup shows the configuration.
Put the variant on an order with the amount the buyer chose and who the card is for. Flint applies purchase limits to the buyer, so paying needs a customer_id for a customer with an email address, or a checkout buyer who verified their identity:
curl -X POST https://api.withflintpay.com/v1/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: order-gift-card-1001" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cus_1kmn0aExample",
"line_items": [{
"type": "variant",
"variant_id": "var_1kmn0aExample",
"quantity": 1,
"gift_card_purchase": {
"face_value_money": { "amount": 5000, "currency": "USD" },
"recipient": {
"email": "sam@example.com",
"name": "Sam",
"message": "Happy birthday!"
}
}
}]
}'
Don't send unit_price_money: Flint prices the line from the offer. Collect payment the way you collect any order, through hosted checkout, your own checkout, or a test token in sandbox. Each card is issued once its full price is captured and settled, and line_items[].purchased_gift_cards lists them by gift_card_id and last4. Order reads never include codes.
With a recipient, Flint emails the recipient as soon as the card is issued, or at recipient.send_at if you set one. Without one, send the code yourself in step 2. Gift card purchase lines covers changing the amount or recipient before payment.
Issue or import a card#
POST /v1/gift-cards creates a card without an order. Send funding with the value and where it came from: import for a balance moved from another system, or external_payment for a sale you collected outside Flint, which also needs source.customer_id and consideration_money, the amount paid. To keep the original code, supply a top-level code only with import funding. It must contain 16 gift card alphabet characters after removing spaces and hyphens; every code input ignores ASCII spaces and hyphens. A duplicate code in the same merchant environment returns 409 ALREADY_EXISTS at code. This imports a $50.00 balance with its existing code:
curl -X POST https://api.withflintpay.com/v1/gift-cards \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: import-legacy-card-0042" \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"code": "7783 3200 0100 1635",
"external_reference_id": "legacy-card-0042",
"funding": {
"value_money": { "amount": 5000, "currency": "USD" },
"source": {
"funding_source_type": "import",
"external_reference_id": "legacy-import-2026-10"
}
}
}'
{
"data": {
"gift_card": {
"gift_card_id": "gc_1kmn0aExample",
"code_source": "imported",
"status": "active",
"last4": "1635",
"balance_money": { "amount": 5000, "currency": "USD" },
"available_money": { "amount": 5000, "currency": "USD" },
"version": 2
}
},
"request_id": "req_..."
}
The response is abridged. Idempotency-Key is required; reuse it after a timeout. Supplying code sets code_source to imported and omits data.code and data.code_recovery_expires_at, including on replays. Without code, Flint generates one and returns it as data.code, recoverable through a replay for 24 hours. Store codes as credentials, never in URLs or logs. Retire an imported balance in its old system so it can't be spent twice. Without funding, the card stays pending until you load it.
Holders can spend imported codes in checkout. To save an imported-code card in a buyer account, use a private recipient link; saving it by code returns not found. Rotate imported codes when you can reach their holders; rotation sets code_source to generated. Delete migration exports after completing the import.
To add paid value to an existing card, use POST /v1/gift-card-loads with gift_card_id, value_money, and source in the body. Add expected_gift_card_version with the card's version from your read to reject a concurrent change. List that card's loads with GET /v1/gift-card-loads?gift_card_id={gift_card_id} and retrieve one with GET /v1/gift-card-loads/{gift_card_load_id}.
2. Deliver the code#
To email a card's code, create a notification. Flint sends the recipient a private link to the code:
curl -X POST https://api.withflintpay.com/v1/gift-card-notifications \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: notify-gc-0042" \
-H "Content-Type: application/json" \
-d '{
"gift_card_id": "gc_1kmn0aExample",
"recipient": {
"email": "sam@example.com",
"name": "Sam",
"message": "Enjoy!"
}
}'
Add recipient.send_at to send it later, up to 90 days ahead. You can also send notification in the POST /v1/gift-cards body to issue and email in one call. Both need commerce.gift_cards.secrets.write.
Follow the notification's status: scheduled waits for its send time, pending waits to send or is sending, and sent means Flint's email service accepted the message, not that the recipient opened it. A bounce reports failed and remains visible in delivery.events as bounced. Cancel only when supported_actions includes cancel; a pending notification may already be sending and refuse cancellation. A notification for a pending card waits until the card has value. Recipient email covers every status, resends, and cancellation.
Code rotation cancels unsent notifications for the previous code with status_reason: credential_rotated. This includes scheduled notifications and pending notifications whose sending has not started. Notifications already being sent or accepted by the email service are left as they are. To send the new code, include notification in the rotation request or resend afterward with a new notification request. Other notifications return status_reason: null.
3. Redeem the card#
Where the buyer spends the card decides the calls. In a Flint checkout, the order applies the card. At a register that isn't on Flint, you redeem it yourself.
In your checkout#
Hosted checkout does all of this for you. In your own checkout, your backend applies the code the buyer typed, with the session's checkout credential:
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/gift-cards \
-H "Authorization: Bearer ckat_1kmn0aExample" \
-H "Idempotency-Key: apply-gift-card-cart-123-1" \
-H "Content-Type: application/json" \
-d '{"code": "ABCD-EFGH-JKMN-PQRS", "expected_version": 1790760300}'
Applying debits nothing. The order's gift_card_estimate splits the balance into gift_card_money and processor_money. When can_pay is true, copy the estimate into accepted_gift_card_allocation and pay. Here the card covers $50.00 of a $64.00 order and the buyer's card pays the rest:
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/pay \
-H "Authorization: Bearer ckat_1kmn0aExample" \
-H "Idempotency-Key: pay-cart-123-attempt-1" \
-H "Content-Type: application/json" \
-d '{
"action": "pay",
"payment_source": {
"credential_type": "stripe_confirmation_token",
"stripe": {"confirmation_token": "ctoken_1kmn0aExample"}
},
"expected_outstanding_money": {"amount": 6400, "currency": "USD"},
"expected_version": 1790760301,
"accepted_gift_card_allocation": {
"order_version": 1790760301,
"gift_cards": [
{"gift_card_id": "gc_1kmn0aExample", "amount_money": {"amount": 5000, "currency": "USD"}}
],
"gift_card_money": {"amount": 5000, "currency": "USD"},
"processor_money": {"amount": 1400, "currency": "USD"}
}
}'
When the cards cover everything, send action: "pay" without payment_source. If the estimate changed before the payment started, read the order, show the buyer the new split, and pay again with a new key. Flint settles the card as part of the payment attempt, so you never capture or cancel an order's gift card redemption yourself. Pay with gift cards covers the estimate, and Apply gift cards covers the challenge Flint requires after repeated wrong codes.
At your own register#
Look up the code to get the card and what it can spend. Lookup reserves nothing:
curl -X POST https://api.withflintpay.com/v1/gift-cards/lookup \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "ABCD-EFGH-JKMN-PQRS"}'
The response is the card, with gift_card_id and available_money. A code that's unknown, unusable, or another merchant's returns the same error. When the sale has another tender, reserve the card's share first with capture_mode: "manual", then charge the rest:
curl -X POST https://api.withflintpay.com/v1/gift-card-redemptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pos-sale-1042-gift-card" \
-H "Content-Type: application/json" \
-d '{
"gift_card_id": "gc_1kmn0aExample",
"amount_money": {"amount": 2500, "currency": "USD"},
"capture_mode": "manual",
"sale_reference": "pos-sale-1042",
"expected_gift_card_version": 1
}'
expected_gift_card_version is optional and checks the card version returned by lookup. Capture and cancel instead take expected_version for the redemption.
The redemption is reserved for 15 minutes, or until expires_at if you set it, up to 24 hours. When the rest of the sale succeeds, capture it to spend the value:
curl -X POST https://api.withflintpay.com/v1/gift-card-redemptions/gcr_1kmn0aExample/capture \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pos-sale-1042-capture"
Capture returns the redemption with status: "succeeded". A reserved redemption lists capture and cancel in supported_actions when those commands are available; a finished redemption returns [].
If the sale fails, call /cancel on the redemption instead to release the value. When the card is the only tender, send capture_mode: "automatic" to spend it in one call, also with status succeeded. sale_reference is your sale ID, and GET /v1/gift-card-redemptions?sale_reference=pos-sale-1042 finds the redemption later.
If the card doesn't cover the amount, Flint returns 409 LIMIT_EXCEEDED and debits nothing. In details[0], reason is insufficient_available_value and maximum_money is the available value when Flint tried to reserve it, including any existing reservations. Redeem up to details[0].maximum_money and collect the rest another way. Availability can change before the next request, so handle another refusal if it does.
4. Refund a split-tender sale#
Refund each tender that paid with tender_allocations. For the $64.00 order from step 3, this returns $50.00 to the gift card and $14.00 to the buyer's card. The order's gift_card_settlements gives the gift_card_redemption_id:
curl -X POST https://api.withflintpay.com/v1/refunds \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: refund-ord-1kmn0a-full" \
-H "Content-Type: application/json" \
-d '{
"refund_type": "amount",
"order_id": "ord_1kmn0aExample",
"amount_money": {"amount": 6400, "currency": "USD"},
"tender_allocations": [
{
"tender_type": "gift_card_redemption",
"gift_card_redemption_id": "gcr_1kmn0bExample",
"amount_money": {"amount": 5000, "currency": "USD"}
},
{
"tender_type": "payment_intent",
"payment_intent_id": "pi_1kmn0aExample",
"amount_money": {"amount": 1400, "currency": "USD"}
}
],
"reason": "requested_by_customer"
}'
The allocations must add up to amount_money, and a tender you leave out gets nothing. Without tender_allocations, an order refund returns value to the gift cards first. The gift card share credits the original card as soon as the refund is created, while the card refund can still be pending, so read each outcome in tender_allocations. If the original card is closed or can't hold the value, set destination: "new_gift_card" on its allocation. Refund gift card and mixed payments covers both cases.
To refund a sale you redeemed at your own register, send the same gift_card_redemption allocation without order_id, as Refunds and funding losses shows. Don't use adjust-balance for a return: an adjustment isn't linked to the sale and counts toward funding limits.
5. Reconcile#
Every change to a card's value is an immutable transaction with the balance before and after. Read the month's postings from the merchant-wide feed:
curl "https://api.withflintpay.com/v1/gift-card-transactions?posted_after=2026-10-01T00:00:00Z&posted_before=2026-11-01T00:00:00Z" \
-H "Authorization: Bearer YOUR_API_KEY"
transaction_type says what each posting did, such as load, import, redeem, refund, or cash_out, and source names the load, redemption, or refund allocation behind it. Filter by transaction_type, gift_card_id, order_id, or source_id to match your own records.
To keep up as it happens, subscribe to gift_card.*, gift_card_load.*, gift_card_redemption.*, gift_card_notification.*, and gift_card_transaction.created. Events can arrive more than once and out of order, so deduplicate by event ID and compare version.
For the books, create a gift_card_liability_v1 report, which gives opening and closing liability for the period:
curl -X POST https://api.withflintpay.com/v1/reports \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: gift-card-liability-2026-10" \
-H "Content-Type: application/json" \
-d '{
"report_type": "gift_card_liability_v1",
"currency": "USD",
"interval_start_at": "2026-10-01T00:00:00Z",
"interval_end_at": "2026-11-01T00:00:00Z"
}'
Reports covers waiting for the file and the other report types, and Reconcile covers every filter and how a card sold at a discount is recorded. When a holder asks for the balance in cash, see cash-out eligibility.
Freeze, unfreeze, or close a card#
Read the card's supported_actions before acting. It lists the commands its state permits, including freeze, unfreeze, close, adjust_balance, and cash_out when available. The list is the same for every credential; your credential still needs the command's permission. These actions also appear in webhook snapshots.
Use POST /v1/gift-cards/{gift_card_id}/freeze to stop new spending and cash-outs while retaining the balance. Send reason as suspicious_activity, customer_request, or support_issue. Use /unfreeze to remove a merchant freeze; an unresolved funding dispute prevents it. Use /close to permanently close a card once its remaining value and reservations are resolved. All three accept an optional expected_version and Idempotency-Key, return the card in data, and take no action field.
