Shopify Guide: Setup Gift With Purchase Using Discount Code Only

Shopify Guide: Setup Gift With Purchase Using Discount Code Only

Introduction

Gift With Purchase promotions are one of the highest-converting tactics in ecommerce. The mechanic is simple: spend enough, get a free gift. Most GWP apps implement this automatically — the moment a customer's cart hits the threshold, the gift appears.

But that's not what you want.

You want the gift to be gated behind a specific discount code. The customer must know the code, enter it at checkout, and only then does the free gift get triggered. The code might come from:

  • An email campaign exclusive to VIP subscribers
  • A partnership with an influencer or affiliate
  • A print insert in previous orders
  • A bank or loyalty card promotion
  • A limited event or pop-up activation

The problem is that Shopify's native discount system and most GWP apps are not built for this specific mechanic. Automatic GWP apps add the gift based on cart value — not on code entry. Discount codes can give percentage or fixed amount off — but not add a physical product to the cart. These two systems don't natively talk to each other.

This guide covers every approach to building a code-gated GWP — from clever native workarounds to Shopify Functions to custom JavaScript — so you can choose the right implementation for your store.


Who This Guide Is For

  • Merchants running exclusive GWP promotions tied to specific campaigns or audiences
  • Developers building custom GWP logic in Shopify themes or apps
  • Merchants who find that automatic GWP apps add gifts too freely without code gating
  • Teams implementing influencer or affiliate gift promotions via unique codes
  • Anyone who has tried to combine a discount code with a free product add and hit a dead end

Understanding the Native Limitation

Shopify's discount system supports these discount types natively:

Discount Type Supported?
Percentage off ✅ Yes
Fixed amount off ✅ Yes
Free shipping ✅ Yes
Buy X Get Y (BXGY) ✅ Yes — closest native option
Add free product on code entry ❌ Not directly
Add product to cart via discount ❌ Not possible natively

The key constraint: Shopify discount codes operate on the cart's existing contents — they cannot add new items to the cart. A discount code can make a product free, but the product must already be in the cart for the discount to apply to it.

This creates the core challenge of code-gated GWP: how do you get the gift product into the cart, priced at zero, but only when the code is entered?


Approach 1 — Buy X Get Y Discount Code (Native, No Code Required)

Complexity: ⭐ Easy True code gate: Partial — BXGY requires code entry but customer controls gift selection Cost: Free Best for: Simple GWP where the gift is one specific product and customer selects it

Shopify's native Buy X Get Y discount type is the closest native mechanism to a code-gated GWP. When a customer enters the code, they receive a specific product (the gift) for free — but they must add it to their cart themselves, or it appears as a selectable offer.

Setup:

  1. Go to Discounts → Create discount → Buy X Get Y
  2. Configure Customer buys:
    • Minimum quantity: 1 (or minimum quantity of your qualifying products)
    • Any products from: select your qualifying collection or specific products
  3. Configure Customer gets:
    • Quantity: 1
    • Specific products: select your gift product
    • At a discounted value: 100% off (makes it free)
  4. Under Method:
    • Select Discount code (not automatic — this is the key setting)
    • Enter your code (e.g., VIPGIFT2026)
  5. Set Usage limits, customer eligibility, and expiry as needed
  6. Save

What happens at checkout:

  • Customer shops normally and adds qualifying products
  • Customer enters VIPGIFT2026 at checkout
  • Shopify shows the gift offer — customer clicks to add it to their order for free
  • Order completes with gift at $0

Pros:

  • Zero development — fully native
  • Works on all Shopify plans
  • Customer explicitly chooses to add the gift — intentional UX
  • Code-gated — only customers with the code get the gift

Cons:

  • Customer must manually add the gift — some will miss it or not bother
  • Gift appears as a selectable offer at checkout — not added to cart automatically
  • No automatic cart add behavior — just a discount on a product the customer must add
  • BXGY logic may conflict with other active discounts depending on your plan's stacking rules

Improving the UX — link to the gift product:

In your promotional email or banner, include a direct link that pre-adds the gift to the cart alongside a cart note:


https://yourstore.com/cart/add?id=[GIFT_VARIANT_ID]&quantity=1&return_to=/cart

This gets the gift in the cart before the customer even enters the code — then the BXGY discount makes it free when code is applied. Combine with a banner that says "Add your gift to cart, then enter code VIPGIFT2026 at checkout."


Approach 2 — Zero-Price Gift Variant + Code-Triggered Cart Script

Complexity: ⭐⭐⭐ Intermediate True code gate: Yes — gift only added when code is detected Cost: Free (theme development time) Best for: Themes with accessible cart JavaScript, technically comfortable merchants

This approach uses a $0 gift variant combined with JavaScript that monitors the discount code field and automatically adds the gift to the cart when the matching code is entered.


Step 1 — Create the Gift Product

  1. Go to Products → Add product
  2. Create the gift product:
    • Title: "Free Gift — [Campaign Name]" (or a non-public name)
    • Price: $0.00
    • Track quantity: Yes — set to your gift inventory
    • Status: Active but not published to any sales channel
    • Note the Variant ID — you'll need it for the JavaScript

Why not publish it: A $0 product visible in your store can be added freely by anyone. Keep it unpublished — it can still be added to cart programmatically even when not in a collection.

Getting the Variant ID:

  1. Go to the product → click the variant
  2. The URL shows: /products/[handle]/variants/[VARIANT_ID]
  3. Save this number

Step 2 — Create the Discount Code

Create a standard discount code that customers will enter:

  1. Go to Discounts → Create discount → Amount off order (or percentage)
  2. Set the code: VIPGIFT2026
  3. You can set $0 off if the only benefit is the gift itself, or add a real discount on top
  4. Set usage limits and expiry
  5. Save

Note: This discount code itself doesn't add the gift — the JavaScript does. The code just needs to exist so Shopify accepts it at checkout.


Step 3 — Add Cart Page JavaScript

In your theme, add JavaScript to the cart page that watches for the discount code entry:


// theme/assets/gwp-code-trigger.js

(function() {
  'use strict';

  const GWP_CONFIG = {
    triggerCode: 'VIPGIFT2026',           // The discount code that triggers the gift
    giftVariantId: 'YOUR_VARIANT_ID_HERE', // Variant ID of your $0 gift product
    giftQuantity: 1,                        // How many gifts to add
    storageKey: 'gwp_code_applied',         // localStorage key to prevent duplicate adds
    checkInterval: 500                      // ms between discount field checks
  };

  // Normalize code for comparison (case-insensitive, trimmed)
  function normalizeCode(code) {
    return (code || '').trim().toUpperCase();
  }

  // Add gift to cart via Ajax API
  async function addGiftToCart() {
    // Check if gift already in cart
    const cartResponse = await fetch('/cart.js');
    const cart = await cartResponse.json();

    const giftAlreadyInCart = cart.items.some(
      item => String(item.variant_id) === String(GWP_CONFIG.giftVariantId)
    );

    if (giftAlreadyInCart) {
      console.log('[GWP] Gift already in cart — skipping add');
      return;
    }

    // Add gift to cart
    const addResponse = await fetch('/cart/add.js', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        items: [{
          id: GWP_CONFIG.giftVariantId,
          quantity: GWP_CONFIG.giftQuantity,
          properties: {
            '_gift': 'true',           // Hidden property — marks it as a gift
            '_gwp_code': GWP_CONFIG.triggerCode
          }
        }]
      })
    });

    if (addResponse.ok) {
      console.log('[GWP] Gift added to cart successfully');
      localStorage.setItem(GWP_CONFIG.storageKey, GWP_CONFIG.triggerCode);

      // Refresh cart UI
      refreshCartUI();
      showGiftNotification();
    } else {
      console.error('[GWP] Failed to add gift:', await addResponse.text());
    }
  }

  // Remove gift from cart if code is cleared
  async function removeGiftFromCart() {
    const cartResponse = await fetch('/cart.js');
    const cart = await cartResponse.json();

    const giftItem = cart.items.find(
      item => String(item.variant_id) === String(GWP_CONFIG.giftVariantId)
    );

    if (!giftItem) return;

    await fetch('/cart/change.js', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        id: giftItem.key,
        quantity: 0
      })
    });

    localStorage.removeItem(GWP_CONFIG.storageKey);
    refreshCartUI();
    showGiftRemovedNotification();
  }

  // Watch for discount code input on cart page
  function watchDiscountField() {
    // Find discount input — selector varies by theme
    const discountSelectors = [
      'input[name="discount"]',
      '#discount-code',
      '.discount-form__input',
      '[data-discount-input]'
    ];

    let discountInput = null;
    for (const selector of discountSelectors) {
      discountInput = document.querySelector(selector);
      if (discountInput) break;
    }

    if (!discountInput) {
      console.log('[GWP] Discount input not found on this page');
      return;
    }

    let lastCheckedCode = '';

    // Watch for input changes
    discountInput.addEventListener('input', debounce(async (e) => {
      const enteredCode = normalizeCode(e.target.value);
      if (enteredCode === lastCheckedCode) return;
      lastCheckedCode = enteredCode;

      if (enteredCode === GWP_CONFIG.triggerCode) {
        await addGiftToCart();
      } else if (localStorage.getItem(GWP_CONFIG.storageKey)) {
        // Code was previously applied but now changed — remove gift
        await removeGiftFromCart();
      }
    }, 600));

    // Also watch for form submission (code applied via button)
    const discountForm = discountInput.closest('form');
    if (discountForm) {
      discountForm.addEventListener('submit', async () => {
        const enteredCode = normalizeCode(discountInput.value);
        if (enteredCode === GWP_CONFIG.triggerCode) {
          // Small delay to ensure Shopify processes the code first
          setTimeout(addGiftToCart, 1000);
        }
      });
    }
  }

  // Watch for checkout discount field (if on checkout page)
  function watchCheckoutDiscountField() {
    // Checkout pages have different selectors
    const checkoutInput = document.querySelector(
      '#checkout_reduction_code, [data-checkout-discount-field]'
    );

    if (!checkoutInput) return;

    checkoutInput.addEventListener('change', async (e) => {
      const enteredCode = normalizeCode(e.target.value);
      if (enteredCode === GWP_CONFIG.triggerCode) {
        await addGiftToCart();
      }
    });
  }

  // Refresh cart drawer/page UI after adding gift
  function refreshCartUI() {
    // Trigger Shopify's cart update events
    document.dispatchEvent(new CustomEvent('cart:refresh'));

    // For themes that use fetch-based cart updates
    if (typeof window.Shopify?.onCartUpdate === 'function') {
      window.Shopify.onCartUpdate();
    }

    // For Dawn and compatible themes
    document.documentElement.dispatchEvent(
      new CustomEvent('cart:update', { bubbles: true })
    );

    // Fallback — reload the page
    setTimeout(() => {
      if (window.location.pathname === '/cart') {
        window.location.reload();
      }
    }, 1500);
  }

  // User-facing notification
  function showGiftNotification() {
    const notification = document.createElement('div');
    notification.className = 'gwp-notification gwp-notification--added';
    notification.innerHTML = `
      <p>🎁 Your free gift has been added to your cart!</p>
    `;
    notification.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      background: #2e7d32;
      color: white;
      padding: 16px 24px;
      border-radius: 8px;
      font-size: 14px;
      z-index: 9999;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      animation: slideIn 0.3s ease;
    `;
    document.body.appendChild(notification);
    setTimeout(() => notification.remove(), 4000);
  }

  function showGiftRemovedNotification() {
    const notification = document.createElement('div');
    notification.className = 'gwp-notification gwp-notification--removed';
    notification.innerHTML = `<p>Gift removed — discount code cleared.</p>`;
    notification.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      background: #c62828;
      color: white;
      padding: 16px 24px;
      border-radius: 8px;
      font-size: 14px;
      z-index: 9999;
    `;
    document.body.appendChild(notification);
    setTimeout(() => notification.remove(), 3000);
  }

  // Utility — debounce function
  function debounce(fn, delay) {
    let timer;
    return function(...args) {
      clearTimeout(timer);
      timer = setTimeout(() => fn.apply(this, args), delay);
    };
  }

  // Initialize
  function init() {
    watchDiscountField();
    watchCheckoutDiscountField();

    // Check if code was previously applied (page reload persistence)
    const savedCode = localStorage.getItem(GWP_CONFIG.storageKey);
    if (savedCode === GWP_CONFIG.triggerCode) {
      // Verify gift is still in cart
      fetch('/cart.js')
        .then(r => r.json())
        .then(cart => {
          const giftInCart = cart.items.some(
            item => String(item.variant_id) === String(GWP_CONFIG.giftVariantId)
          );
          if (!giftInCart) {
            // Code was applied before but gift got removed — re-add
            addGiftToCart();
          }
        });
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

Step 4 — Include the Script in Your Theme

In your theme's layout/theme.liquid, include the script on cart and product pages:


{%- if request.page_type == 'cart' or request.page_type == 'product' -%}
  {{ 'gwp-code-trigger.js' | asset_url | script_tag }}
{%- endif -%}

Step 5 — Prevent Gift Removal by Customer

Mark the gift line item so your cart template hides the remove button:


{%- comment -%} In your cart-items snippet — hide remove for gift items {%- endcomment -%}
{%- for item in cart.items -%}
  <div class="cart-item">
    {%- unless item.properties['_gift'] == 'true' -%}
      <button class="cart-item__remove">Remove</button>
    {%- endunless -%}
    {%- comment -%} Rest of cart item markup {%- endcomment -%}
  </div>
{%- endfor -%}

Approach 3 — Shopify Functions: Cart Transform for GWP

Complexity: ⭐⭐⭐⭐ Advanced True code gate: Yes — server-side enforcement Cost: Shopify Plus required + development Best for: Enterprise merchants needing server-side GWP logic that cannot be bypassed

Shopify Functions' Cart Transform API allows you to modify cart contents server-side — including adding free gift line items when specific conditions are met. This is the most robust implementation because it runs on Shopify's infrastructure and cannot be bypassed by JavaScript manipulation.


The Cart Transform Approach


// extensions/gwp-cart-transform/src/index.js

export function run(input) {
  const cart = input.cart;
  const discountCodes = cart.discountCodes ?? [];
  const lines = cart.lines;

  // Configuration
  const GWP_TRIGGER_CODE = 'VIPGIFT2026';
  const GIFT_VARIANT_GID = 'gid://shopify/ProductVariant/YOUR_VARIANT_ID';
  const MIN_ORDER_VALUE = 50.00; // Optional minimum order value

  // Check if the GWP code is applied
  const gwpCodeApplied = discountCodes.some(
    dc => dc.code.toUpperCase() === GWP_TRIGGER_CODE && dc.applicable
  );

  if (!gwpCodeApplied) {
    return { operations: [] }; // No GWP code — do nothing
  }

  // Optional: Check minimum order value
  const cartTotal = lines.reduce((sum, line) => {
    return sum + (parseFloat(line.cost.totalAmount.amount) * line.quantity);
  }, 0);

  if (cartTotal < MIN_ORDER_VALUE) {
    return { operations: [] }; // Below threshold — no gift
  }

  // Check if gift is already in cart
  const giftAlreadyInCart = lines.some(
    line => line.merchandise.id === GIFT_VARIANT_GID
  );

  if (giftAlreadyInCart) {
    return { operations: [] }; // Gift already present — skip
  }

  // Add the gift line item
  return {
    operations: [
      {
        addLineItem: {
          merchandiseId: GIFT_VARIANT_GID,
          quantity: 1,
          price: {
            adjustment: {
              fixedPricePerUnit: {
                amount: '0.0',
                currencyCode: input.cart.cost.totalAmount.currencyCode
              }
            }
          },
          attributes: [
            { key: '_gift', value: 'true' },
            { key: '_gwp_code', value: GWP_TRIGGER_CODE }
          ]
        }
      }
    ]
  };
}

shopify.extension.toml:


[[extensions]]
type = "function"
name = "GWP Code Gate"
handle = "gwp-code-gate"
api_version = "2024-10"

[[extensions.targeting]]
module = "./src/index.js"
target = "purchase.cart-transform.run"

Deploy and activate:


# Deploy the function
shopify app deploy

# The function activates automatically when your app is installed
# Verify in Partner Dashboard → Apps → [Your App] → Extensions

Pros of Functions approach:

  • Server-side — cannot be bypassed by JavaScript manipulation
  • Gift is added and priced server-side — no $0 product needed
  • Runs on every cart update — always in sync with code application
  • Works in cart, checkout, and Buy Now flows consistently

Cons:

  • Requires Shopify Plus
  • More complex to develop and deploy
  • Requires app infrastructure (not a standalone theme customization)

Approach 4 — GWP App + Code Gating Workaround

Complexity: ⭐⭐ Easy-Moderate True code gate: Partial — depends on app capabilities Cost: App subscription cost Best for: Merchants who prefer apps over custom code

Most GWP apps trigger automatically based on cart value. However, some apps support discount code triggers as a condition. When evaluating GWP apps, look specifically for these features:

Key questions to ask before installing a GWP app:

  • Does it support discount code as a trigger condition (not just cart value)?
  • Can it require a specific code to be entered before the gift is added?
  • Does it remove the gift if the code is removed?
  • Does it work with Shopify's native discount codes or its own code system?
  • Is the gift locked in the cart (customer can't remove it)?

App evaluation checklist:


□ Discount code trigger: YES / NO
□ Removes gift if code removed: YES / NO
□ Works at cart AND checkout stage: YES / NO
□ Supports $0 line item gift: YES / NO
□ Hides gift remove button: YES / NO
□ Supports multiple concurrent GWP campaigns: YES / NO
□ Theme compatibility confirmed: YES / NO

Search the Shopify App Store for:

  • "gift with purchase"
  • "free gift discount code"
  • "GWP code trigger"

Look for apps that explicitly mention "discount code" as a trigger in their feature list — not just cart value thresholds.


Approach 5 — Checkout UI Extension + Metafield Gate

Complexity: ⭐⭐⭐⭐ Advanced True code gate: Yes Cost: Shopify Plus + development Best for: Custom checkout experiences with explicit gift selection UI

Build a Checkout UI Extension that displays a gift selection widget only when the correct code is applied — letting customers choose their gift from multiple options:


// extensions/gwp-checkout-widget/src/index.jsx
import { useState, useEffect } from 'react';
import {
  reactExtension,
  useDiscountCodes,
  useApplyCartLinesChange,
  Banner,
  Button,
  BlockStack,
  Text,
  Image,
  InlineLayout,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
  'purchase.checkout.cart-line-list.render-after',
  () => <GWPWidget />
);

function GWPWidget() {
  const discountCodes = useDiscountCodes();
  const applyCartLinesChange = useApplyCartLinesChange();
  const [giftAdded, setGiftAdded] = useState(false);
  const [selectedGift, setSelectedGift] = useState(null);

  const GWP_CODE = 'VIPGIFT2026';
  const GIFT_OPTIONS = [
    {
      id: 'gid://shopify/ProductVariant/111111',
      title: 'Silk Pouch',
      image: 'https://cdn.shopify.com/...',
      variantId: '111111'
    },
    {
      id: 'gid://shopify/ProductVariant/222222',
      title: 'Travel Candle',
      image: 'https://cdn.shopify.com/...',
      variantId: '222222'
    }
  ];

  // Check if GWP code is applied
  const gwpCodeActive = discountCodes.some(
    dc => dc.code.toUpperCase() === GWP_CODE && dc.applicable
  );

  if (!gwpCodeActive) return null; // Hide widget if code not applied

  async function addSelectedGift(gift) {
    const result = await applyCartLinesChange({
      type: 'addCartLine',
      merchandiseId: gift.id,
      quantity: 1,
      attributes: [{ key: '_gift', value: 'true' }]
    });

    if (result.type === 'success') {
      setGiftAdded(true);
      setSelectedGift(gift);
    }
  }

  if (giftAdded) {
    return (
      <Banner status="success">
        <Text>🎁 Your free gift ({selectedGift.title}) has been added!</Text>
      </Banner>
    );
  }

  return (
    <BlockStack>
      <Banner status="info">
        <Text emphasis="bold">You've unlocked a free gift! Choose one:</Text>
      </Banner>
      <InlineLayout columns={['50%', '50%']}>
        {GIFT_OPTIONS.map(gift => (
          <BlockStack key={gift.id} spacing="tight">
            <Image source={gift.image} alt={gift.title} />
            <Text>{gift.title}</Text>
            <Button onPress={() => addSelectedGift(gift)}>
              Add This Gift
            </Button>
          </BlockStack>
        ))}
      </InlineLayout>
    </BlockStack>
  );
}

This gives customers a choice of gift — a powerful UX upgrade over automatic single-gift adds that significantly boosts perceived value.


Preventing Gift Abuse Across All Approaches

Regardless of which approach you use, implement these safeguards:


Validate Gift Is Tied to Code at Checkout

Use a Shopify Flow workflow to validate the order after placement:


Trigger: Order created
Condition: Order contains gift product variant ID
Condition: Order discount codes does NOT contain "VIPGIFT2026"
Action: Add order tag "gwp-abuse-suspected"
Action: Send internal alert email

Prevent Direct Cart URL Gift Adding

Block direct add-to-cart for the gift product via a cart:add webhook or server-side validation:


// Server-side cart validation (via App Proxy or custom endpoint)
app.post('/validate-cart', async (req, res) => {
  const { items, discountCode } = req.body;

  const giftVariantId = 'YOUR_VARIANT_ID';
  const requiredCode = 'VIPGIFT2026';

  const hasGiftItem = items.some(
    item => String(item.variant_id) === String(giftVariantId)
  );

  const hasCorrectCode = normalizeCode(discountCode) === requiredCode;

  if (hasGiftItem && !hasCorrectCode) {
    return res.json({
      valid: false,
      error: 'Gift product requires discount code VIPGIFT2026'
    });
  }

  return res.json({ valid: true });
});

Order Tag Audit Report via Shopify Flow


Weekly scheduled trigger
Action: Search orders tagged "gwp-abuse-suspected"
Action: Send summary email with order IDs
Action: For each flagged order, create internal task

Complete Implementation Checklist

Before launch:

  • Gift product created at $0, unpublished from storefront
  • Gift variant ID noted and added to JavaScript/Function config
  • Discount code created in Shopify Admin with correct settings
  • JavaScript installed and tested on cart page
  • Gift remove button hidden in cart template
  • Gift adds correctly on code entry
  • Gift removes correctly if code is cleared
  • Gift persists across page reloads (localStorage check)
  • Mobile cart tested — discount field and gift add both work
  • Out-of-stock scenario handled gracefully

After launch:

  • Shopify Flow abuse detection workflow active
  • Gift inventory monitored (set low-stock alert)
  • Code usage tracked (Shopify discount analytics)
  • Promo email click-through vs. gift redemption rate compared

Choosing the Right Approach

Situation Recommended Approach
Non-technical merchant, want it working today Approach 1 (BXGY discount code)
Technical merchant, one gift product, any plan Approach 2 (JavaScript + $0 variant)
Enterprise, need bulletproof server-side logic Approach 3 (Shopify Functions Cart Transform)
Multiple gift choices at checkout Approach 5 (Checkout UI Extension)
Prefer an app, willing to research options Approach 4 (GWP app with code trigger)
Influencer/affiliate unique codes, large volume Approach 2 or 3 with bulk code generation
Back to blog

Leave a comment