Gifting
Implementing Product-Linked Greeting Messages with the io.tt SDK
The Gifting template allows a sender (the gifter) to attach a personal message to a unique product code, and a receiver to reveal that message later by scanning the same code.
The SDK exposes six Gifting methods:
getGiftingStatusgetGiftingMessageStatusgetGiftingMessagecreateGiftingMessagedeleteGiftingMessagesignupForGifting
This guide explains how to implement both sides of the journey:
- Gifter flow – creating and attaching a message to a code
- Receiver flow – scanning and viewing the attached message
All rules around message expiry (e.g. "expires after 3 months") are configured in the io.tt dashboard.
High-Level Behaviour
When a QR/NFC code configured for Gifting is scanned:
- If no valid message exists, the user is treated as the gifter and taken through the message creation flow.
- If a valid message exists, the user is treated as the receiver and sees the "You have a message" experience.
- If the message is processing (e.g. audio transcription in progress), show an appropriate loading state.
The following sections describe how to use the SDK methods to power this behaviour.
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 Experience Status (getGiftingStatus)
Before showing any content, confirm the experience is currently active. If it isn't, route the user to an appropriate ended or not-yet-started screen rather than displaying the gifting journey.
const status = await sdk.getGiftingStatus(EXPERIENCE_ID);
if (!status.isActive) {
renderExperienceEndedScreen();
return;
}The full GiftingExperienceStatus object includes:
| Field | Type | Description |
|---|---|---|
isActive | boolean | Whether the experience is currently active |
hasEnded | boolean | Whether the experience has ended |
startDate | Date | null | Scheduled start date |
endDate | Date | null | Scheduled end date |
3. Check Item Message Status (getGiftingMessageStatus)
Once the experience is confirmed active, check whether this specific item already has a message attached. The result determines which flow to show — gifter (create a message) or receiver (view the existing message).
const msgStatus = await sdk.getGiftingMessageStatus(EXPERIENCE_ID, itemSlug);The GiftingMessageStatus object includes:
| Field | Type | Description |
|---|---|---|
valid | boolean | undefined | Whether a valid message exists for this item |
processing | boolean | undefined | Whether the message is still being processed (e.g. audio transcription) |
errorMessage | string | undefined | Error detail if something went wrong |
Route based on status:
if (msgStatus.processing) {
return renderProcessingScreen();
}
if (msgStatus.valid) {
// Receiver flow — fetch and display the message
} else {
// Gifter flow — build a new message
}4. Gifter Flow – Creating a Message
The gifter journey in your UI normally includes:
- Enter recipient and sender names
- Write a personal message (text and/or audio)
- Optionally add extra data
- Confirm and save
The SDK methods you use:
signupForGifting– register the gifter and capture their contact / consentcreateGiftingMessage– attach the actual message content to the code
4.1 Register the Gifter (signupForGifting)
Register the gifter's contact details and consent before creating the message. Only itemSlug and email are required — all other fields 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.
await sdk.signupForGifting(
EXPERIENCE_ID,
{
itemSlug, // required
email, // required
// 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
},
},
);4.2 Create the Message (createGiftingMessage)
Attach the personal message content to the code. to and from are required — everything else is optional. Pass any audio recordings as the fourth files argument; the SDK handles the presigned URL upload automatically.
await sdk.createGiftingMessage(
EXPERIENCE_ID,
itemSlug,
{
to: form.toName, // required
from: form.fromName, // required
occasion: form.occasion, // optional
text: form.messageBody, // optional
extra: { // optional
// additional custom fields
},
},
audioFiles, // optional: Array<File | Blob | Buffer> for audio upload
);The GiftingMessage object includes:
| Field | Type | Description |
|---|---|---|
to | string | Recipient name — required |
from | string | Sender name — required |
occasion | string | undefined | Occasion label (e.g. "Birthday") |
text | string | undefined | Written message body |
audio | object | undefined | Audio message — populated automatically when files are passed |
extra | Record<string, unknown> | undefined | Additional custom fields |
On success, show a confirmation screen such as:
"Your message is ready – tell them to scan the QR code."
5. Gifter Flow – Reset / Delete a Message (deleteGiftingMessage)
To remove an existing message so the code can start a new gifting journey, call deleteGiftingMessage. After deletion, the next scan of this item will enter the gifter flow again.
await sdk.deleteGiftingMessage(EXPERIENCE_ID, itemSlug);6. Receiver Flow – Fetching and Rendering the Message (getGiftingMessage)
When getGiftingMessageStatus returns valid: true, fetch the full message content. Use the returned data to populate the receiver card UI — the to, from, occasion, text, and audio fields are all available.
const message = await sdk.getGiftingMessage(EXPERIENCE_ID, itemSlug);The returned GiftingMessage shape:
{
"to": "Sara",
"from": "Daniel",
"occasion": "Birthday",
"text": "Happy Birthday, my dear friend...",
"audio": {
"src": "fileId",
"fileName": "message.webm",
"transcription": [
{ "start": 0, "end": 2.5, "text": "Happy Birthday" }
]
}
}After reading, subsequent scans will continue to show the same message until it is deleted or the experience ends.
7. End-to-End Example
export async function handleGiftingEntry(experienceId, itemSlug, form) {
// 1. Check experience is active
const status = await sdk.getGiftingStatus(experienceId);
if (!status.isActive) return renderExperienceEndedScreen();
// 2. Check item message state
const msgStatus = await sdk.getGiftingMessageStatus(experienceId, itemSlug);
if (msgStatus.processing) {
return renderProcessingScreen();
}
if (msgStatus.valid) {
// Receiver flow
const message = await sdk.getGiftingMessage(experienceId, itemSlug);
return renderReceiverCard(message);
}
// Gifter flow
if (!form) return renderGifterBuilder();
await sdk.signupForGifting(experienceId, {
itemSlug,
email: form.email,
firstName: form.firstName,
lastName: form.lastName,
marketingConsent: form.marketingConsent,
});
await sdk.createGiftingMessage(
experienceId,
itemSlug,
{
to: form.toName,
from: form.fromName,
occasion: form.occasion,
text: form.messageBody,
},
form.audioFiles,
);
return renderGifterSuccess();
}8. Summary Table
| Method | Signature | Used By | Purpose |
|---|---|---|---|
getGiftingStatus | (experienceId) | Both | Check whether the experience is active. |
getGiftingMessageStatus | (experienceId, itemSlug) | Both | Determine whether this item has a valid/processing message. |
getGiftingMessage | (experienceId, itemSlug) | Receiver | Retrieve message content for display. |
createGiftingMessage | (experienceId, itemSlug, message, files?) | Gifter | Attach or update the personal message on a code. |
deleteGiftingMessage | (experienceId, itemSlug) | Gifter | Remove the message so the code can start a new gifting journey. |
signupForGifting | (experienceId, entry) | Gifter | Register sender details and consent. |
Notes
- Message expiry duration and validation rules are configured on the io.tt dashboard, not in client code.
- All state (message existence, processing, validity) is enforced server-side. Treat SDK responses as the source of truth and handle UI branching only.