A gift card is one sf_gift_card record. It has:
- a serial (
GC-XXXX-XXXX-XXXX), which staff use; - a code (
XXXX-XXXX-XXXX-XXXX, no 0, O, 1 or I), which the holder types; - for a physical card only, a PIN.
Every movement of value is a line in the card's uses[]: redeem, refund, adjust, transfer, reload. A card's status is inactive (issued, not live yet), active, suspended, used (balance 0), expired or cancelled. Only active can be spent. (appengine storefront/services/giftcard.service.ts.)
Whoever has the code can spend the card, signed in or not. Treat a code like cash.
Ask first
- Sell them, or give one out? Selling is a shop product (step 1). Giving one out, or selling one at the counter, is issuing by hand (step 3).
- Amounts. Fixed amounts, or let the buyer choose between a smallest and a largest.
- Expiry. Never, or after a number of days. Purchased cards are held to a minimum: 5 years (1,825 days) unless the business lowers it (step 7).
- Who gets it, and how? Email, text, or both. Now, or on a date.
- Was it paid for? For a card issued by hand: sold (cash, card terminal, cheque, other) or given away. This decides the books.
Steps
1. Products: a gift card the shop sells
Any product becomes a gift card with Sell this product as a gift card (Products, open the product, Gift card section; sf_product.giftCard, websitemint product-form.tsx 1691–1760):
| Field | On screen | What it does |
|---|---|---|
enabled | Sell this product as a gift card | Turns it on. |
denominations | Amounts to choose from | Buttons on the product page, e.g. [25, 50, 100]. |
allowCustomAmount, minAmount, maxAmount | Let the shopper type their own amount; Smallest / Largest amount | A typed amount between the two. |
collectRecipient | Ask who it is for: name, email and a message | On unless false. The buyer can also tick Send it to me. |
allowScheduledDelivery | Let the shopper choose the day it is sent | On unless false. The card is sent at 9am, buyer's time, that day. |
expiresAfterDays | Expires after (days) | Blank: never. Raised to the business's minimum (step 7). |
The product page adds the buyer's choices to the cart line as options[] with fixed names: Gift card amount, Recipient name, Recipient email, Recipient phone, Gift message, Deliver on (base-app GiftCardOptions.tsx 16–25). The server prices the line from Gift card amount. Tested: a $25 product with Gift card amount 40 quoted $40.
When the order is paid, one card per unit is issued and made active (issueForOrder, giftcard.service.ts 2372). Payment can come from checkout, or from staff recording it later. Each card is then sent at once, or on the chosen day. Tested: a staff order for the gift card product, paid in cash, issued a $40 card to the recipient and emailed it to them. A card for someone else belongs to the recipient. The buyer sees it in their account with only its last four.
A gift card can't pay for a gift card. With one in the cart, a gift card applied at checkout covers nothing for it: "Gift cards in your cart are paid another way — a gift card cannot buy a gift card." (tested).
Hands on: the product's SKU. Check: the product page shows the amounts and the "Who is it for?" fields.
2. Site settings: the card's own page
The email links to the card's page: <site>/gift-card?code=<code>, or the page chosen for the Gift Cards feature. That page shows what is left, the message, a copy button and wallet buttons. It also checks a typed code. It sells nothing.
The link is a 404 until Gift Cards is on. Tested: on a site whose features are only the store, the link in a real gift card email answered 404. Ask whether to switch on Site Config › Site Features › Gift Cards; see Site page routing.
Stop: turning a feature on changes the live site. Ask first, and never change a site's features without a yes.
Signed-in holders also see their cards at My Account › Gift cards (/account/gift-cards). There they can top a card up with the gift card product, or send part of a card to someone. See Built-in pages and templates for the gift-cards section, which Studio's list leaves out.
Check: curl -s -o /dev/null -w "%{http_code}" "<site>/gift-card?code=<a test code>" answers 200.
3. Gift cards: issue a card by hand
Stop: a live card is value someone can spend. Recording it as a sale books money taken. Show the amount, the holder, sold or given away, and wait for yes.
On screen: Gift Cards (/gift-card), Issue cards, mode One card. A batch issues several identical cards (POST storefront/giftcards/batch, more than 50 in the background). From a list issues one card per row.
Real action: call_appengine POST storefront/giftcards (staff only):
| Field | What it is |
|---|---|
initialAmount | The value. Must be more than 0. |
currency | Default USD. |
type | digital (default), physical (gets a PIN) or promotional. Only promotional cards are free of the expiry minimum. |
name | Shown on the card. |
recipientName, recipientEmail, recipientPhone, customerId | The holder. An email or phone that matches a customer links the card to them. |
recipientMessage | A note shown on the card and in the email. |
expirationDate | Optional. A purchased card can't expire sooner than the minimum: "A purchased gift card cannot expire before … under this business's policy (1825 days)". |
restrictions | { minPurchase?, maxUsePerTransaction?, productSkus?, categoryIds?, customerGroups?, excludeDiscountedItems? } |
code, pin | Optional. Your own code (8 or more letters or digits); a physical card's PIN (4–8 digits). Otherwise generated. |
activate | true: live now. Otherwise inactive until Activate. |
record | With activate: { as: 'sale', tender: 'cash' | 'card' | 'check' | 'other', reference? } books the money taken; { as: 'giveaway', reason? } books it as a promotional cost. Nothing given means giveaway. |
The answer carries the serial and code. A physical card's pin is in this answer once and never again (tested: the card record keeps only a hash). Copy it to the card now.
Issuing sends nothing. Studio's Issue drawer then sends the card itself (POST …/<serial>/send) when it is live, has an email or phone, and has no Send on date (websitemint issue-drawer.tsx 209).
Hands on: the serial and code.
Check: open the card (/gift-card?openId=<serial>). It shows the balance, status and holder.
4. Gift cards: send it
Stop: this emails and/or texts the holder the code. Show where it goes and wait for yes.
On screen: the card, Send card (or Send again).
Real action: call_appengine POST storefront/giftcards/<serial>/send with {}, or { email?, phone? } to send elsewhere. The new address replaces the one on the card.
It sends the email template gift-card-issued (or the card's own message template) to the email, and gift-card-issued-sms to the phone (deliver, giftcard.service.ts 2490). The subject is "delivered: false, "no email or phone on the card". Every send is listed on the card (deliveries).
Check: the card's timeline lists the send. The message is in the org's messages, addressed to the holder.
5. Checkout: balance and spending
Balance. Signed-out visitors and the site use GET client/giftcards/balance?code=<code>&pin=<pin>. It is limited to 30 checks per address every 10 minutes: "Too many gift card checks. Try again in a few minutes." Staff use GET storefront/giftcards/balance. The code is matched without dashes, spaces or case (tested). Answers (all tested except used):
status | valid | Meaning |
|---|---|---|
active | true | balance, expirationDate, the card's face. |
invalid_pin | false | A physical card, with no PIN or the wrong one. |
inactive, suspended, cancelled, expired | false | "Gift card is inactive" and so on. A live card past its date is marked expired here. |
used | false | Fully spent; the history is shown. |
not_found | false | "Gift card not found" |
At checkout. The Gift card box sends the codes with the quote: POST storefront/discounts/calculate with giftCards: [{ code, pin? }]. The quote takes nothing off. It answers each card's applied and balanceAfter, then giftCardTotal and amountDue (tested: a $30 card on a $40 cart applied $30, $10 due).
- Cards are used in the order given.
- Each card is capped by its balance, by its
maxUsePerTransaction, and by what is still due. - A card whose
minPurchaseor other restriction isn't met applies nothing and says why. - A card with a PIN asks for it: "This card has a PIN — enter it to use the card".
- Wrong PINs are counted, in the balance check, the checkout box and a spend alike: after 5 on one card, or 20 from one address, within 15 minutes, every try answers 429 "Too many wrong gift card PINs. Try again in 15 minutes." (
giftcard-client.service.ts). A customer who hits it waits, or staff check the card in Studio.
When the order is placed (POST storefront/checkout-cart with giftCards), each card is spent for real under the order's number. A retry spends once. Tested:
- Partial use: a $30 card paid a $20 order, and $10 stayed on it.
- Full cover: the order was
paidwith agiftcardpayment whose ref isGCT-…, and no card payment. The button reads "Place order — paid with gift card". - A card that fails at that moment (spent elsewhere, suspended) is noted on the order's remarks. The order is then
paid-partial, not refused.
Each spend emails the holder gift-card-redeemed ("Gift card payment: $…"), if the card has an email or phone.
Refunds on an order paid by gift card go back onto the card. Cancelling the order does not. See Refund, cancel or return an order.
Check: the card's timeline shows the spend with the order number.
6. Gift cards: look after a card
All staff-only, on the card in Gift Cards, under storefront/giftcards/<serial>/…. Each is recorded on the card's timeline (GET …/<serial>/timeline).
| Need | Action and body | Email to the holder |
|---|---|---|
| Lost or stolen | Suspend: PUT …/suspend { reason }. Reversible; balance kept. Tested: balance check then says "Gift card is suspended". | gift-card-blocked |
| Found again | Reactivate: PUT …/reactivate {} | gift-card-reactivated |
| A correction | Adjust: POST …/adjust { amount: ±, reason }. Can't go below 0. | none |
| Value back for a sale | Refund onto card: POST …/refund { amount, orderNumber, reason? }. Booked as store credit given for that sale. | gift-card-credited |
| Move value | Transfer: POST storefront/giftcards/transfer { fromSerial, toSerial, amount }. Both active, same currency; an over-ask is refused. | none |
| Code leaked, card damaged | Reissue: POST …/reissue { reason }. A new card to the same holder, with the balance; the old one is voided. | the new card is sent at once (gift-card-issued) |
| More time | Change expiry: PUT …/expiry { expirationDate | null, reason? }. Must be in the future and inside the minimum. Tested both. | none |
| Never spent, not wanted | Cancel card: PUT …/cancel { reason, refund? } | gift-card-cancelled |
| A different holder | Set holder: POST …/owner { customerId? | name, email, phone } | none |
Stop before cancel, adjust, refund onto card, transfer and reissue (it emails the new code): they change what someone can spend. Show the card, its balance and the change.
Cancel works only on a card that was never spent: "Cannot cancel a gift card that has been used. Use suspend instead." (tested). Without refund, what was on it is released as breakage income. With refund: true and a card bought on an order, the order's payments are refunded up to the card's balance. The buyer gets gift-card-purchase-refunded.
Cancel › Refund the purchase skips the order's checks. Tested on a card bought for $40 in cash: the $40 was recorded as cash handed back with nobody confirming it. The order still read paid with nothing refunded, and the refund row named no order. Card payments go back through the gateway the same way, so a PayPal or Helcim purchase is not refunded. Prefer: refund the order (Refund, cancel or return an order), then cancel the card without refund.
7. Gift cards: expiry
The business's policy (GET / POST storefront/giftcards/policy, on Overview):
defaultExpiresAfterDays: unset means purchased cards never expire.minimumExpiryDays: default 1825. A purchased or staff-issued card can't expire sooner. Onlypromotionalcards are free of it.
A product's Expires after (days) shorter than the minimum is silently raised to the minimum (applyExpiryPolicy, giftcard.service.ts 2662). A product set to 365 days issues 5-year cards, while its page still says "valid for 365 days". Lower the minimum first if a shorter expiry is wanted, and check your local law on gift card expiry.
The daily gift-card-expiry job (03:20) does two things:
- It emails
gift-card-expiringonce, 30 days before a card that still has money on it expires. - It then marks every card past its date
expired, and books what was left as breakage.
Run the expiry sweep now on Overview does the same at once (POST storefront/giftcards/expire-due).
Emails a card sends
All go to the holder: the recipient's email, else the buyer's. Each has an -sms twin for the phone. Templates live in tools/templates/default-email-templates.ts 1075–1290; an org's own template of the same name wins.
| Template | When |
|---|---|
gift-card-issued | Sent (step 4), and when the shop issues a paid card |
gift-card-delivered | To the buyer, when a card they scheduled went out |
gift-card-redeemed | Every spend |
gift-card-credited | Value put back (an order refund onto the card, Refund onto card) |
gift-card-reloaded | Topped up from an order |
gift-card-blocked, gift-card-reactivated, gift-card-cancelled | Suspend, reactivate, cancel |
gift-card-purchase-refunded | To the buyer, on cancel with refund |
gift-card-expiring | 30 days before expiry |
When it's done
Tell the person:
- the card's serial, value, status and expiry (or that it never expires);
- who holds it, and whether it was sent and where;
- for a sold card, how it was recorded (sale and tender, or given away);
- for a physical card, that the PIN was shown once and must be on the card.
Open the card in Gift Cards.
What it can't do yet
- Card emails link to a 404 while the Gift Cards feature is off (step 2).
- A staff "Send on" date never sends. Issuing by hand saves
deliverAtbut schedules nothing, and Studio skips its own send when a date is set (issue-drawer.tsx202–209). Only cards bought in the shop are sent on their date. Send by hand on the day. - A product's card type is ignored. Shop-issued cards are always
digital(giftcard.service.ts2434). - Product expiry is raised to the minimum without a word (step 7).
- No reload in Studio's Gift Cards app, though its menu says "reload". Holders top up from My Account; the till reloads at the POS.
- Studio's "Check a code" box has no PIN field and shows nothing for a good card. It opens the card.
- "From a list" ignores Spending rules. Restrictions aren't sent with an import.
- Cancelling an order doesn't give its gift card value back. Refund the order.
Don'ts
- Don't read a code or PIN out in a channel others can see. The code is the money.
- Don't send or resend a card without a yes, and check the address first.
- Don't use Cancel › Refund the purchase for a card bought by PayPal, Helcim or cash; refund the order.
- Don't promise a product's shorter expiry until the policy minimum allows it.
- Don't switch on the Gift Cards feature without asking.