Loyalty
Read the customer's loyalty points, tier and program terms
Loyalty Summary
Requires customer authentication. Returns the shop's loyalty program terms and the logged-in customer's membership:
const { data } = await client.customer.getLoyalty();
// {
// hasProgram: boolean, // false = shop has no active program, hide the section
// enrolled: boolean, // false = show "join and earn" with program terms
// program: LoyaltyProgram | null, // pointName, pointValueRatio, currency, ...
// balance: LoyaltyBalance | null, // currentPoints, currentPointsValue, lifetime stats
// currentTier: LoyaltyTier | null, // name, slug, color, earnMultiplier, perks
// nextTier: LoyaltyNextTier | null, // threshold progress towards the next tier
// referralCode: string | null,
// transactions: LoyaltyTransaction[], // recent point movements
// transactionsNextCursor: string | null,
// canApplyReferral: boolean,
// }LoyaltyTier.perks is { list: string[] } | null with human-readable perk lines to render on the tier card.
balance.currentPoints is available to spend. balance.pointsDebt separately
records an adjustment after a refunded reward was already spent. New credits
cover that adjustment first. Display both values; do not subtract the adjustment
again from the available balance.
For an order fully covered by gift cards or points, the recorded cash refund is zero. A partial return restores only the share of redeemed credit represented by the returned items at their original order prices. If an inspected quantity is recorded, it takes precedence over the claimed quantity. Saving the same return again does not restore the credit twice. Returning all items restores the full redeemed balance; cancelling or explicitly refunding the whole order does too.
Older point history
SDK 2.9 supports all history pages, with 20 entries by default and up to 50 per request. Render the initial page on the server. Use the returned cursor for a server-rendered link to older entries; a new point credit will not repeat entries from an earlier page.
if (previousPage.transactionsNextCursor) {
const {data, error} = await client.customer.getLoyalty({
transactionsCursor: previousPage.transactionsNextCursor,
transactionsLimit: 20,
});
}Only request another page when transactionsNextCursor is non-null. A missing
or foreign cursor returns an error; show a link back to the latest history.
All loyalty responses are private and must not enter a shared cache.
Apply an invitation
const {data, error} = await client.customer.applyReferral(code);
// data: {alreadyApplied: boolean, recipientPoints: number}Use a POST form when canApplyReferral is true. Both customers must belong to
the same shop and the active program; the recipient must already be enrolled.
Invite-only membership is not bypassed by a referral. Self-referral is rejected.
One customer can accept one invitation per program, including when only the
sender receives points. Repeated and concurrent submissions do not award points
twice. Refresh the balance and history after success. A zero recipient reward
can be valid when the merchant only rewards the sender.
Redeeming Points
Show the available balance on the checkout and pass the chosen amount as redeemLoyaltyPoints in checkout.createOrder. The backend validates the redemption against the real balance, the program's minRedemptionPoints and maxRedemptionPercent, so never trust the UI value.
React Hook
import { useLoyalty } from '@behio/storefront-sdk/react';
const { data, isLoading } = useLoyalty();
if (!data?.hasProgram) return null;Badges and challenges
SDK 2.9 adds an authenticated account view. Fetch it on the server using the customer's HTTP-only session. An API error needs a visible retry; it is not an empty rewards list.
const {data, error} = await client.customer.getGamification({
badgesPage: 1,
challengesPage: 1,
limit: 20, // 1..50; independent pagination for both collections
});
await client.customer.joinChallenge(challengeId);
await client.customer.refreshGamification();badges contains earned badges, including badges that can no longer be earned.
challenges contains currently available challenges and the customer's own
previous enrollments. Render canJoin, joined, active, currentProgress,
completedAt, the time window and the actual reward. Joining twice keeps the
original enrollment time. An inactive challenge cannot be joined.
Orders count only when paid and in an active fulfillment state. A joined
challenge counts activity created after enrollment and within its configured
window. TOTAL_SPEND uses the shop's default currency, converting paid totals
with the stored reference rates, without pricing margins. Missing rates stop
reconciliation instead of relabeling the currency. Approved reviews and credited
sender referrals provide the other metrics. Earned badges and completed
challenges remain earned; a later refund does not revoke an already awarded
badge. Uncompleted progress is recalculated from current qualifying activity.
Payment, review approval and customer events enqueue a separate retryable
rewards job. refreshGamification() is an explicit recovery action that checks
persisted activity; callers never supply progress or points. Completion, its
badge and reward points share one transaction. A promised point reward waits
for a membership in the active loyalty program, rather than marking an unpaid
reward as delivered. This refresh action is a mutation, so use a POST-backed
form, not a GET link.
The response does not include another customer's identity, internal conditions, participant counts or private configuration. Show merchant descriptions as plain text and badge icons only through an HTTP(S) image component with a fallback.
A challenge with rewardMembershipRequired: true is waiting for an active loyalty membership. Display this reason and do not offer enrollment when canJoin is false. Existing completed rewards stay completed. Earned badges and challenge responses are private and use Cache-Control: private, no-store.