Gift Cards in your theme

What a theme builds and translates for Gift Cards: the recipient form, cart lines, the Gift Card page and view link, and My Gift Cards, with the checks, contract rules, strings and Canvas as the example.

Gift Cards are bearer cards: anyone with the code can redeem an active card at checkout when Gift Cards are enabled. Checkout does not check whether the shopper matches the recipient email or linked customer. Recipient details control delivery; they do not restrict who can redeem the card. Anyone with the code can use it. Keep the code and the View your gift card link safe. If a code is leaked, disable the card to pause redemption or cancel it to permanently revoke its remaining balance.

Gift Cards are sold as products and spent at checkout. The platform owns checkout, the Gift Card Delivery email and text message, balances, codes and every message about them. Your theme owns everything a shopper reads around them on the storefront: the form that sends a card to someone, the recipient on the cart line, the Gift Card page, the page a recipient opens from their email, and the Gift Cards list in the customer account. A theme that leaves a piece out leaves shoppers without it: a Gift Card product without the recipient form can only be bought for yourself, and a language your theme ships without the gift card strings shows those words in your default language. This guide lists every piece, the data and actions behind it, what the checks enforce and the strings to ship, with Canvas as the worked example.

What to build#

Piece

Where

Data and actions

In Canvas

Recipient form

The product page, and every other section with an Add to cart form that can show a Gift Card product (quick view, featured product)

product.is_gift_card; the recipient fields of cart_add and buy_now; their lengths in forms.cart_add and forms.buy_now, and which of email and phone the store can deliver by (available)

sections/main-product.vasc, sections/product-spotlight.vasc

Recipient on cart lines

Cart page and cart drawer

cart.items[].gift_card_recipient (name, email, phone_display, message, deliver_at_display)

sections/main-cart.vasc, sections/cart-drawer.vasc

Gift Card page

/gift-cards: check a balance

templates/gift_card.json; gift_card, forms.gift_card, form_error; the gift_card_lookup action

templates/gift_card.json, sections/main-gift-card.vasc

View link

/gift-cards/view/{token}, opened from the View your gift card link in the Gift Card Delivery email or text message

The same template, with gift_card.code and gift_card.message filled

sections/main-gift-card.vasc

My Gift Cards

The account page

customer.gift_cards

sections/main-account.vasc

Strings

Every storefront locale file

The t filter

locales/*.json

To read Canvas's files, install Canvas from the Theme Store on a development store and download it with tringify theme pull --store STORE_ID --theme THEME_ID (leave out --theme to list the store's themes). The download is compiled: Canvas's recipient form is one shared snippet in its source, and it appears in each section that uses it.

Required template and what the checks enforce#

The platform serves the Gift Card page on every store, so every theme needs a template for it, as it does for product and cart pages. Give the page one section of its own and list it in theme.json:

json
// templates/gift_card.json
{
  "page_type": "gift_card",
  "sections": [
    { "type": "main-gift-card", "position": 1, "settings": {} }
  ]
}

// theme.json, in "surfaces"
{ "surface_key": "gift_card", "required_sections": ["main-gift-card"], "allowed_sections": [] }

Check

Runs in

Refuses

Template floor

tringify theme check and tringify theme package in sealed mode (the default), tringify theme init, Theme Store publish and install

A theme without templates/gift_card.json, or with one that has no sections: THEME_IMPORT_INVALID_TEMPLATE with page_type gift_card. --mode development and a theme uploaded straight into a store skip this check, so you can build one page at a time.

Page sections

Every check of a package

A section on the page that the gift_card entry in theme.json neither requires nor allows, a required section that is missing or appears twice, or sections on the page without a gift_card entry.

Declared data (ctx-read)

Every check of a package

A section that reads gift_card, forms or form_error without declaring it in ctx_needs.

Words in markup (hardcoded-copy)

Every check of a package; a code save refuses only a finding it adds

Visible text written into markup, such as a literal "Check balance" button. Every label, hint and button goes through t.

Editor labels (setting-missing-label)

Every check of a package

A setting label of your Gift Card section that is missing from en.schema.json, de.schema.json, es.schema.json, fr.schema.json or it.schema.json.

Raw recipient phone (phone-display)

Every check of a package, as a warning

Nothing: it warns wherever a raw phone that has a phone_display beside it reaches the page, printed itself or through a variable or filter argument (assign to = r.email | default: r.phone).

Formatted times (time-display)

Every check of a package, as a warning

Nothing: it warns when a time that comes with a ready-to-show _display field (deliver_at, sent_at, scheduled_at) goes through the date filter.

Recipient fields kept

A code save on a store (the code editor, Store MCP save_theme_code, tringify theme dev, a zip uploaded into a theme, Theme Store updates)

An edit that reduces how many gift_card_send_as_gift, gift_card_recipient_name, gift_card_recipient_email, gift_card_recipient_message or gift_card_deliver_at fields the theme's sections hold.

The checks do not judge the rest of this guide: they do not look for a recipient form on your product page, or for your gift card strings in each locale file. A Theme Store review does: test every piece below on a development store with a Gift Card product before you publish (Publish in the Theme Store).

Recipient form#

When product.is_gift_card is true, put the recipient controls inside the product's {% form 'cart_add' %}, before the Add to cart and Buy now buttons, so both send them. A Buy now button that submits the same form with purchase_intent=buy_now keeps the fields. Bind each field's maxlength (and the message counter) to forms.cart_add.<field>.max_length, the same in forms.buy_now, and declare forms in the section's ctx_needs; never write the numbers yourself. Show the email field only while forms.cart_add.gift_card_recipient_email.available is true and the phone field only while forms.cart_add.gift_card_recipient_phone.available is true: each says whether the store can deliver a card that way now (an active Gift Card delivery email template and a ready sender for email; the Gift Card delivery message sending on SMS or WhatsApp for phone). When neither is true the store cannot send a card to anyone else: leave out the whole send-as-gift switch, so the card goes to the buyer. The platform works available out only when a template reads it, once per request. Pages that read it stay cached at the edge: changing the store's Gift Card delivery email template, its email sender or its Gift Card text message route refreshes them, so the value is always current. Leaving the switch off sends the card to the buyer's own checkout email or phone. Contract rule: gift_card.recipient.

Field

Contract

gift_card_send_as_gift

A checkbox with value true, labelled as sending the card to someone else. The recipient fields are read only when it is true. While it is off, disable the recipient fields (or the fieldset that holds them) so nothing else is posted.

gift_card_recipient_name

Optional. At most forms.cart_add.gift_card_recipient_name.max_length characters.

gift_card_recipient_email

Shown only while forms.cart_add.gift_card_recipient_email.available is true. While sending as a gift, the shown email or phone is required (one of them when both are shown). With both, the card is sent by email; say so in a hint, and name only the fields you show.

gift_card_recipient_phone

Shown only while forms.cart_add.gift_card_recipient_phone.available is true. One number in international format: +, the country code and the number, from any country. The platform stores it as E.164. Build the field like your other phone fields, with a country picker from phone_countries, but accept a number typed with + for a country outside that list, since a recipient may live anywhere.

gift_card_recipient_message

Optional. At most forms.cart_add.gift_card_recipient_message.max_length characters. Show a counter.

gift_card_deliver_at

Optional: when to send the card. Leave it empty to send right after payment. Otherwise RFC 3339 with the shopper's offset, such as 2026-12-24T09:00:00-05:00, in the future. A bare datetime-local value (2026-12-24T09:00) is read as UTC: convert it in the browser, or say in the hint that the time is UTC.

Instead of the separate fields, a script may post one gift_card_recipient field holding the JSON object {name, email, phone, message, deliver_at}; unknown keys are refused. tringify.cart.add and tringify.checkout.buyNow accept the same object. Field details: Cart actions.

The platform refuses a recipient it cannot accept (no email or phone, an invalid email or phone, a name or message that is too long, a send time that has passed) with GIFT_CARD_INVALID, and nothing is added. The response carries one message in the shopper's language: in user_errors (field gift_card_recipient) for a script, or in form_notice after a plain form post. Show it as given. Checking the fields in the browser first is a courtesy that saves a round trip; the platform's answer is the one that counts.

inside {% form 'cart_add' %} vascula
{%- if product.is_gift_card -%}
{%- assign by_email = forms.cart_add.gift_card_recipient_email.available -%}
{%- assign by_phone = forms.cart_add.gift_card_recipient_phone.available -%}
{%- if by_email or by_phone -%}
  <fieldset class="gift">
    <legend>{{ 'products.gift_recipient' | t }}</legend>
    <label>
      <input type="checkbox" role="switch" name="gift_card_send_as_gift" value="true" aria-controls="gift-{{ product.id }}">
      {{ 'products.gift_send' | t }}
    </label>
    <p>{{ 'products.gift_send_hint' | t }}</p>
    <fieldset id="gift-{{ product.id }}" disabled>
      <label>{{ 'products.gift_name' | t }}
        <input name="gift_card_recipient_name" maxlength="{{ forms.cart_add.gift_card_recipient_name.max_length }}" autocomplete="off"></label>
      {%- if by_email -%}
      <label>{{ 'products.gift_email' | t }}
        <input type="email" name="gift_card_recipient_email" maxlength="{{ forms.cart_add.gift_card_recipient_email.max_length }}" autocomplete="off" aria-describedby="gift-{{ product.id }}-reach"></label>
      {%- endif -%}
      {%- if by_phone -%}
      <label>{{ 'products.gift_phone' | t }}
        <input type="tel" name="gift_card_recipient_phone" maxlength="{{ forms.cart_add.gift_card_recipient_phone.max_length }}" autocomplete="off" aria-describedby="gift-{{ product.id }}-reach"></label>
      {%- endif -%}
      <p id="gift-{{ product.id }}-reach">
        {%- if by_email and by_phone -%}{{ 'products.gift_reach_hint' | t }}
        {%- elsif by_email -%}{{ 'products.gift_reach_email' | t }}
        {%- else -%}{{ 'products.gift_reach_phone' | t }}{%- endif -%}
      </p>
      <label>{{ 'products.gift_message' | t }}
        <textarea name="gift_card_recipient_message" maxlength="{{ forms.cart_add.gift_card_recipient_message.max_length }}" rows="3"></textarea></label>
      <label>{{ 'products.gift_date' | t }}
        <input type="datetime-local" name="gift_card_deliver_at" aria-describedby="gift-{{ product.id }}-when"></label>
      <p id="gift-{{ product.id }}-when">{{ 'products.gift_date_hint' | t }}</p>
    </fieldset>
  </fieldset>
{%- endif -%}
{%- endif -%}

A small script enables the inner fieldset while the switch is on. Without script the fieldset stays disabled and the card goes to the buyer, so either enable it in markup and rely on the platform's answer, or keep the switch and fields working without script as Canvas does. Before the form posts, a script that offers a local date picker writes the chosen moment into gift_card_deliver_at with the offset of that moment, so summer time is right:

javascript
function rfc3339(d) {
  const pad = (n) => String(n).padStart(2, "0");
  const off = -d.getTimezoneOffset(), a = Math.abs(off);
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00` +
    `${off < 0 ? "-" : "+"}${pad(Math.floor(a / 60))}:${pad(a % 60)}`;
}
// rfc3339(new Date(2026, 11, 24, 9, 0)) in New York gives "2026-12-24T09:00:00-05:00"

Canvas: the snippet sits in the product form of sections/main-product.vasc and sections/product-spotlight.vasc. It shows the email and phone fields by their available, words the hint and its "add a contact" message for the fields shown, and leaves out the whole switch when neither is available. Its switch is a checkbox with role="switch"; the phone field is Canvas's shared phone field; the message has a live counter; every maxlength and the counter's limit come from forms.cart_add; and with script the native date and time field becomes a picker with Now (after payment), Tomorrow morning and Pick a date, which writes gift_card_deliver_at as RFC 3339 with the shopper's offset and says when the card will send and in which time zone.

Recipient on cart lines#

A cart line for a card sent as a gift carries gift_card_recipient: name, email, phone (E.164, for machines), phone_display (the number in its country's international format), message, deliver_at (RFC 3339 in the store's timezone, for machines such as <time datetime>) and deliver_at_display: the same moment ready to show, in store time with the store's timezone named, in the shopper's language, such as "Fri, Oct 30, 9:00 AM (Hong Kong Time)" (the year appears when it is not this year). Both are null when the card sends right after payment. Show the whole recipient on the cart page and in the cart drawer, each part that is present: who the card is for, the email, phone_display, the message and when it sends. Print phone_display and deliver_at_display as given, never phone or a date you format yourself. A message can be long: clamp it to a couple of lines and keep the full text on the page (Canvas puts it in the element's title too). A line without a recipient (gift_card_recipient null) goes to the buyer.

cart line vascula
{%- assign r = line.gift_card_recipient -%}
{%- if r -%}
  {%- if r.name != blank -%}{%- assign to = r.name -%}{%- assign to_is = 'name' -%}
  {%- elsif r.email != blank -%}{%- assign to = r.email -%}{%- assign to_is = 'email' -%}
  {%- else -%}{%- assign to = r.phone_display -%}{%- assign to_is = 'phone' -%}{%- endif -%}
  <p>{{ 'cart.gift_for' | t: name: to }}</p>
  {%- if r.email != blank and to_is != 'email' -%}<p><bdi dir="ltr">{{ r.email }}</bdi></p>{%- endif -%}
  {%- if r.phone_display != blank and to_is != 'phone' -%}<p><bdi dir="ltr">{{ r.phone_display }}</bdi></p>{%- endif -%}
  {%- if r.message != blank -%}<p class="gift-message" dir="auto" title="{{ r.message }}">{{ 'cart.gift_message' | t: message: r.message }}</p>{%- endif -%}
  {%- if r.deliver_at_display -%}
    <p><time datetime="{{ r.deliver_at }}">{{ 'cart.gift_send_on' | t: date: r.deliver_at_display }}</time></p>
  {%- endif -%}
{%- endif -%}
  • Show phone_display, never phone (contract rule phone.display; themecheck phone-display warns, including when phone reaches the page through a variable such as assign to = r.email | default: r.phone).

  • Show deliver_at_display, never deliver_at through the date filter: the platform already wrote it in store time with the timezone named (contract rule time.display; themecheck time-display warns).

  • The recipient is part of the line: two recipients or two messages make two lines, and the quantity buys that many cards for the same recipient. Update and remove the line with its line_id. To change the recipient, remove the line and add the card again.

  • Express checkout buttons do not offer a cart or product with a Gift Card: express.reason is EXPRESS_REASON_GIFT_CARD. Show express.reason_label if you show a reason at all (Express checkout in themes).

Gift Card page#

/gift-cards checks a balance; /gift-cards/view/{token} opens one card from the View your gift card link in the Gift Card Delivery email or text message. Both work under the store's locale prefixes (such as /fr-ca/gift-cards), are marked noindex, are never cached, and return the store's 404 page when the merchant turns off the balance check in the Gift Card settings. Both render templates/gift_card.json. Declare gift_card, forms and form_error in the section's ctx_needs.

State

What the section gets

What to show

First visit to /gift-cards

gift_card is null.

The lookup form.

After a successful lookup

gift_card with code null and code_masked (such as •••• 4F7K).

code_masked, balance.formatted, initial_value.formatted, status_label, expires_at_label when not null, a link to shop_url, and the form again to check another card.

After a failed lookup

gift_card is null; form_error.message holds one message.

The message as given, and the form.

View link

gift_card with the full code and the sender's message (null when there is none).

The code (with a copy button if you like), the message, the balance and the rest as above.

View link that opens nothing

gift_card is null; form_error.message holds the same kind of message.

The message as given, and the form.

Checking a balance on the Gift Card page requires the code, not the recipient email or a matching customer account. The lookup posts the gift_card_lookup action with one field, code: trimmed, any letter case, dashes optional. Bind the input to forms.gift_card.code (required, max_length) rather than writing your own limits. The platform adds its own proof-of-work script to this page, so a plain form submits correctly. Lookups are rate limited per visitor and per store, and every refusal (unknown code, cancelled card, too many attempts) comes back as a message in form_error. Details: Gift Cards actions.

sections/main-gift-card vascula
{%- if form_error and form_error.message != blank -%}
  <p role="alert">{{ form_error.message }}</p>
{%- endif -%}
{%- if gift_card -%}
  <article aria-labelledby="gc-balance">
    {%- if gift_card.code != blank -%}
      <p>{{ 'gift_card.code' | t }} <bdi dir="ltr">{{ gift_card.code }}</bdi></p>
    {%- else -%}
      <p><bdi dir="ltr">{{ gift_card.code_masked }}</bdi></p>
    {%- endif -%}
    <p>{{ gift_card.status_label }}</p>
    <p><span id="gc-balance">{{ 'gift_card.balance' | t }}</span> {{ gift_card.balance.formatted }}</p>
    <p>{{ 'gift_card.initial_value' | t }} {{ gift_card.initial_value.formatted }}</p>
    {%- if gift_card.expires_at_label -%}<p>{{ gift_card.expires_at_label }}</p>{%- endif -%}
    {%- if gift_card.message != blank -%}<blockquote>{{ gift_card.message }}</blockquote>{%- endif -%}
    <a href="{{ gift_card.shop_url }}">{{ 'gift_card.shop' | t }}</a>
  </article>
{%- endif -%}
<h2>{{ 'gift_card.check_title' | t }}</h2>
{% form 'gift_card_lookup' %}
  <label>{{ 'gift_card.code' | t }}
    <input name="code" autocomplete="off" autocapitalize="characters" spellcheck="false"
      maxlength="{{ forms.gift_card.code.max_length }}"{% if forms.gift_card.code.required %} required{% endif %}></label>
  <button type="submit">{{ 'gift_card.check' | t }}</button>
{% endform %}
  • Print code only where it is not null, and code_masked otherwise. Never put a code in a URL, an analytics event, a cookie, storage or the console (contract rule gift_card.code).

  • Print form_error.message as given. Do not write your own not-found text, and do not try to tell a wrong code from any other refusal (contract rule gift_card.message).

  • status (active, used, expired, disabled, cancelled) is for styling only; print status_label. expires_at is RFC 3339 for scripts; print expires_at_label.

  • Theme Store demos show the page without a card: the form does not submit there and no balance is invented.

Canvas: sections/main-gift-card.vasc shows the card above the lookup form, a Copy code button that appears only when the browser can copy and the full code is on the page, and the code in <bdi dir="ltr"> so it reads correctly in right-to-left languages. All fields: Gift Card products and purchases and Shared data shapes.

My Gift Cards#

In the account template, the section that shows the customer's account declares customer.gift_cards in ctx_needs; the list is loaded only for a section that declares it. It holds items, page_size, next_url, first_url and active. Items come newest first, page_size (20) at a time. Print next_url and first_url as given (next_url is empty on the last page), and open your Gift Cards tab or panel when active is true, which means the shopper is paging through the list.

kind

What it is

Show

received

A card sent to an email or phone the customer has confirmed, or one they bought for their own email or phone.

code when not null, otherwise code_masked; balance.formatted, initial_value.formatted, status_label, expires_at_label.

sent

A card the customer bought for someone else.

recipient_name when not null, initial_value.formatted, delivery_status_label, and sent_at_display or scheduled_at_display as given (store time with the timezone named; sent_at and scheduled_at are RFC 3339 for machines). Code, balance and status are null: the recipient's card is theirs.

delivery_status is scheduled, sending, sent or not_sent. sent means the card was handed off for delivery; the store does not know whether it arrived, so never word it as delivered: print delivery_status_label. Print kind_label if you label the two kinds. With no cards, show an empty state from your locale files. Canvas: the Gift Cards tab of sections/main-account.vasc.

Theme contract rules#

Rule

In short

gift_card.recipient

Offer the recipient form on every Gift Card product form with its lengths from forms.cart_add, post it only while sending as a gift, send gift_card_deliver_at with the shopper's offset, and show the platform's message.

gift_card.message

On the Gift Card page, print form_error.message as given and never explain a refusal yourself.

gift_card.code

Print a code only where it is not null; never put it in a URL, analytics, a cookie or storage.

phone.display

Show the recipient's phone_display, never phone.

time.display

Print deliver_at_display, sent_at_display and scheduled_at_display as given; never format those times.

editing.merchant_editable

Every word comes from settings or locale keys through t, never from literals in markup.

Strings to ship#

Everything the platform does not word, your theme words, through t and keys in its locale files. Put every key in your default locale file and in every other storefront locale file you ship (locales/<language>.json). A key missing from one language is not an error: the shopper sees your default language's text there, so check the files yourself. Merchants can reword any of these keys in Store Admin without editing your theme. Canvas ships these keys in all ten of its languages:

Where

Canvas key

English text

Recipient form

products.gift_recipient

Recipient

products.gift_send

Send as a gift to someone

products.gift_send_hint

Leave this off to receive the card yourself.

products.gift_name

Recipient name

products.gift_email

Recipient email

products.gift_phone

Recipient phone

products.gift_reach_hint

Add an email or a phone number. With both, we send it by email.

products.gift_reach_email

We send the card to this email.

products.gift_reach_phone

We send the card to this phone number.

products.gift_message

Message (optional)

products.gift_need_contact

Add the recipient’s email or phone number.

products.gift_need_email

Add the recipient’s email.

products.gift_need_phone

Add the recipient’s phone number.

products.gift_phone_plus

Start the phone number with + and the country code.

Send time

products.gift_date

Send on (optional)

products.gift_date_hint

Leave empty to send right after payment. Enter the time in UTC.

products.gift_date_now

Now (after payment)

products.gift_date_now_hint

Sends right after payment.

products.gift_date_tomorrow

Tomorrow morning

products.gift_date_pick

Pick a date…

products.gift_date_sends

Sends {{ date }} at {{ time }}.

products.gift_date_zone

Your time zone: {{ zone }}.

products.gift_date_prev

Previous month

products.gift_date_next

Next month

products.gift_date_time

Time

products.gift_date_done

Done

products.gift_date_past

That time has passed. Choose a later time.

Cart lines

cart.gift_for

Gift for {{ name }}

cart.gift_message

Message: {{ message }}

cart.gift_send_on

Sends {{ date }}

Gift Card page

gift_card.title

Gift Card

gift_card.check_title

Check a balance

gift_card.intro

Enter your Gift Card code to see its balance and expiry date.

gift_card.code

Gift Card code

gift_card.check

Check balance

gift_card.balance

Balance

gift_card.initial_value

Original value

gift_card.expiry

Expiry

gift_card.copy

Copy code

gift_card.copied

Copied

gift_card.shop

Shop now

My Gift Cards

account.gift_cards

Gift Cards

account.no_gift_cards

You have no Gift Cards yet.

gift_card.recipient

Recipient

gift_card.sent_at

Sent

gift_card.scheduled

Scheduled for

The key names are yours; only the coverage matters. Do not add keys for what the platform words: status_label, expires_at_label, kind_label, delivery_status_label, form_error.message, user_errors and form_notice messages, and express.reason_label already arrive in the shopper's language. Gift Card send times arrive ready to show (deliver_at_display, sent_at_display, scheduled_at_display); print them as given. Other dates go through the date filter, which writes them in the shopper's language. The editor labels of your Gift Card section live in the *.schema.json files, which need en, de, es, fr and it (Theme Locale Files).

To list the keys a language is missing compared with your default locale (here en), using jq:

bash
jq -r 'keys[]' locales/en.json | sort > /tmp/default.keys
for f in locales/*.json; do
  case "$f" in *.schema.json|locales/en.json) continue ;; esac
  missing=$(jq -r 'keys[]' "$f" | sort | comm -23 /tmp/default.keys -)
  [ -n "$missing" ] && printf '%s is missing:\n%s\n' "$f" "$missing"
done

Accessibility#

  • Give every field a visible <label>. Group the recipient fields in a <fieldset> with a <legend>, and tie hints ("an email or a phone", "sends right after payment") to their fields with aria-describedby.

  • Make the send-as-gift control a real checkbox (Canvas adds role="switch") with aria-controls pointing at the fields it opens, and disable the hidden fields so keyboard and screen reader users do not land in them.

  • Keep the native datetime-local field as the fallback for a date picker. A custom picker is a radio group for its choices and a grid for the calendar: arrow keys move by day and week, Home and End to the ends of the week, Page Up and Page Down by month, Escape closes it and returns focus to the control that opened it. Name each day in full with aria-label, mark today with aria-current="date", disable past days, follow the page language for day and month names and the first day of the week (Intl), and swap Left and Right in right-to-left languages.

  • Announce the chosen send time and the shopper's time zone in a polite live region, and the message counter likewise.

  • Print refusals in an element with role="alert". When your own browser check stops the form, use setCustomValidity and reportValidity on the field with your locale text, so the browser reads it out.

  • Wrap codes, emails and phone numbers in <bdi dir="ltr"> so they read correctly in right-to-left languages.

Checklist#

  • templates/gift_card.json has your Gift Card section, and the gift_card entry in theme.json requires it. tringify theme check passes.

  • The section declares gift_card, forms and form_error, binds the code input to forms.gift_card.code, and posts gift_card_lookup with a plain form.

  • The page shows code only on the view link and code_masked otherwise, the sender's message, balance, initial_value, status_label, expires_at_label and a link to shop_url, and prints form_error.message as given.

  • Every Add to cart form that can show a Gift Card product has the recipient form inside it, with every maxlength from forms.cart_add (forms declared), the email and phone fields shown only while their available is true (and no send-as-gift switch when neither is), posted only while sending as a gift, and Buy now sends it too.

  • gift_card_deliver_at is RFC 3339 with the shopper's offset, or its hint says the time is UTC.

  • Cart page and cart drawer show the whole recipient: who each card is for, the email and phone_display (never phone), the message (clamped, full text kept) and when it sends (deliver_at_display, never a formatted deliver_at).

  • The account page declares customer.gift_cards, lists received and sent cards, pages with next_url and first_url, opens the list when active, and prints sent_at_display / scheduled_at_display, and never calls a sent card delivered.

  • No word is written into markup; every gift card key is in the default locale file and in every other storefront locale file you ship.

  • On a development store with a Gift Card product: buy a card for yourself and for someone else (email, phone, a send time), turn off the Gift Card delivery email template or the text message route and see the form drop that field (and the whole switch when both are off), check its balance, open the View your gift card link from the email, and find it under My Gift Cards, in each language you ship and at phone and desktop widths.

Related: Gift Card products and purchases for the data, Cart actions and Gift Cards actions for the forms, and Theme contract for every rule.

Merchant apps edit Gift Card recipients, messages, language and delivery schedules only when the card response allows it with can_edit_delivery. Sent or in-flight cards and cards with any redemption or debit lock those details; expired, depleted, cancelled and disabled cards also refuse Resend. Resend uses the saved recipient and language. Saving a future delivery schedule creates or moves its waiting send; clearing it cancels that send and sends nothing immediately. Use the Store API card capabilities and reason wording directly in merchant integrations; these permissions do not change the shopper-facing theme contract.

Buying a gift card creates a liability, not sales revenue: gift card lines are excluded from gross, net and total sales, product sales, frequently bought together and repeat purchases. They are never taxed or discounted. Spending a gift card pays for goods, which count as sales once on the order; the payment method is gift_card. Store credit is reported as store_credit. Completed refunds to either balance reduce sales once, on the day they complete; pending or failed refunds do not. Gift cards can't be bought with a gift card or store credit. In mixed carts, gift cards apply first to the non-gift-card payable amount. Store credit then applies to that eligible amount remaining after gift cards, subject to the customer's balance and the store's per-order limits. Neither field appears for a gift-card-only cart. A direct checkout attempt is refused with GIFT_CARD_PURCHASE_TENDER_FORBIDDEN: “Gift cards can’t be bought with a gift card or store credit”. The checkout editor's SAMPLEGIFT and sample store credit follow the same rule and never spend real balances. Gift-card-only orders count as orders but are excluded from average order value. Average order value = total sales ÷ recognized orders that have sales lines. Free merchandise and fully refunded merchandise orders still have sales lines. A mixed order contributes only its sales amount. A sale notification shows that same amount; a gift-card-only order says Gift card sold and shows the gift card purchase amount. Checkout started, payment chosen and purchase event values and items exclude gift card lines.

Order and payment webhooks remain complete transaction records: their order totals include gift card purchases and their payment records include stored-value tender. They are not sales totals. Sales analytics and advertising events exclude gift card lines. Gift card movement webhooks describe liability changes, never new merchandise revenue. Timestamps are RFC 3339 in the store timezone.

Merchant balance adjustments appear in the existing balance values on the Gift Card page and customer account. Always render the current server-provided balance and status; adjustment reasons are merchant-only and must not be shown to shoppers. Cards issued by CSV follow the same Gift Card display and delivery contract as cards issued by hand. CSV exports contain only the last four code characters. See the Store API Gift Cards reference and Bulk changes with CSV imports for merchant operations.