io.tt SDK Docs
GuidesExperiences

Loyalty

Implementing Stamp Cards & Claim Rules with the io.tt SDK

The Loyalty template enables product‑based stamp collection, allowing customers to scan product codes (QR/NFC) to earn "stamps" toward a reward.

The SDK exposes four Loyalty methods:

  • getItemClaimStatus
  • registerLoyaltyUser
  • claimLoyaltyItem
  • getStampHistory

This guide describes how to implement a complete loyalty card system, including per‑code claiming rules, scan cooldowns, and reward unlock behaviour. All experience rules (claim limits, cooldowns, number of stamps required) are configured in the io.tt dashboard.


How Loyalty Experiences Work

A Loyalty experience is fully configured inside the io.tt dashboard, including:

SettingMeaning
Number of StampsThe number of stamps required to fill 1 card
Item Claim LimitThe number of users that may claim the same code
Rescan LimitThe number of times the same user can scan the same code
Rescan Time ThrottleThe amount of time a user must wait before rescanning the same code
Card Claim LimitThe number of times the same user can start a new card
Card Time ThrottleThe amount of time a user must wait before starting a new card

These rules are enforced server‑side and returned through SDK responses.


Typical Loyalty Flow

  1. User scans a product → app gets itemSlug
  2. Check whether the code can be claimed → getItemClaimStatus
  3. If user not yet registered → registerLoyaltyUser
  4. Claim the stamp → claimLoyaltyItem
  5. Fetch updated stamp card → getStampHistory
  6. Render card progression
  7. If card complete → show configured reward

1. Initialise the SDK

Create a single SDK instance using your Experience API key. If you haven't set this up yet, see Getting Started.

import { IOTT } from "@io-tt/sdk";

const sdk = new IOTT({
  apiKey: process.env.IOTT_API_KEY,
});

2. Check Eligibility (getItemClaimStatus)

Before collecting any user details, check whether this specific product code is eligible to earn a stamp. This lets you surface an appropriate message early — for example, if the code has already been claimed by the maximum number of users — before asking the consumer to register or log in.

const status = await sdk.getItemClaimStatus(EXPERIENCE_ID, itemId);

// status.claimed — whether this code has already been claimed

The returned ItemClaimStatus object includes:

FieldTypeDescription
idstringInternal ID of the claim item
createdAtstringISO 8601 timestamp of when the item was created
claimedbooleanWhether this code has already been claimed

3. Register Loyalty User (registerLoyaltyUser)

If the user is not yet registered, register them before claiming. This creates a loyalty profile linked to their email address and returns their initial stamp history. Email is required for loyalty because it serves as the persistent identifier used to track stamps across multiple scans and sessions.

const history = await sdk.registerLoyaltyUser(
  EXPERIENCE_ID,
  {
    itemSlug,                // required
    email,                   // required — email is required for loyalty and also serves as the
                             // identifier for `claimLoyaltyItem` and `getStampHistory`
    // Personal details (all optional)
    title,
    firstName,
    lastName,
    gender,                  // 'male' | 'female' | 'other'
    phoneNumber: { countryCode: 44, number: '7700900000' },
    dateOfBirth,             // ISO 8601 string, e.g. "1990-01-15"
    // Address (all optional)
    addressLine1,
    addressLine2,
    locality,
    administrativeDivision,
    postalCode,
    country,
    // Consent (all optional)
    marketingConsent,
    profilingConsent,
    // Custom fields (optional)
    extra: {
      // additional custom fields
    },
  },
  images,                    // optional: Array<File | Blob | Buffer>
);

Only itemSlug and email are required. All other entry fields and the images argument are optional.

Registration data is automatically routed to your configured CRM. See the CRM Integration Guide for details on field mapping and how extra fields are handled.

Proof of Purchase

To attach structured proof-of-purchase data (e.g. receipt fields), pass proofOfPurchaseData inside the entry object. This data is stored separately in io.tt and linked to the participant record — it is not forwarded to native CRM integrations (Salesforce or Klaviyo), but is included in custom webhook payloads and is available in the io.tt dashboard for review.

const history = await sdk.registerLoyaltyUser(
  EXPERIENCE_ID,
  {
    itemSlug,
    email,
    proofOfPurchaseData: [   // optional: ProofOfPurchaseItem[]
      {
        type: 'image',       // required — free-form string identifying the field type
        label: 'Receipt',    // required — human-readable label
        value: imageBlob,    // required — Blob for images
        itemId: 'abc',       // optional — links to a specific item
      },
      {
        type: 'text',
        label: 'Barcode',
        value: '123456789',  // string for text values
      },
    ],
  },
);

For image uploads alongside proof-of-purchase data, pass the files as the third images argument as shown above.


4. Claim the Stamp (claimLoyaltyItem)

Once the user is registered and the code is confirmed eligible, claim the stamp. The server validates all configured rules — rescan limits, cooldown periods, and item claim limits — before accepting the claim.

await sdk.claimLoyaltyItem(EXPERIENCE_ID, itemSlug, email);

5. Fetch Stamp History (getStampHistory)

After a successful claim, fetch the updated stamp card. This gives you the full list of stamps earned so far, which you can use to render the card progression UI and determine whether the user has reached the reward threshold.

const history = await sdk.getStampHistory(EXPERIENCE_ID, email);

Example response:

{
  "stamps": [
    { "createdAt": "2025-01-10T10:00:00Z" },
    { "createdAt": "2025-01-12T12:00:00Z" }
  ]
}

6. Reward Unlock

Once the required stamp count is reached, show the reward screen. The required stamp count is set in the io.tt dashboard when the experience is configured — read it from your experience configuration rather than hardcoding it.

const requiredStamps = 6; // configured in the dashboard

if (history.stamps.length >= requiredStamps) {
  renderRewardScreen();
}

Summary Table

API MethodPurpose
getItemClaimStatusDetermines whether a scan can earn a stamp
registerLoyaltyUserRegisters a new user and returns initial stamp history
claimLoyaltyItemClaims a stamp for the user against a product code
getStampHistoryFetches the full list of the user's stamps

On this page