Developer Docs

Headless SDK

Add the full Anchor loyalty widget to any headless storefront: Hydrogen, Next.js, Gatsby, Vue, or custom builds.

Full widget experience
Lightweight footprint
Framework-agnostic
The Headless SDK is available on the Pro plan ($39.99/mo)

#Implement a loyalty program in Hydrogen: 30-minute quickstart

Adding a loyalty program to a Shopify Hydrogen storefront takes about 30 minutes with an API-first provider: roughly 10 minutes for environment variables and a signed-token loader, 15 minutes for the widget component and route wiring, and the rest for testing the logged-in and logged-out states.

The reason it is a 30-minute job rather than a multi-day one comes down to three architecture decisions. Get these right and the implementation is wiring; get them wrong and you end up rebuilding a loyalty UI from scratch.

1. Rendering: SDK component, not theme injection

Most Shopify loyalty apps deliver their customer-facing UI through the theme layer with script tags, app embeds, or theme app extensions. A Hydrogen storefront has no theme, so none of those injection points exist. Points keep accruing from order webhooks while every customer-facing surface silently disappears. On headless you need a widget you mount from your own component tree, or a JSON API plus a UI you build and maintain yourself.

2. Identity: resolve and sign server-side

Never send a raw customer ID from the browser to a loyalty API. Resolve the logged-in customer inside a Hydrogen loader with context.customerAccount.isLoggedIn() and a customer query, then sign the ID with a secret only your server holds. A proxy route that accepts an unsigned customer ID or email address is the most common security hole in headless loyalty implementations, and it leaks balances by email to anyone who guesses the endpoint. The full code is in Security, Customer Token.

3. Earning: order webhooks, not front-end events

Point accrual has to run server-side off order webhooks. Anything that depends on a pixel firing or a theme event will drift the moment a customer checks out in a different context, and drifted balances are far more expensive to fix than a missing widget.

The four steps

  1. 1
    Set your environment variables (5 min)

    Add PUBLIC_ANCHOR_LOYALTY_API_KEY and ANCHOR_SIGNING_SECRET to your Hydrogen environment. The API key is public and travels to the browser; the signing secret never leaves the server.

  2. 2
    Generate the signed customer token in your loader (5 min)

    HMAC-SHA256 over customerId.timestamp, returned from the loader alongside the rest of your route data. Tokens expire after five minutes, so regenerate on each page load. Copy-paste loader code is in Security.

  3. 3
    Mount the widget component (15 min)

    A small client component loads the SDK and calls AnchorLoyaltySDK.init() with the customer ID, the signed token, and your locale. Render it once in your root layout, not per route. Full component in Quick Start, and the ID-extraction detail in Getting the Customer ID.

  4. 4
    Test both states before you ship (5 min)

    Logged out: the widget should show the guest prompt and no balance. Logged in: balance renders and a redemption returns a discount code. Then confirm a redemption attempt with a tampered or expired token returns 403. If it succeeds, your token is not actually being verified.

Evaluating providers rather than implementing? The three decisions above are also the three questions worth asking any loyalty vendor before you commit: does the storefront API have documented authentication, is there an SDK or only a JSON API, and does earning run server-side. Our evaluation framework scores those alongside cost and migration, and the headless overview covers what breaks when they are missing.

#Overview

The Anchor Loyalty Headless SDK allows you to add the loyalty widget to any storefront that doesn't use Shopify's Online Store channel: including Hydrogen, Next.js, Gatsby, Vue, or any custom frontend.

The SDK renders the exact same widget as the embedded version: same UI, same features, lightweight footprint. No custom UI development required.

How it works

The SDK handles all communication with Anchor securely. You only need to provide your API key during initialization; the SDK takes care of the rest.

  • Anchor Loyalty app installed on your Shopify store
  • PRO plan subscription
  • API key generated from the Anchor Loyalty dashboard

#Authentication

All headless SDK API calls require an API key.

  1. 1Generate your API key, from the Anchor Loyalty dashboard (Headless SDK section).
  2. 2Save the key securely, it is displayed only once. If you lose it, use "Regenerate Key" to create a new one (this invalidates the old key).
  3. 3Include apiKey in your SDK initialization, call (see Quick Start below).
  4. 4Revoke and regenerate, if a key is compromised, revoke it from the dashboard. A new key can be generated immediately.
  5. 5Generate customer tokens, Your backend must sign a customer token using HMAC-SHA256 with your signing secret. The signing secret is shown alongside the API key when generated, store it securely in your backend environment variables. Required for all authenticated requests (reads and redemptions). See Security section.

#Security, Customer Token

The API key authenticates your store, but it's visible in your page source. To prevent unauthorized access, all authenticated headless requests require a customer token, an HMAC-signed proof that your backend verified the customer.

#How it works

  1. 1Customer logs in, on your storefront.
  2. 2Your backend generates a signed token, using HMAC-SHA256 with your signing secret (never send the signing secret to the browser).
  3. 3Frontend passes the token, to the SDK via the customerToken parameter.
  4. 4Anchor verifies the signature, before processing any request.

#Generating the token (backend)

Node.jsJS
const crypto = require('crypto');
const ts = Math.floor(Date.now() / 1000);
const hmac = crypto.createHmac('sha256', SIGNING_SECRET)
  .update(`${customerId}.${ts}`).digest('hex');
const customerToken = `${customerId}.${ts}.${hmac}`;
Hydrogen loaderTSX
// In a Hydrogen loader (e.g., app/routes/($locale)._index.tsx)
import crypto from 'crypto';

export async function loader({ context }: LoaderFunctionArgs) {
  const isLoggedIn = await context.customerAccount.isLoggedIn();
  let customerToken = null;

  if (isLoggedIn) {
    const { data } = await context.customerAccount.query(`
      query { customer { id } }
    `);
    const customerId = data.customer.id.replace('gid://shopify/Customer/', '');
    const ts = Math.floor(Date.now() / 1000);
    const hmac = crypto.createHmac('sha256', context.env.ANCHOR_SIGNING_SECRET)
      .update(`${customerId}.${ts}`).digest('hex');
    customerToken = `${customerId}.${ts}.${hmac}`;
  }

  return json({ customerToken });
}

Token expires after 5 minutes. Regenerate on each page load.

#When is it required?

EndpointTokenNote
POST /api/redeemREQUIREDReturns 403 without valid token
GET /api/customerREQUIREDReturns 403 without valid token
GET /api/codesREQUIREDReturns 403 without valid token

#Quick Start

#HTML / Custom Storefront

Recommended, only API key needed:

index.html, recommendedHTML
<script src="https://anchorloyalty.app/api/sdk.js?key=YOUR_API_KEY"></script>
<script>
  AnchorLoyaltySDK.init({
    apiKey: 'YOUR_API_KEY',
    customerId: '12345', // Shopify Customer ID (numeric)
    customerToken: 'TOKEN_FROM_YOUR_BACKEND', // HMAC-signed token
  });
</script>

When loading via ?key=, the SDK automatically resolves your store, no shop parameter needed. Your myshopify.com domain is never exposed in page source.

Alternative, with explicit shop domain:

index.html, alternativeHTML
<script src="https://anchorloyalty.app/api/sdk.js?shop=YOUR-STORE.myshopify.com"></script>
<script>
  AnchorLoyaltySDK.init({
    shop: 'YOUR-STORE.myshopify.com',
    apiKey: 'YOUR_API_KEY',
    customerId: '12345',
  });
</script>

#Hydrogen / React

Create a LoyaltyWidget component that loads the SDK and initializes it with the authenticated customer:

LoyaltyWidget.tsxTSX
import { useEffect } from 'react';
import { useCustomer } from '~/hooks/useCustomer';

const API_KEY = import.meta.env.PUBLIC_ANCHOR_LOYALTY_API_KEY;

export function LoyaltyWidget() {
  const customer = useCustomer();

  useEffect(() => {
    if (!API_KEY) {
      console.error('Missing PUBLIC_ANCHOR_LOYALTY_API_KEY env var');
      return;
    }
    const script = document.createElement('script');
    script.src = `https://anchorloyalty.app/api/sdk.js?key=${API_KEY}`;
    script.async = true;
    script.onload = () => {
      window.AnchorLoyaltySDK?.init({
        apiKey: API_KEY,
        customerId: customer?.id?.replace('gid://shopify/Customer/', '') || null,
        customerToken: customerToken, // Pass token from your backend
        customerName: customer?.firstName || '',
        locale: 'en',
      });
    };
    document.body.appendChild(script);

    return () => {
      window.AnchorLoyaltySDK?.destroy();
      script.remove();
    };
  }, [customer]);

  return null;
}

#Next.js

For Next.js, use the built-in next/script component with the onReady callback. This ensures the SDK re-initializes on SPA route transitions:

LoyaltyWidget.tsxTSX
'use client';
import Script from 'next/script';

const API_KEY = process.env.NEXT_PUBLIC_ANCHOR_LOYALTY_API_KEY;

export function LoyaltyWidget({ customerId, customerName, customerToken }: {
  customerId: string | null;
  customerName?: string;
  customerToken?: string;
}) {
  if (!API_KEY) {
    console.error('Missing NEXT_PUBLIC_ANCHOR_LOYALTY_API_KEY env var');
    return null;
  }
  return (
    <Script
      src={`https://anchorloyalty.app/api/sdk.js?key=${API_KEY}`}
      strategy="lazyOnload"
      onReady={() => {
        window.AnchorLoyaltySDK?.init({
          apiKey: API_KEY,
          customerId,
          customerName,
          customerToken,
        });
      }}
    />
  );
}

Why next/script? It handles deduplication (loads only once across navigations), supports lazyOnload for zero performance impact, and onReady re-fires on every mount, perfect for SPA route changes.

#Getting the Customer ID

The customerId must be the Shopify numeric Customer ID (not the GID).

#From Hydrogen (Customer Account API)

Hydrogen loaderTSX
// In a Hydrogen loader:
const isLoggedIn = await context.customerAccount.isLoggedIn();
if (isLoggedIn) {
  const { data } = await context.customerAccount.query(`
    query { customer { id firstName lastName } }
  `);
  // data.customer.id = "gid://shopify/Customer/12345"
  const numericId = data.customer.id.replace('gid://shopify/Customer/', '');
  // numericId = "12345"
}

#From Storefront API

Storefront API QueryGraphQL
query {
  customer(customerAccessToken: "TOKEN") {
    id  # Returns "gid://shopify/Customer/12345"
  }
}

Extract the numeric part: id.split('/').pop()"12345"

#From Custom Backend (Admin API)

Admin APIHTTP
GET /admin/api/2026-01/customers.json?email=user@example.com

Use the id field from the response (already numeric).

#Guest Users (Not Logged In)

Pass null or omit customerId:

Guest ModeJS
AnchorLoyaltySDK.init({
  apiKey: 'YOUR_API_KEY',
  customerId: null, // Guest mode, shows sign-in prompt
});

#All Parameters

ParameterTypeDefaultDescription
apiKeystringrequiredAPI key from your Anchor Loyalty dashboard
shopstringauto-resolvedYour *.myshopify.com domain. Auto-resolved from API key when SDK is loaded via ?key=
customerIdstringnullShopify Customer ID (numeric). Omit or pass null for guest mode
customerTokenstringnullHMAC-signed customer verification token. Required for all authenticated requests (reads and redemptions). Generated by your backend: HMAC-SHA256(customerId.timestamp, signingSecret). See Security section below.
customerNamestring''Customer's first name for welcome message
localestring'en'Language code. 32 locales built-in (see below)
positionstring'bottom-right''bottom-right' or 'bottom-left'
primaryColorstring'#4A56E2'Primary brand color (hex)
backgroundColorstring'#FFFFFF'Widget background color
textColorstring'#1F2937'Widget text color
launcherStylestring'bubble''bubble', 'bubble-text', 'icon-only', 'pill', 'square'
launcherGradientstring'solid''solid', 'sunset', 'ocean', 'forest', 'candy', 'midnight'
launcherShadowstring'medium''none', 'subtle', 'medium', 'strong', 'glow'
launcherAnimationstring'pulse''none', 'pulse', 'bounce', 'shake', 'glow'
launcherTextTypestring'static''static' (shows 'Rewards') or 'dynamic' (shows points)
animationDelaynumber5Seconds before first animation
animationRepeatnumber30Seconds between repeats (0 = no repeat)
animateOnScrollbooleantrueAnimate when user scrolls 25% down
bannerImagestring''URL to hero banner image
bannerLinkstring''URL the banner links to
bannerLinkNewTabbooleanfalseOpen banner link in new tab
showRewardsbooleantrueShow rewards tab
showReferralbooleantrueShow referral tab
showGuestMessagebooleantrueShow sign-in prompt for guests
accountUrlstring'/account'"View Account" link destination
loginUrlstring'/account/login'"Sign In" link for guests
registerUrlstring'/account/register'"Create Account" link for guests
translationsobject{}Override any default translation key

#Locale Support

32 built-in locales, set locale to auto-select:

Locale exampleJS
AnchorLoyaltySDK.init({ apiKey: '...', locale: 'de' }); // Deutsch
32 Supported locales
arالعربية
csČeština
daDansk
deDeutsch
elΕλληνικά
enEnglish
esEspañol
fiSuomi
frFrançais
fr-CAFrançais (CA)
heעברית
hiहिन्दी
huMagyar
idBahasa Indonesia
itItaliano
ja日本語
ko한국어
msBahasa Melayu
nlNederlands
noNorsk
plPolski
pt-BRPortuguês (BR)
pt-PTPortuguês (PT)
roRomână
ruРусский
svSvenska
thไทย
trTürkçe
ukУкраїнська
viTiếng Việt
zh-CN简体中文
zh-TW繁體中文

RTL support: Arabic (ar) and Hebrew (he) locales automatically switch the widget layout to right-to-left, including text direction, icon positions, and animations.

Override individual keys:

Override specific keysJS
AnchorLoyaltySDK.init({
  apiKey: '...',
  locale: 'fr',
  translations: { title: 'Mon Programme Fidélité' },
});

#Methods

AnchorLoyaltySDK.init(options)

Initialize the widget. Call once after the script loads.

AnchorLoyaltySDK.destroy()

Remove the widget completely. Cleans up:

  • - Widget DOM element
  • - Injected CSS and JS
  • - All window globals (anchorLoyaltyConfig, anchorLoyaltyTranslations, anchorLoyalty)

Safe for SPA route transitions.

AnchorLoyaltySDK.update(newOptions)

Update options and re-initialize (e.g., after login/logout). Preserves custom translations.

Example: Update after loginJS
// After user logs in:
AnchorLoyaltySDK.update({
  customerId: '12345',
  customerName: 'Jane',
});

#Architecture

Your Headless Storefront                    Anchor Loyalty API
(Hydrogen, Next.js, etc.)                   (anchorloyalty.app)
        |                                            |
        |  1. Load SDK                               |
        |  GET /api/sdk.js?key=ak_xxx                |
        |------------------------------------------->|
        |  <-- JS (widget + CSS + 32 locales)        |
        |                                            |
        |  2. Fetch customer data (automatic)        |
        |  GET /api/customer                         |
        |------------------------------------------->|
        |  <-- JSON (points, rewards, tier, etc.)    |
        |                                            |
        |  3. Redeem reward                          |
        |  POST /api/redeem                          |
        |------------------------------------------->|
        |  <-- JSON (discount code)                  |
        |                                            |
        |  4. Redeem reward (with customer token)    |
        |  POST /api/redeem                          |
        |  Headers: X-Anchor-Customer-Token:         |
        |    {customerId}.{ts}.{hmac}                |
        |------------------------------------------->|
        |  <-- JSON (discount code)                  |
Authentication
API key sent automatically by SDK
Signing Secret
Used server-side only for customer token HMAC (never sent to browser)
CORS
Enabled for all origins
Caching
SDK file cached 1hr (CDN)
Rate limits
300 req/min per IP, 600 req/min per shop
Customer Token
HMAC-signed proof for all authenticated requests

#Troubleshooting

IssueSolution
SDK returns 403Verify API key is correct and not revoked. Check PRO plan is active.
Widget doesn't appearCheck browser console for errors
"Customer not logged in"Verify customerId is the numeric Shopify ID, not the GID
Wrong languageSet locale parameter (e.g., 'de', 'fr', 'ja')
Widget overlaps UIAdjust position or use custom CSS
CORS errorsVerify you're loading SDK from anchorloyalty.app, not from a local file
Redemption returns 403 "Invalid or expired customer token"Verify your backend generates the token correctly. Check timestamp is within 5 minutes. Ensure customerId in token matches the request.
"API key required" on redemptionInclude the API key via X-Anchor-Api-Key header or api_key query param alongside the customer token.
"Signing secret not configured"Regenerate your API key from the dashboard. Both API key and signing secret will be shown once, store the signing secret in your backend environment variables.

Ready to add loyalty to your headless store?

Install Anchor, upgrade to Pro, and drop the SDK into your storefront. Full widget experience in minutes.