Skip to content

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.

StepCallsSection
Give a card valueSell a catalog gift card on an order, or issue one directly1. Sell or issue a card
Send the codeRecipient email2. Deliver the code
Spend itApply it to an order, or redeem it from your register3. Redeem the card
Take a returnRefund each tender that paid4. Refund a split-tender sale
Close the booksTransaction feed, webhooks, liability report5. Reconcile

Before you start#

You need a sandbox API key with the scopes for the steps you use:

  • commerce.products.write and commerce.orders.write to sell cards from your catalog and to apply cards to orders.
  • commerce.gift_cards.write to issue cards and commerce.gift_cards.secrets.write to email codes.
  • commerce.gift_cards.redemptions.write to redeem from your own register, and commerce.gift_cards.read to read cards and the transaction feed.
  • commerce.refunds.write for refunds and reports.write for 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
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
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
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"
      }
    }
  }'
Response
{
  "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
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
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
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
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
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
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
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
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
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.

API calls#

  • POST/v1/productsRequired API key scope: commerce.products.writeReference for POST /v1/products

    Create the gift card product your buyers choose from.

  • POST/v1/gift-cardsRequired API key scope: commerce.gift_cards.writeReference for POST /v1/gift-cards

    Issue or import a card, and get its code once.

  • POST/v1/gift-card-notificationsRequired API key scope: commerce.gift_cards.secrets.writeReference for POST /v1/gift-card-notifications

    Email a card's code to its recipient.

  • POST/v1/orders/{order_id}/gift-cardsRequired API key scope: commerce.orders.writeReference for POST /v1/orders/{order_id}/gift-cards

    Apply a code to an order and get the payment split.

  • POST/v1/gift-cards/lookupRequired API key scopes: commerce.gift_cards.adjustments.write, commerce.gift_cards.read, commerce.gift_cards.redemptions.write, commerce.gift_cards.secrets.write or commerce.gift_cards.writeReference for POST /v1/gift-cards/lookup

    Find a card by its code without reserving anything.

  • POST/v1/gift-card-redemptionsRequired API key scope: commerce.gift_cards.redemptions.writeReference for POST /v1/gift-card-redemptions

    Spend or reserve value for a sale outside a Flint order.

  • POST/v1/gift-card-redemptions/{gift_card_redemption_id}/captureRequired API key scope: commerce.gift_cards.redemptions.writeReference for POST /v1/gift-card-redemptions/{gift_card_redemption_id}/capture

    Spend a reserved redemption.

  • POST/v1/gift-cards/{gift_card_id}/freezeRequired API key scope: commerce.gift_cards.writeReference for POST /v1/gift-cards/{gift_card_id}/freeze

    Stop new spending while retaining the card's value.

  • POST/v1/gift-cards/{gift_card_id}/unfreezeRequired API key scope: commerce.gift_cards.writeReference for POST /v1/gift-cards/{gift_card_id}/unfreeze

    Remove a merchant freeze when no other restriction remains.

  • POST/v1/gift-cards/{gift_card_id}/closeRequired API key scope: commerce.gift_cards.writeReference for POST /v1/gift-cards/{gift_card_id}/close

    Permanently close a card after resolving its value and reservations.

  • POST/v1/refundsRequired API key scope: commerce.refunds.writeReference for POST /v1/refunds

    Refund each tender that paid.

  • GET/v1/gift-card-transactionsRequired API key scopes: commerce.gift_cards.adjustments.write, commerce.gift_cards.read, commerce.gift_cards.redemptions.write, commerce.gift_cards.secrets.write or commerce.gift_cards.writeReference for GET /v1/gift-card-transactions

    Read every posting for reconciliation.

Next steps#

Gift cards API referenceLimits, statuses, codes, disputes, cash-outs, and every field.Apply gift cards in your own checkoutThe checkout credential, the challenge, and retries.RefundsPartial refunds, store credit, and gift card purchase refunds.

Was this helpful?