Gift Cards
Sell, redeem, and customize gift cards in your Nimbu webshop
Nimbu can sell gift cards as products, deliver them by email with a PDF voucher, and let customers redeem the codes against their cart. Once your shop owner enables gift cards in the admin (Settings → Webshop → Enable Gift Cards), every theme surface needed to integrate them is exposed through Liquid.
This page covers everything a theme developer needs:
- Selling gift card products
- Applying and removing codes on the cart
- Showing balances and applied adjustments on cart, checkout and order pages
- Linking customers to their gift card PDF
- Customizing the guest retrieval page, the PDF voucher and the delivery email
Selling Gift Cards
A gift card is a regular product flagged as a gift card in the admin. The flag controls two things:
- After payment, Nimbu issues real
GiftCardrecords — one per gift card line item — and emails them to the buyer. - Gift card products are filtered out of
products.all,products.featured, andcollection.productsby default. They will not appear in your normal listings unless you explicitly include them.
A dedicated "Buy a gift card" page
Use {% scope %} with gift_card == true to query gift card products:
<section class="gift-cards-shop">
<h1>{% translate 'gift_cards.shop_title', default: 'Buy a gift card' %}</h1>
{% scope gift_card == true %}
<div class="product-grid">
{% for product in products.all %}
{% include 'product-card', product: product %}
{% endfor %}
</div>
{% endscope %}
</section>Variant prices on a gift card product become the available denominations. Add to cart works exactly like any other product:
{{ product | add_to_cart, btn_class: 'btn btn-primary' }}Applying a Gift Card Code
Gift card codes share the unified apply endpoint with promotion codes — POST /cart/coupons. Use the Liquid {% form 'coupon' %} tag so the action URL, locale prefix, CSRF token and method are wired correctly on every site, including multilingual ones. Inside the form, {% input %} knows the model and emits name="coupon[code]" and id="coupon_code" for you. To localize labels and placeholders, assign each translation to a variable first with {% translate …, assign: … %}, then pass the variable into the input:
{% translate 'gift_cards.code_label', default: 'Gift card code', assign: code_label %}
{% translate 'gift_cards.apply', default: 'Apply', assign: apply_label %}
{% form 'coupon' %}
{% input 'code', label: code_label, required: true, placeholder: 'ABCD-EFGH-IJKL-MNOP' %}
{% submit_tag apply_label, class: 'btn btn-primary' %}
{% endform %}{% input 'code' %} wraps the field in a simple_form-style <div class="control-group string"> with an auto-rendered <label> and validation-error class — adjust with wrapper_class:, label_class:, etc., or restyle the defaults from your CSS. If you want a bare input without the wrapper, {% text_field_tag 'coupon[code]', required: true %} is the lower-level alternative.
Internally Nimbu first tries the submitted value as a coupon, and falls back to applying it as a gift card if no coupon matches. The endpoint redirects (303 See Other) back to the referrer — typically the cart page — with a flash message. Codes are case-insensitive and tolerant of spaces and dashes (abcd efgh ijkl mnop and ABCD-EFGH-IJKL-MNOP are equivalent).
Flash messages
The apply and remove endpoints set one of the following flash keys, all under the gift_cards namespace:
| Key | Meaning |
|---|---|
applied | Code accepted and applied to the cart |
removed | Adjustment removed from the cart |
invalid | Code does not match any gift card |
disabled | Gift card has been disabled by the shop owner |
expired | Gift card has expired |
empty | No remaining balance |
unavailable | Balance is currently held by another reservation — try again later |
failed | Generic apply failure |
not_found | Adjustment not found when removing |
purchase_blocked | Cart contains only gift card products |
try_again | Reservation system is temporarily busy |
Render them with your existing flash partials. Localized strings live under flash.gift_cards.* in your locale files.
Removing an Applied Gift Card
Each applied code becomes an entry in cart.gift_card_adjustments. Use the adjustment's remove_url to render a small delete form:
{% if cart.gift_card_adjustments? %}
<ul class="applied-gift-cards">
{% for adjustment in cart.gift_card_adjustments %}
<li>
<span class="code">{{ adjustment.code }}</span>
<span class="amount">−{{ adjustment.amount | money_with_currency }}</span>
<form action="{{ adjustment.remove_url }}" method="post" class="remove-gift-card">
<input type="hidden" name="_method" value="delete">
<input type="hidden" name="authenticity_token" value="{{ auth_token }}">
<button type="submit">
{% translate 'gift_cards.remove', default: 'Remove' %}
</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}adjustment.code is the masked code (e.g. ABCD-••••-••••-MNOP) so it is safe to render at the cart line. adjustment.remove_url already contains the active locale prefix, so this snippet works on multilingual sites without extra plumbing.
Cart and Checkout Totals
The cart (and any order) drop exposes gift-card-specific helpers:
| Property | Description |
|---|---|
gift_card_adjustments | Array of currently applied codes (see Applied gift card properties) |
gift_card_adjustments? | true if at least one is applied |
gift_card_total | Sum of the applied gift card discounts |
gift_cards | Array of gift cards issued by this order, available after payment |
gift_cards? | true if the order has issued gift cards |
any_giftcards? | true if any line item is a gift card product |
only_giftcards? | true if every line item is a gift card product |
no_giftcards? | true if no line item is a gift card product |
cart.grand_total already reflects any applied gift card discount — you do not need to subtract gift_card_total yourself. Add a row to the cart totals block to make the discount visible:
<dl class="cart-totals">
<dt>{% translate 'cart.subtotal', default: 'Subtotal' %}</dt>
<dd>{{ cart.subtotal_price | money_with_currency }}</dd>
{% if cart.gift_card_adjustments? %}
{% for adjustment in cart.gift_card_adjustments %}
<dt>
{% translate 'gift_cards.applied', default: 'Gift card' %}
<small>{{ adjustment.code }}</small>
</dt>
<dd>−{{ adjustment.amount | money_with_currency }}</dd>
{% endfor %}
{% endif %}
<dt class="total">{% translate 'cart.total', default: 'Total' %}</dt>
<dd class="total">{{ cart.grand_total | money_with_currency }}</dd>
</dl>Applied gift card properties
Each cart.gift_card_adjustments entry exposes:
| Property | Description |
|---|---|
id | Adjustment id (used in remove_url) |
amount | Discount amount applied to this cart |
code / masked_code | Masked code (safe to render) |
state | Internal state |
eligible? | Whether the adjustment currently reduces the order total |
gift_card | The underlying gift card. On adjustments this drop always returns masked_code from .code, so it is safe in cached or shared contexts. |
remove_url / undo_url | Locale-prefixed URL to DELETE to remove this adjustment |
Showing Issued Gift Cards on the Order Confirmation
After a successful payment, the buyer's order includes the freshly issued gift cards in order.gift_cards. Each entry is a gift card drop with balance, masked code, expiry and links to the tokenized retrieval page and the account PDF download. The full code lives behind gift_card.url — link the buyer through rather than trying to render it inline:
{% if order.gift_cards? %}
<section class="issued-gift-cards">
<h2>{% translate 'gift_cards.issued_heading', default: 'Your gift cards' %}</h2>
{% for gift_card in order.gift_cards %}
<article class="gift-card">
<p class="amount">
<strong>{{ gift_card.initial_balance | money_with_currency }}</strong>
</p>
<p class="masked-preview">
{% translate 'gift_cards.code_preview', default: 'Code ending in' %}
<code>{{ gift_card.code }}</code>
</p>
<p>
{% translate 'gift_cards.expires', default: 'Expires' %}:
{{ gift_card.expires_on | localized_date: 'long' }}
</p>
<p>
<a href="{{ gift_card.url }}">
{% translate 'gift_cards.open', default: 'Open gift card' %}
</a>
</p>
</article>
{% endfor %}
</section>
{% endif %}gift_card.code here is the masked form (e.g. ****-****-****-MNOP) — useful as a recognizable preview but not redeemable on its own. Send the buyer through gift_card.url for the full code reveal.
Gift card properties
| Property | Description |
|---|---|
id | Gift card id |
code | Masked code (e.g. ****-****-****-MNOP) on cart, order and account surfaces. Returns the full code only when used inside the gift card retrieval page, the PDF voucher, or the delivery email — those templates receive a recipient-only drop variant. |
masked_code | Always the masked form. Safe to render anywhere. |
currency | ISO currency code |
initial_amount / initial_balance | Face value at issue time |
remaining_amount | Raw remaining balance — does not subtract amounts held by other carts in flight. Use this when you want the underlying card balance regardless of pending checkouts. |
available_amount / remaining_balance / balance | Spendable balance: remaining_amount minus reserved_amount. Three names for the same value — available_amount is the canonical one; the other two are aliases kept for theme compatibility. This is the number you usually want to display. |
reserved_amount | Amount currently held by other in-flight carts (will turn into a redemption when those carts are paid, or release back if they expire) |
expires_on | Expiry date — render with the localized_date filter |
state | Raw state machine value (pending, active, disabled, …) |
pending? | Issued but not yet activated (e.g. order not fully paid) |
expired? | Past expires_on |
disabled? | Manually disabled by the shop owner |
partially_used? | Has been spent but still has balance |
depleted? | Balance has hit zero |
activated_at | Timestamp when the card became spendable |
disabled_at | Timestamp when disabled (if applicable) |
url / public_url | Tokenized public retrieval link — works without authentication |
document_url | Authenticated PDF download URL (account scope) |
Customer Account
Logged-in customers can download a gift card's PDF voucher at /account/gift-cards/:id/document. The drop already exposes this:
<a href="{{ gift_card.document_url }}" download>
{% translate 'gift_cards.download_pdf', default: 'Download PDF' %}
</a>Customizing the Guest Retrieval Page
When the first gift card is issued for your shop, Nimbu provisions two templates inside your active theme:
templates/shop/gift_card.liquid— the page rendered at/gift-cards/retrieve/:tokenfor anyone holding the linktemplates/shop/gift_card_pdf.liquid— the HTML used to render the PDF attached to the delivery email
These templates are yours to edit — they live in the theme like any other template. You can also push them manually before the first gift card is issued.
The retrieval page (and the delivery email body) receive the following Liquid variables:
| Variable | Description |
|---|---|
gift_card | The gift card drop — see Gift card properties above |
customer | The buyer (may be nil if the gift card was issued outside an order) |
order | The originating order (may be nil) |
retrieval_url | The current page's tokenized URL |
redemption_url | The shop's primary domain (where the gift card can be redeemed) |
resent | true when the buyer triggered a resend from their order page |
invoice_logo_url | Embedded data URL (or remote URL) of the shop's invoice logo, if configured |
Invalid or expired links
When a token has been rotated, the gift card has been disabled, or the link has expired, the retrieval route renders a built-in 404 page in the minimal layout — the theme template is not invoked. The strings live under gift_cards.invalid_link.title and gift_cards.invalid_link.body in your locale files; override them to match your tone.
Default markup
Use this as a starting point and restyle it to match your brand:
<main class="gift-card-artifact">
<h1>{{ site.name }}</h1>
<h2>{% translate 'gift_cards.title', default: 'Gift Card' %}</h2>
<p>
<strong>{% translate 'gift_cards.amount', default: 'Amount' %}:</strong>
{{ gift_card.initial_balance | money_with_currency }}
</p>
<p>
<strong>{% translate 'gift_cards.code', default: 'Code' %}:</strong>
{{ gift_card.code }}
</p>
<p>
<strong>{% translate 'gift_cards.remaining_balance', default: 'Remaining balance' %}:</strong>
{{ gift_card.remaining_balance | money_with_currency }}
</p>
<p>
<strong>{% translate 'gift_cards.expiry', default: 'Expiry' %}:</strong>
{{ gift_card.expires_on | localized_date: 'long' }}
</p>
<p><a href="{{ retrieval_url }}">{% translate 'gift_cards.open', default: 'Open this gift card' %}</a></p>
<p>
{% translate 'gift_cards.redeem_at', default: 'Visit' %}
<a href="{{ redemption_url }}">{{ redemption_url }}</a>
{% translate 'gift_cards.to_redeem', default: 'to redeem this gift card.' %}
</p>
</main>Customizing the PDF Voucher
The PDF template templates/shop/gift_card_pdf.liquid is rendered through wkhtmltopdf and attached to the delivery email. It receives the same variables as the guest page plus an extra gift_card_pdf hash with localized labels.
| Label | Default text |
|---|---|
gift_card_pdf.badge | "Gift card" |
gift_card_pdf.title | "Gift Card" |
gift_card_pdf.code_label | "Gift card code" |
gift_card_pdf.original_value | "Original value" |
gift_card_pdf.expiry | "Expiry" |
gift_card_pdf.buyer | "Buyer" |
gift_card_pdf.order | "Order" |
gift_card_pdf.redeem | "Redeem" |
gift_card_pdf.instructions | "Enter this gift card code at checkout." |
Override the defaults in your locale files under gift_cards.pdf.*.
PDF rendering quirks to keep in mind:
- Inline
<style>blocks in the document<head>are reliable. External stylesheet links are not — wkhtmltopdf often skips them. - Reference assets with absolute URLs (
{{ 'logo.png' | theme_image_url }}already produces one). - The default template targets A4 (
@page { size: A4; }). Change the@pagerule if you need a different size. - The
invoice_logo_urlandcustomerandorderblocks are optional — wrap them in{% if … %}so the PDF still renders for orphan gift cards or anonymous orders.
The default PDF template is intentionally compact and brand-neutral — replace it wholesale to match your brand identity.
Customizing the Delivery Email
Nimbu also creates a notification with slug gift_card_delivery the first time a gift card is issued. The shop owner can edit its subject, HTML body and text body in the admin under Settings → Notifications → Gift card delivery.
The HTML and text bodies are Liquid and receive the same variables as the guest page (gift_card, customer, order, retrieval_url, redemption_url, resent, invoice_logo_url). A copy of the PDF voucher is attached automatically — you do not need to embed the code or balance inside the email if you would rather link out.
A minimal HTML body:
<p>{% translate 'gift_cards.email.greeting', default: 'Hi' %} {{ customer.name | default: 'there' }},</p>
{% if resent %}
<p>{% translate 'gift_cards.email.resend_intro', default: 'Here is your gift card again.' %}</p>
{% else %}
<p>{% translate 'gift_cards.email.intro', default: 'Thanks for your purchase! Your gift card is ready.' %}</p>
{% endif %}
<p>
<strong>{{ gift_card.initial_balance | money_with_currency }}</strong> —
{% translate 'gift_cards.expires', default: 'expires' %}
{{ gift_card.expires_on | localized_date: 'long' }}
</p>
<p>
<a href="{{ retrieval_url }}">
{% translate 'gift_cards.email.cta', default: 'Open your gift card' %}
</a>
</p>
<p>
{% translate 'gift_cards.email.redeem_at', default: 'Redeem at' %}
<a href="{{ redemption_url }}">{{ redemption_url }}</a>
</p>Routes Reference
| Method | Path | Purpose |
|---|---|---|
POST | /cart/coupons | Apply a gift card or coupon code to the current cart. Body: coupon[code]. Render with {% form 'coupon' %} + {% input 'code' %}. |
DELETE | /cart/gift-cards/:id | Remove an applied gift card adjustment. :id is the adjustment id from cart.gift_card_adjustments (use adjustment.remove_url). |
GET | /gift-cards | Built-in storefront landing page for gift cards. Link with url.gift_cards. |
GET | /gift-cards/retrieve/:token | Public, tokenized retrieval page rendered through templates/shop/gift_card.liquid. Renders a 404 page when the token is no longer valid. Link with gift_card.url. |
GET | /account/gift-cards/:id/document | Authenticated PDF download. Available as gift_card.document_url. |
What Is Not Exposed (yet)
A few intentional gaps to be aware of so you do not fight the framework:
customer.gift_cardsis not available on the customer drop — surface gift cards throughorder.gift_cardson the order history instead.- There is no
gift_card?flag on the product drop. Use{% scope gift_card == true %}to query gift card products. - There is no real-time pre-flight balance check before applying — feedback is delivered through the flash message after the redirect. Plan your UI around an optimistic apply, then a clear flash message.
Best Practices
- Keep gift card listings on a dedicated page, not on the homepage or default collections — Nimbu's default visibility filter is intentional, surfacing them everywhere undermines it.
- Render flash messages immediately above the apply form so the response from a failed code lands where the customer is looking.
- Always render
expires_onwith thelocalized_datefilter so the format respects the active locale. - The drop masks
gift_card.codefor you outside the retrieval page, PDF and delivery email — you no longer have to remember to callmasked_codeyourself on cart, order or account surfaces. Reach formasked_codeonly when you want to be explicit. - Wrap optional blocks (
{% if customer %},{% if order.number %},{% if invoice_logo_url %}) in the retrieval and PDF templates — gift cards can be issued without a buyer or order in some manual flows.
Next Steps
- Webshop — Shopping cart, checkout, payment forms
- E-commerce filters —
add_to_cart,update_cart_item,apply_coupon_form, … - Money filters — Price formatting helpers