Nimbu Developer Docs
Using Content

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 GiftCard records — one per gift card line item — and emails them to the buyer.
  • Gift card products are filtered out of products.all, products.featured, and collection.products by 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:

KeyMeaning
appliedCode accepted and applied to the cart
removedAdjustment removed from the cart
invalidCode does not match any gift card
disabledGift card has been disabled by the shop owner
expiredGift card has expired
emptyNo remaining balance
unavailableBalance is currently held by another reservation — try again later
failedGeneric apply failure
not_foundAdjustment not found when removing
purchase_blockedCart contains only gift card products
try_againReservation 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:

PropertyDescription
gift_card_adjustmentsArray of currently applied codes (see Applied gift card properties)
gift_card_adjustments?true if at least one is applied
gift_card_totalSum of the applied gift card discounts
gift_cardsArray 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:

PropertyDescription
idAdjustment id (used in remove_url)
amountDiscount amount applied to this cart
code / masked_codeMasked code (safe to render)
stateInternal state
eligible?Whether the adjustment currently reduces the order total
gift_cardThe 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_urlLocale-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

PropertyDescription
idGift card id
codeMasked 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_codeAlways the masked form. Safe to render anywhere.
currencyISO currency code
initial_amount / initial_balanceFace value at issue time
remaining_amountRaw 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 / balanceSpendable 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_amountAmount currently held by other in-flight carts (will turn into a redemption when those carts are paid, or release back if they expire)
expires_onExpiry date — render with the localized_date filter
stateRaw 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_atTimestamp when the card became spendable
disabled_atTimestamp when disabled (if applicable)
url / public_urlTokenized public retrieval link — works without authentication
document_urlAuthenticated 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/:token for anyone holding the link
  • templates/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:

VariableDescription
gift_cardThe gift card drop — see Gift card properties above
customerThe buyer (may be nil if the gift card was issued outside an order)
orderThe originating order (may be nil)
retrieval_urlThe current page's tokenized URL
redemption_urlThe shop's primary domain (where the gift card can be redeemed)
resenttrue when the buyer triggered a resend from their order page
invoice_logo_urlEmbedded data URL (or remote URL) of the shop's invoice logo, if configured

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.

LabelDefault 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 @page rule if you need a different size.
  • The invoice_logo_url and customer and order blocks 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

MethodPathPurpose
POST/cart/couponsApply a gift card or coupon code to the current cart. Body: coupon[code]. Render with {% form 'coupon' %} + {% input 'code' %}.
DELETE/cart/gift-cards/:idRemove an applied gift card adjustment. :id is the adjustment id from cart.gift_card_adjustments (use adjustment.remove_url).
GET/gift-cardsBuilt-in storefront landing page for gift cards. Link with url.gift_cards.
GET/gift-cards/retrieve/:tokenPublic, 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/documentAuthenticated 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_cards is not available on the customer drop — surface gift cards through order.gift_cards on 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_on with the localized_date filter so the format respects the active locale.
  • The drop masks gift_card.code for you outside the retrieval page, PDF and delivery email — you no longer have to remember to call masked_code yourself on cart, order or account surfaces. Reach for masked_code only 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

On this page