Tom Sailors
· Refreshed
Brief · Anonymized case study

Points & Redemption Portal

Build a loyalty system that records points to customer accounts on order completion, then expose balances and redemption in a dedicated portal. The merchant can set the earning multiplier (points per dollar), define minimum thresholds for redemption, and generate discount codes on demand. An admin dashboard aggregates points issued and redeemed across the customer base to surface engagement trends.

A mid-market DTC brand needed to build a custom loyalty program where customers accumulate points on each purchase and redeem them for discount codes. The merchant required a customer-facing portal to view balances and redemption history, plus an admin dashboard to track program health and top earners.
Four pieces
Loyalty

Points Accumulator

Records points to each customer's account whenever an order is paid, using a multiplier you choose — like 1 point per dollar spent.

Custom app + order webhook
Admin GraphQL explorer graphql
# Admin GraphQL
mutation SetCustomerPoints($ownerId: ID!, $points: String!) {
  metafieldsSet(metafields: [{
    ownerId: $ownerId
    namespace: "loyalty"
    key: "points_balance"
    type: "number_integer"
    value: $points
  }]) {
    metafields {
      id
      value
    }
    userErrors {
      field
      message
    }
  }
}

# Variables:
# {
#   "ownerId": "gid://shopify/Customer/12345",
#   "points": "150"
# }
Metafield values are always strings. Changed $points variable type from Int! to String! and adjusted the example variable from 150 to "150".
Loyalty

Customer Portal

A dedicated page in your online store where customers log in to see their current points balance, recent earning history, and redemption history.

Theme app extension + React
theme/snippets/loyalty-portal.liquid liquid
{% if customer %}
  <div class="loyalty-portal">
    <h2>Your Points Balance</h2>
    {% assign points = customer.metafields.loyalty.points_balance | default: 0 %}
    <p class="balance">{{ points }} points</p>
    
    <h3>Redeem</h3>
    {% if points >= 100 %}
      <form action="/apps/loyalty/redeem" method="POST">
        <input type="hidden" name="customer_id" value="{{ customer.id }}">
        <label>
          <input type="number" name="redeem_points" min="100" max="{{ points }}" value="100">
          points
        </label>
        <button type="submit">Generate Code</button>
      </form>
    {% else %}
      <p>Earn {{ 100 | minus: points }} more points to redeem.</p>
    {% endif %}
  </div>
{% else %}
  <p><a href="/account/login">Log in</a> to view your points.</p>
{% endif %}
Include this snippet on a custom page or in the customer account template; the /apps/loyalty/redeem endpoint is handled by the custom app backend.
Loyalty

Redemption Engine

When a customer clicks redeem, creates a discount code pegged to their points spend, deducts the points from their account, and logs the redemption.

Custom app backend + Discount API
Admin GraphQL explorer graphql
# Admin GraphQL
mutation CreateDiscountCode($input: DiscountCodeAppInput!) {
  discountCodeAppCreate(codeAppDiscount: $input) {
    codeAppDiscount {
      startsAt
      endsAt
      title
      codes(first: 1) {
        edges {
          node {
            code
          }
        }
      }
    }
    userErrors {
      field
      message
    }
  }
}

# Variables:
# {
#   "input": {
#     "combinesWith": {"shippingDiscounts": false, "orderDiscounts": true},
#     "startsAt": "2024-01-15T00:00:00Z",
#     "endsAt": "2024-03-15T00:00:00Z",
#     "title": "POINTS-REDEEM-12345",
#     "codes": ["POINTS-REDEEM-12345"],
#     "customerGets": {
#       "value": {"amount": "50.00", "appliesOnEachItem": false}
#     }
#   }
# }
Fixed argument name from 'input' to 'codeAppDiscount', removed unsupported 'id' field, queried only available fields (startsAt, endsAt, title, codes).
Operations

Loyalty Dashboard

Admin view showing total points issued, redemptions by customer, and top earners — helps track loyalty health and spot trends.

Custom app dashboard
Admin GraphQL explorer graphql
# Admin GraphQL — fetch customers with points metafield
query GetLoyaltyMetrics($first: Int!) {
  customers(first: $first) {
    edges {
      node {
        id
        displayName
        email
        numberOfOrders
        points: metafield(namespace: "loyalty", key: "points_balance") {
          value
        }
        redemptions: metafield(namespace: "loyalty", key: "redemption_history") {
          value
        }
      }
    }
  }
}

# Variables:
# { "first": 50 }

# Note: redemption_history is a JSON string; parse on the app side to compute totals.
Store redemption history as JSON in loyalty.redemption_history metafield (array of {date, points_spent, code}); query and aggregate in the dashboard.

Got a similar problem?

Sketch your build in 30 seconds — voice, type, or attach a screenshot.

Sketch the build →