"Buy 12 Get 1 Free" Loyalty Program Implementation discount issue

Summary: “Buy 12 Get 1 Free” Loyalty Program Implementation

What We’re Trying to Achieve

We run a frequent buyer program where customers earn a free item after purchasing 12 qualifying items (e.g., coffee bags). When they’ve earned their reward:

  • They should get exactly 1 item FREE on their next visit

  • The discount should auto-apply at Square POS when the customer is identified

  • They can redeem on any item in the offer (not just the exact SKU they purchased)

  • If they buy multiple qualifying items, only 1 should be free, not all of them

Our Implementation Approach

We’re using Customer Group Discounts with the Catalog API:

  1. When customer earns a reward, we create a Customer Group and add them to it

  2. We create a Catalog Discount (either FIXED_PERCENTAGE 100% or FIXED_AMOUNT)

  3. We create a Product Set with the qualifying variation IDs

  4. We create a Pricing Rule linking the discount to the customer group and product set

This works for auto-applying the discount when the customer is identified at POS.

The Problem

The discount applies to EVERY qualifying item in the cart, not just 1.

Example: Customer has earned 1 free coffee bag. They come in and buy 3 bags at $15 each.

  • Expected: 1 bag free, pay $30 for the other 2

  • Actual: All 3 bags are free (or all get the discount applied)

What We’ve Tried

Approach Result
FIXED_PERCENTAGE: 100% with maximum_amount_money cap Cap applies per-item, not per-order. All items get discounted up to the cap.
FIXED_AMOUNT equal to 1 item’s price Amount applies to each matching item separately. All items get the discount.
exclude_strategy: LEAST_EXPENSIVE with same product set for match and exclude Only excludes 1 item. If customer buys 3 items, 2 still get discounted.
quantity_exact: 1 on product set Staff must remove extra discounts if customer buys 2+

Our Question for Square

Is there a way to configure a Catalog Pricing Rule to apply a discount to only 1 item, even when multiple matching items are in the cart?

Specifically:

  • Can maximum_amount_money be made to cap the total discount across all items rather than per-item?

  • Is there an exclude_strategy or product set configuration that excludes “all but 1” item?

  • Is there a different discount structure (VARIABLE_PERCENTAGE, etc.) that achieves “1 free item” behavior?

  • Should we be using a different API entirely (e.g., Loyalty API with custom reward tiers)?

Environment

  • Using Catalog API v2 (Square-Version: 2024-01-18)

  • Pricing Rules with customer_group_ids_any for auto-apply

  • Product Sets with product_ids_any for qualifying items

:waving_hand: I’m Square Dev AI, here to help answer your question!
Based on the information you’ve provided, the following documentation will help answer your question:

Additional Documentation

Create Customer Group Discounts
Automatically Apply Discounts
Apply Square-Defined Discounts to Orders

If this documentation helped you find a solution, please check the box indicating that this reply solves the problem.
Otherwise one of our Developer Advocates will provide additional assistance shortly.

I think it’s fixed!

After a lot of trial and error with the exclude strategies, we found a simpler solution that works.

What Didn’t Work:

The exclude_strategy: LEAST_EXPENSIVE approach was unreliable. The quantity_min on the exclude product set seemed to control how many items to exclude, not when the exclusion activates:

  • quantity_min: 1 → excluded the single item, leaving 0 for discount

  • quantity_min: 2 → required 3+ items in cart to leave 1 for discount

What Worked:

We removed the exclude product set entirely and used maximum_amount_money on a FIXED_PERCENTAGE discount:

{
    type: 'DISCOUNT',
    id: discountId,
    discount_data: {
        name: 'Loyalty: FREE Item',
        discount_type: 'FIXED_PERCENTAGE',
        percentage: '100.0',
        maximum_amount_money: {
            amount: maxItemPriceCents,  // e.g., 1500 for $15.00
            currency: 'USD'
        },
        modify_tax_basis: 'MODIFY_TAX_BASIS'
    }
}

Then a simple pricing rule with just a match product set (no exclude):

{
    type: 'PRICING_RULE',
    id: pricingRuleId,
    pricing_rule_data: {
        name: 'FBP Reward',
        discount_id: discountId,
        match_products_id: matchProductSetId,
        customer_group_ids_any: [groupId]
    }
}

How It Works:

Cart 100% Discount Would Be Capped At Actual Discount Customer Pays
1 × $15 $15 $15 $15 $0 (FREE) ✓
2 × $15 $30 $15 $15 $15 ✓
3 × $15 $45 $15 $15 $30 ✓

The maximum_amount_money caps the total discount value, not per-item. So regardless of how many qualifying items are in the cart, only one item’s worth gets discounted.

Important: Set the cap to the most expensive item price in your offer (fetch from catalog), so customers can redeem on any qualifying item.

Hope this helps someone else struggling with the same issue!

At this time there isn’t a way to configure the Catalog pricing rule to apply a discount to 12 item when matching items are in the order with auto discounts. We’re constantly working to improve our features based on feedback like this, so I’ll be sure to share your request to the API product team. :slight_smile:

I think you misunderstand what I am doing, we are recording the 12 prior purchases and when they come back their is a discount in their profile for the 13th free.

We have found some success, however there may be some edge cases we havnt identified. If you’d like to see my code you can review the repo at GitHub - JTPets/SquareDashboardTool

@Bryan-Square this is my workflow for this project, and I THINK I have it working, please take a look and give me your expert opinion - also this is an “OPEN” feature request with Square so feel free to tag the team responsible for implementing if you’d like :slight_smile:

Frequent Buyer Program - Technical Flow

1. CUSTOMER IDENTIFICATION

Order comes in via webhook (order.payment.completed)
    ↓
Get customer ID (priority order):
    1. order.customer_id (most reliable - POS identified customer)
    2. tender.customer_id (fallback - customer on payment)
    3. Square Loyalty API lookup by order_id (last resort)
    ↓
Store: square_customer_id (UUID) - never fuzzy/timestamp matching

2. ITEM MATCHING

For each line_item in order:
    ↓
Query: Does variation_id exist in loyalty_qualifying_variations?
    ↓
YES → Get associated offer (e.g., "Buy 12 Astro 12oz, Get 1 Free")
NO  → Skip (not a qualifying item)

Key: Explicit variation IDs only - no brand/category inference

3. QUANTITY TRACKING

Record purchase in loyalty_purchase_events:
    - square_customer_id
    - variation_id  
    - quantity
    - window_end_date (purchase_date + 12 months)
    ↓
Calculate progress:
    SELECT SUM(quantity) FROM loyalty_purchase_events
    WHERE square_customer_id = X
      AND offer_id = Y
      AND window_end_date >= TODAY  -- Rolling window
      AND reward_id IS NULL         -- Not yet locked to a reward
    ↓
If SUM >= required_quantity (e.g., 12):
    → Create reward with status = 'earned'
    → Lock those purchase events to this reward_id

4. REWARD ISSUANCE (Square Objects)

When reward earned, create 3 Square objects:

1. CUSTOMER GROUP (one member only)
   POST /v2/customers/groups
   → Add only this customer to the group
   
2. CATALOG DISCOUNT  
   - 100% off
   - maximum_amount_money = highest qualifying item price
   
3. PRICING RULE (ties it together)
   - discount_id = the discount above
   - match_products_id = qualifying variation IDs
   - customer_group_ids_any = [the group above]  ← ONLY THIS CUSTOMER

5. HOW IT AUTO-APPLIES AT POS

Customer checks out at Square POS
    ↓
Square checks: Is customer in any groups?
    ↓
YES → Check pricing rules for those groups
    ↓
Pricing rule matches:
    - Customer in group? ✓
    - Item in product set? ✓
    ↓
100% discount auto-applies (capped at max item price)

6. REDEMPTION DETECTION

Webhook: order.completed with discounts
    ↓
For each discount in order:
    Does catalog_object_id match our square_discount_id?
    ↓
YES → Mark reward as 'redeemed'
    → Cleanup: Remove customer from group, delete discount objects


Square API Objects Created Per Reward

Customer Group:     "FBP Reward #123: Astro 12oz"
                    └─ Members: [1 customer only]

Catalog Discount:   "zz_Loyalty: FREE Astro 12oz"  
                    └─ 100%, max $X.XX

Pricing Rule:       Links discount + products + customer group
                    └─ Auto-applies when all 3 match