Recipe: store credit
Goal: show a signed-in shopper their store-credit balance and let them spend it at checkout.
Audience: storefront (server-side, via the storefront SDK).
Store credit is a per-shopper ledger held by the cash module — money
attributed to an account, distinct from bearer-code
gift cards. It is backed by the platform’s double-entry
ledger, so a spend that would overdraw is rejected before any row is written.
Prerequisites
- The store’s manifest gate
storeCredit.enabledmust be on. It ships disabled (a new money surface goes out dark), distinct from thegiftCards.enabledgate. - Store credit applies to authenticated shoppers only — a balance is read for a signed-in session, not a guest. See Accounts & orders.
Step 1 — read the balance (honestly)
// Server-side, with the shopper's session established.
const balance = await store.storeCredit.balance();Render the store-credit affordance only when the gate is on and you read a positive, known balance. Critically, distinguish a real zero from a failed read — a failed balance read is unknown, never “$0 credit”:
try {
const balance = await store.storeCredit.balance();
if (balance.amount_cents > 0) {
return <StoreCreditOption balance={balance} />; // offer it
}
return null; // genuinely no credit
} catch {
return null; // unknown — do NOT show "$0 credit"
}This mirrors the honest-absence rule the platform holds everywhere: unknown and absent are different types (see Money invariants).
Step 2 — apply it at checkout
There is no separate “spend credit” call — it is a checkout tender. Pass
apply_store_credit: true to store.checkout.submit:
// Server-side.
const result = await store.checkout.submit(
{
customer_ref: shopperRef, // resolved from the session, never the body
items: [{ variant_id: "var_123", quantity: 1 }],
apply_store_credit: true,
card: { ccnumber: "4111111111111111", ccexp: "12/28", cvv: "123" },
},
{ idempotencyKey: `cart-${cartId}:attempt-1` },
);The platform re-checks the real balance server-side and applies
min(balance, orderTotal) as a partial tender — the amount charged to the card
is reduced accordingly, and it never over-applies from a stale client number. If
credit covers the whole total, no card is charged. The cash ledger is debited
via cash’s deterministic-idempotency spend, so a retried checkout never
double-spends.
The placed-order response reports the split:
{
"outcome": "placed",
"order": { "id": "ord_…", "total_cents": 4900 },
"store_credit_applied_cents": 4900,
"card_charged_cents": 0,
// …
}Multi-tender orders
An order can split across store credit, a gift card, and a payment card, in that
priority. Each tender books independently against the ledger — commerce
recognizes the full GMV while cash and giftcards each book their own
liability draw-down, as separate balanced transactions. You do not orchestrate
this; you pass the tenders and read the split back. See
Checkout for the full outcome shape.
Where credit comes from
This recipe covers spending credit. Store credit is granted through the
back-office — for example, a merchant issuing credit, or a
return/refund returning value as store credit
rather than to a card. Grants and spends both post through cash’s
deterministic-idempotency discipline, and a debit that would overdraw is
rejected up front, never caught and reversed after the fact.
Related
- Accounts & orders — shopper sessions and the balance read.
- Gift cards — the bearer-instrument alternative.
- Money invariants — fail-closed spends and honest absence.