io.tt SDK Docs
GuidesExperiences

Scan And Register

The Foundational Connected Packaging Experience

The Scan & Register template is the foundational connected packaging experience. A consumer scans a QR or NFC code on a product, the code is verified as genuine, and the consumer is invited to register their details. It is the simplest experience type and often serves as the base layer for campaigns where the primary goal is consumer registration and CRM data capture — without the complexity of prize mechanics, loyalty programmes, or gifting.

This guide covers the four Scan & Register methods:

  • getBasicExperienceStatus
  • checkBasicExperienceItem
  • checkBasicExperienceEntry
  • signupForBasicExperience

Overview of the Scan & Register Flow

  1. Consumer scans a product → your app receives an itemSlug from the URL query string
  2. Check whether the experience is currently active → getBasicExperienceStatus
  3. Verify the scanned code belongs to this campaign → checkBasicExperienceItem
  4. Strip the id= parameter from the URL (recommended — see Security Note)
  5. Optionally check whether this consumer has already registered → checkBasicExperienceEntry
  6. Collect consumer details → signupForBasicExperience
  7. Show confirmation

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. Extract the Item ID from the URL

When a consumer scans an io.tt QR code, the redirector appends the item identifier as an id= query parameter. Extract it before making any SDK calls:

const params = new URLSearchParams(window.location.search);
const rawId = params.get("id");
const itemSlug = rawId?.startsWith("!") ? rawId.slice(1) : rawId;

if (!itemSlug) {
  renderInvalidCodeScreen();
  return;
}

3. Check Experience Status

Before showing any content, confirm the experience is currently active. This allows you to route the consumer to an appropriate screen if the experience has ended or hasn't started yet, rather than displaying an entry form against an inactive campaign.

const status = await sdk.getBasicExperienceStatus(EXPERIENCE_ID);

if (!status.isActive) {
  if (status.hasEnded) {
    renderExperienceEndedScreen();
  } else {
    renderNotYetAvailableScreen(status.startDate);
  }
  return;
}

The returned BasicExperienceStatus object includes:

FieldTypeDescription
isActivebooleanWhether the experience is currently active
hasEndedbooleanWhether the experience has ended
startDateDate | nullScheduled start date — null if always-on
endDateDate | nullScheduled end date — null if always-on

Experiences with no start or end date configured are always-on.


4. Verify the Scanned Code

This is the core step. The server checks that the scanned item belongs to one of the batches associated with this experience — confirming the code is genuine and part of this campaign. Codes from other campaigns or unknown codes both return authorised: false; the response is intentionally indistinguishable between the two cases.

const auth = await sdk.checkBasicExperienceItem(EXPERIENCE_ID, itemSlug);

if (!auth.authorised) {
  renderInvalidCodeScreen();
  return;
}

The response:

FieldTypeDescription
authorisedbooleanWhether the item belongs to this experience's batches

Security Note: Strip the id= Parameter

Once the item is authorised, remove the id= parameter from the browser URL before rendering any content. This prevents a consumer from sharing the full URL with someone else — a recipient arriving without the id= parameter will fail the authorisation check.

const url = new URL(window.location.href);
url.searchParams.delete("id");
window.history.replaceState({}, "", url.toString());

This does not reload the page. It only updates the address bar.


5. Check If the Consumer Has Already Registered (Optional)

Before showing the registration form, you can check whether this consumer has already submitted an entry. This prevents showing a form they've already completed and lets you display a more appropriate "you're already registered" state instead. This step requires you to have collected the consumer's identifier (typically their email) first.

const entryCheck = await sdk.checkBasicExperienceEntry(EXPERIENCE_ID, uid);

if (entryCheck.hasEntered) {
  renderAlreadyRegisteredScreen();
  return;
}

6. Submit Registration

Once the consumer is confirmed eligible, collect their details and submit the registration. Only uid is required — all other fields are optional and should only be collected if your experience form asks for them. Registration data is automatically routed to your configured CRM.

await sdk.signupForBasicExperience(
  EXPERIENCE_ID,
  {
    uid,                     // required — unique identifier, typically email
    email,
    // 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: {
      // any additional fields — passed through to your configured CRM
    },
  },
  images,                    // optional: Array<File | Blob | Buffer>
);

Only uid is required. All other fields are optional.

When using email as the uid (the most common pattern), pass the same value to both uid and email. See the CRM Integration Guide for how these fields are routed to your configured CRM.


7. End-to-End Example

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

const sdk = new IOTT({ apiKey: process.env.IOTT_API_KEY });
const EXPERIENCE_ID = "your-experience-id";

async function handleBasicExperience(userEmail: string) {
  // 1. Extract item ID from URL
  const params = new URLSearchParams(window.location.search);
  const rawId = params.get("id");
  const itemSlug = rawId?.startsWith("!") ? rawId.slice(1) : rawId;

  if (!itemSlug) {
    renderInvalidCodeScreen();
    return;
  }

  // 2. Check experience status
  const status = await sdk.getBasicExperienceStatus(EXPERIENCE_ID);
  if (!status.isActive) {
    renderExperienceEndedScreen();
    return;
  }

  // 3. Check item authorisation
  const auth = await sdk.checkBasicExperienceItem(EXPERIENCE_ID, itemSlug);
  if (!auth.authorised) {
    renderInvalidCodeScreen();
    return;
  }

  // 4. Strip id= from URL
  const url = new URL(window.location.href);
  url.searchParams.delete("id");
  window.history.replaceState({}, "", url.toString());

  // 5. Check for existing registration
  const entryCheck = await sdk.checkBasicExperienceEntry(EXPERIENCE_ID, userEmail);
  if (entryCheck.hasEntered) {
    renderAlreadyRegisteredScreen();
    return;
  }

  // 6. Submit registration
  await sdk.signupForBasicExperience(EXPERIENCE_ID, {
    uid: userEmail,
    email: userEmail,
    firstName: "Jane",
    lastName: "Smith",
    marketingConsent: true,
    extra: {
      productVariant: "premium-reserve",
      retailer: "harrods-london",
    },
  });

  renderConfirmationScreen();
}

8. Summary Table

MethodSignaturePurpose
getBasicExperienceStatus(experienceId)Check whether the experience is active.
checkBasicExperienceItem(experienceId, itemSlug)Verify the scanned code belongs to this campaign's batches.
checkBasicExperienceEntry(experienceId, uid)Check whether this consumer has already registered.
signupForBasicExperience(experienceId, entry, images?)Submit the consumer's registration details.

Notes

  • Authorisation is scoped strictly to the batches linked to this experience — codes from other campaigns or other organisations are rejected.
  • All validation rules are enforced server-side. Treat SDK responses as the source of truth.
  • See the CRM Integration Guide for how registration data is routed to Salesforce, Klaviyo, and other platforms.

On this page