Behio Storefront SDK
Customer

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.

On this page