Prize Draw
The Prize Draw template lets consumers enter a competition by scanning a QR/NFC code on a product. The server enforces all competition rules — availability window, duplicate submissions, per-scan uniqueness, and entry limits — so your frontend only needs to handle the UI states the SDK returns.
Overview
A typical Prize Draw experience follows this sequence:
- User scans the product → your app receives an
itemSlugfrom the URL query string - Call
getCompetitionStatusto confirm the competition is open - Collect the user's email and call
checkCompetitionEntryto verify they haven't already entered - If eligible, display the signup form
- On submit, call
signupForCompetition - Show a confirmation state
- Handle closed, duplicate, or invalid states as needed
Step-by-Step Implementation
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 Competition Status
Before showing any UI, confirm the competition is currently open. This allows you to route the user to an appropriate closed or not-yet-started screen rather than displaying a form against an inactive experience. The server enforces the availability window, but checking status first avoids unnecessary API calls and gives you the startDate / endDate values needed to render helpful messaging.
const status = await sdk.getCompetitionStatus(EXPERIENCE_ID);
if (!status.isActive) {
renderCompetitionEnded();
return;
}The full CompetitionStatus object includes:
| Field | Type | Description |
|---|---|---|
isActive | boolean | Whether the competition is currently active |
hasEnded | boolean | Whether the competition has ended |
startDate | Date | null | Scheduled start date |
endDate | Date | null | Scheduled end date |
allowMultipleEntries | boolean | Whether a user may enter more than once |
enforceUniqueScan | boolean | undefined | Whether each physical scan must be unique |
3. Check if the User Has Already Entered
Before showing the form, check whether this user has already submitted an entry. This prevents showing a form the server will reject and lets you display a more appropriate "already entered" state instead.
uid is the unique identifier you use to track this user — typically their email address, but it can be any consistent string.
const entry = await sdk.checkCompetitionEntry(EXPERIENCE_ID, uid);
// Optionally pass itemSlug as a third argument to scope the check to a specific product scan
// const entry = await sdk.checkCompetitionEntry(EXPERIENCE_ID, uid, itemSlug);
if (entry.hasEntered) {
renderAlreadyEnteredState();
return;
}4. Submit a Competition Entry
Once the user is confirmed eligible, collect their details and submit the entry. Only uid is required — all other fields are optional and should only be collected if your experience form asks for them.
await sdk.signupForCompetition(
EXPERIENCE_ID,
{
uid, // required — you provide this value, typically the user's email.
// io.tt uses it to detect whether this person has already entered.
email, // if using email as the uid, pass the same value here too
title, // must match Salesforce picklist exactly if using Salesforce: Mr. Ms. Mrs. Dr. Prof.
firstName,
lastName,
gender, // 'male' | 'female' | 'other'
phoneNumber: { countryCode: 44, number: '7700900000' },
dateOfBirth, // ISO 8601 string, e.g. "1990-01-15"
addressLine1,
addressLine2,
locality,
administrativeDivision,
postalCode,
country,
marketingConsent,
profilingConsent,
// Proof of purchase (optional — see note below)
proofOfPurchaseData: [
{
type: 'image', // free-form string identifying the field type
label: 'Receipt',
value: imageBlob, // Blob for images
},
{
type: 'text',
label: 'Barcode',
value: '123456789', // string for text values
itemId: 'abc', // optional — links to a specific item
},
],
// Custom fields (optional)
extra: {
customProperty: formData.customField1,
},
},
images, // optional: Array<File | Blob | Buffer>
);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
proofOfPurchaseData is stored separately in io.tt and linked to the participant record. It is not forwarded to native CRM integrations (Salesforce or Klaviyo) — it remains within io.tt's infrastructure where it can be reviewed in the dashboard and used for fraud prevention and prize fulfilment workflows. If you are using a custom webhook integration, the full proofOfPurchaseData array is included in the webhook payload for downstream processing.
5. Render the Confirmation State
After a successful submission, show the user appropriate confirmation. All competition rules (entry limits, duplicate checks) are enforced server-side — a successful response from signupForCompetition means the entry has been accepted.
renderSuccessConfirmation();Your UI may include:
- A success message and entry confirmation
- Draw date and how winners will be notified
- Links to Terms & Conditions and Privacy Policy
Summary
| Method | Signature | Purpose |
|---|---|---|
getCompetitionStatus | getCompetitionStatus(experienceId: string): Promise<CompetitionStatus> | Confirms the competition is open and returns availability window and entry rule flags |
checkCompetitionEntry | checkCompetitionEntry(experienceId: string, uid: string, itemSlug?: string): Promise<CompetitionEntryStatus> | Returns whether this user has already entered, optionally scoped to a specific product scan |
signupForCompetition | signupForCompetition(id: string, entry: Omit<CompetitionEntry, 'images'>, images?: Array<File | Blob | Buffer>): Promise<void> | Submits the entry and validates all server-side rules |