> ## Documentation Index
> Fetch the complete documentation index at: https://developers.loyaltylion.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Free product rewards

You can use the Headless API with Shopify Hydrogen to implement product rewards
which are added directly to the cart for free.

Unlike other kinds of reward, free products require updates to your cart, which
is done using the Storefront API, so they require a few more steps to implement.

Our [reference
implementation](https://github.com/loyaltylion/hydrogen-reference-store/pull/1)
includes a full implementation of free product rewards, including them in the
Cart sidebar. Even if you use our reference implementation, we recommend reading
the rest of this guide to understand the steps involved.

## Prerequisites

### Ensure you're using Shopify Functions for free products

Some older LoyaltyLion accounts may be using Shopify Scripts to implement free
products. You must migrate to Shopify Functions in order to use free product
rewards with the Headless API.

If you're still using Shopify Scripts (or you're not sure), [get in
touch](support@loyaltylion.com) and we can help you migrate over to Shopify
Functions.

### Add a Buyer Identity to the Cart

Free product rewards only work if the cart has a buyer identity, to associate the
current logged in customer with the current cart.

If you're using Hydrogen, you can do this using the provided helpers. In our
reference implementation, we do this in the `root` loader inside `loadCriticalData`.

```typescript theme={null}
export async function loader(args: LoaderFunctionArgs) {
  const { context } = args;

  if (await context.customerAccount.isLoggedIn()) {
    const buyer = await context.customerAccount.getBuyer();
    await context.cart.updateBuyerIdentity({
      customerAccessToken: buyer.customerAccessToken,
    });
  }
}
```

## Implementation

### Initialize a customer session with your current cart

Use the [Initialize Session](/headless-api/2025-06/customers/initialize-session) API
to initialize a customer session and pass along your current cart.

We use your cart to generate the response, for example any product rewards
which have requirements like a minimum cart total will indicate they are not
available to redeem in the response based on the provided cart.

In our reference implementation, we do this using a deferred API call in the
`root` loader. This pending data is then passed down to the `Cart`
component and awaited in a `<Suspense>` wrapper.

```tsx theme={null}
return (
  <Suspense fallback={<p>Loading rewards...</p>}>
    <Await resolve={loyaltylion}>
      {(loyaltylion) => {
        if (!loyaltylion) {
          return null;
        }
      }}
    </Await>
  </Suspense>
);
```

### Check if the customer is enrolled

Confirm that the customer is enrolled by checking that `customer.state ==
"enrolled"`. Customers in any other state are not eligible to redeem rewards.

### Render free product rewards available to the customer

Search for any applicable rewards in the `customer.available_rewards` array.
Free product rewards will have `kind == "product_cart"`.

Next, iterate each reward and render it along with an action button to redeem the reward. You'll
need to check each reward's `context.can_redeem.state` to determine if the customer can redeem the reward,
and, if they can't, the reason why, which is useful to display to the customer.

```tsx theme={null}
const fetcher = useFetcher();
const isLoading = fetcher.state !== 'idle';

const canRedeem = reward.context.can_redeem.state === 'redeemable';

const redeemButtonText = (() => {
  switch (reward.context.can_redeem.state) {
    case 'redeemable':
      return `Redeem for ${reward.variant.cost_text}`;
    case 'claim_limit_reached':
      return 'Limit reached';
    case 'insufficient_points':
      return `${reward.context.can_redeem.additional_points_required} more points needed`;
    case 'cart_requirements_not_met':
      return 'Cart requirements not met';
    case 'max_redemptions_for_cart_reached':
      return 'Already redeemed';
    default:
      return 'Unavailable';
  }
})();

return (
  <div key={reward.id}>
    <div>
      {reward.properties.product.image_url ? (
        <img src={reward.properties.product.image_url} alt="" width="64" />
      ) : (
        <div className='image-placeholder' />
      )}
    </div>
    <div>
      <h4>{reward.variant.title}</h4>
      <div>
        <fetcher.Form
          method="post"
          action={`/loyaltylion/redeem-free-product/${reward.id}`}
        >
          <input
            type="hidden"
            name="variant_id"
            value={reward.properties.product.variants[0].id}
          />
          <button type="submit" disabled={!canRedeem || isLoading}>
            {isLoading ? 'Loading...' : redeemButtonText}
          </button>
        </fetcher.Form>
      </div>
    </div>
  </div>
);
```

### Handle the redemption of a free product reward

When the customer clicks the redeem button, you'll need to submit an action. Inside
your server-side action handler,

* use the [Redeem Free
  Product](/headless-api/2025-06/reward-redemption/free-product) endpoint to
  redeem the reward
* if successful, the response will contain an `active_cart_redemption.cart_line`
  object, which includes the necessary information you need to add to the cart
  using the Storefront API

In our reference implementation, we do this in the
[loyaltylion.redeem-free-product.\$id.tsx](https://github.com/loyaltylion/hydrogen-reference-store/blob/integrate-loyaltylion/app/routes/loyaltylion.redeem-free-product.%24id.tsx)
route.

Once the action is successful, Hydrogen/Remix should automatically re-run
the relevant loaders, which will update both the cart and LoyaltyLion data,
triggering an automatic UI update.

```tsx theme={null}
export async function action({params, request, context}: ActionFunctionArgs) {
  if (!(await context.customerAccount.isLoggedIn())) {
    return null;
  }

  const {data} = await context.customerAccount.query(`
    query { customer { id } }
  `);

  if (!data.customer || !params.id) {
    return null;
  }

  const rewardId = parseInt(params.id);
  const variantId = (await request.formData()).get('variant_id') as string;
  const cartId = context.cart.getCartId();

  if (!cartId) {
    throw new Error('Cannot redeem free product reward without a cart');
  }

  const res = await createLoyaltyLionClient(context).rewards.redeemProductCart({
    cart_id: cartId,
    customer_merchant_id: data.customer.id,
    reward_id: rewardId,
    variant_id: variantId,
  });

  if (!res.data) {
    console.error('Failed to redeem product reward', {
      data: res.data,
      error: res.error,
    });
    throw new Error('Request failed');
  }

  const cartLine = res.data.active_cart_redemption.cart_line;

  await context.cart.addLines([
    {
      merchandiseId: cartLine.merchandise_id,
      quantity: cartLine.quantity,
      attributes: cartLine.attributes,
    },
  ]);
}
```

### Handle requested cart actions

When you call [Initialize Session](/headless-api/2025-06/customers/initialize-session)
and provide your current cart, will check the cart for any expired or invalid
product redemptions and automatically cancel them and refund the points.

If this happens, we'll return a list of [`requested_cart_actions`](/headless-api/2025-06/customers/initialize-session#response-requested-cart-actions) in the response.

These actions indicate the changes, if any, that need to made to the cart. Currently
this is limited to *removing* lines from the cart, because we've cancelled their
associated redemption. This might be because the redemption expired, or because
its requirements are no longer met by the cart (e.g. the cart total is too low).

In our reference implementation, we [stash these requested line IDs](https://github.com/loyaltylion/hydrogen-reference-store/blob/integrate-loyaltylion/app/lib/loyaltylion/fetchLoyaltylionData.ts#L102) to remove on
the `LoyaltyLionData` object, pass them down to the `Cart` component, and then
[use a hook](https://github.com/loyaltylion/hydrogen-reference-store/blob/integrate-loyaltylion/app/lib/loyaltylion/useLoyaltylionCartUpdates.ts) which will remove the lines from the cart by submitting an action.
