Scratch & Discover
Integration Playbook
Everything your digital team needs to connect the scratch card to Rituals' existing infrastructure — from hosting to CDP event tracking and dynamic prize management.
The scratch card is a self-contained single HTML file. It has no runtime dependencies, no build step, and no framework requirement. The entire experience — scratch mechanic, prize logic, animations, and event emission — lives in one file you can drop onto any web server.
From a MarTech perspective, it behaves like any other campaign landing page: it accepts customer context via URL parameters, fires browser events that your tag manager or CDP SDK can intercept, and optionally calls an API endpoint to fetch personalised prize assignments.
prize_revealed
The scratch card is intentionally stateless on the client. It receives context, runs the experience, and emits events. All persistence (who won, redemption status, fraud checks) lives in Rituals' existing backend — not in the widget itself.
Fastest path: deploy to Netlify as a static site. Gives you a custom subdomain (scratch.rituals.com) with SSL, global CDN, and zero server maintenance.
index.html to a private GitHub repo. Connect the repo to Netlify — it will auto-deploy on every commit to main.scratch.rituals.com. Add a CNAME record in DNS pointing to your Netlify URL. SSL is provisioned automatically.PRIZE_API_URL if using server-side prize configuration. The build injects it at deploy time.If Rituals already has a Coolify instance running on Hetzner (as many teams in this architecture do), the simplest option is a Static Site service with Let's Encrypt SSL.
# 1. In Coolify → New Resource → Static Site # 2. Source: GitHub repo containing index.html # 3. Publish directory: / (root) # 4. Domain: scratch.rituals.com # 5. SSL: Let's Encrypt (automatic) # Or deploy manually via rsync: rsync -avz index.html \ user@hetzner-ip:/var/www/scratch-card/ # Nginx config (add to server block): location / { root /var/www/scratch-card; index index.html; add_header X-Frame-Options "SAMEORIGIN"; add_header Content-Security-Policy "frame-ancestors 'self' https://rituals.com"; }
For hosting directly on www.rituals.com within Salesforce Commerce Cloud, upload the file as a static asset and reference it from a Content Asset or Experience page.
index.html to /scratch-card/. It will be served at https://www.rituals.com/on/demandware.static/scratch-card/index.html.The cleanest integration for teams who don't want to touch the main site codebase: host the scratch card on its own subdomain, then embed it via iFrame into any campaign or content page on rituals.com.
<!-- Drop this into any Rituals campaign page --> <iframe id="scratch-card-frame" src="https://scratch.rituals.com/index.html?{{ customer_id }}&campaign={{ campaign_id }}&prize={{ prize_token }}" width="100%" height="680" frameborder="0" scrolling="no" allow="autoplay" style="border-radius:8px; display:block; margin:0 auto; max-width:480px" ></iframe> <!-- Listen for events broadcast from the iFrame --> <script> window.addEventListener('message', (e) => { if (e.origin !== 'https://scratch.rituals.com') return; const { event, data } = e.data; switch (event) { case 'scratch:prize_revealed': // data = { customerId, prize, campaign, won } // Option A: Hightouch Events SDK (recommended) htevents.track('scratch_prize_revealed', data); break; case 'scratch:completed': // Option B: push to dataLayer for GTM → SFMC connector window.dataLayer.push({ event: 'scratch_completed', ...data }); break; } }); </script>
The card uses postMessage to broadcast events to the parent window, bypassing iFrame isolation. The parent page should always validate event.origin before acting on messages.
URL parameters are the simplest way to inject personalised data. Salesforce Marketing Cloud merges subscriber attributes and Data Extension values into the link at send time using AMPscript. The scratch card reads them on load via URLSearchParams.
https://scratch.rituals.com/index.html ?cid=%%ContactID%% &email=%%emailaddr%% &campaign=spring_ritual_2026 &prize=%%scratch_prize_token%% &locale=de-DE &loyalty_tier=%%loyalty_tier%% &sig=%%=HMAC('SHA256', concat(ContactID, campaign), @secret)=%%
| Parameter | Type | Required | Description |
|---|---|---|---|
| cid | string | Required | The customer's unique ID — use SFMC %%ContactID%%. Used to associate the scratch event with a known Hightouch profile and SFMC contact record. |
| string (url-encoded) | Optional | Use SFMC %%emailaddr%%. Pre-fills identity for profile resolution in Hightouch if the customer arrives without a session cookie. |
|
| campaign | string | Required | Campaign identifier (e.g. spring_ritual_2026). Attached to all events fired by the card. Matches the Journey name in SFMC for cross-channel attribution. |
| prize | string (token) | Optional | A server-issued prize token stored as a Data Extension attribute in SFMC (%%scratch_prize_token%%). Pre-determines the outcome server-side. Must be validated before redemption. |
| locale | BCP-47 string | Optional | Sets UI language and currency formatting. Defaults to de-DE. Supports all Rituals markets: nl-NL, en-GB, fr-FR, etc. Map from SFMC %%locale%% attribute. |
| loyalty_tier | string | Optional | Customer's My Rituals loyalty tier (silver / gold / platinum). Stored in SFMC as a contact attribute synced from Hightouch. Used to unlock tier-specific prize pools. |
| sig | string (HMAC-SHA256) | Recommended | HMAC signature of the other parameters, signed with a shared secret using SFMC's HMAC() AMPscript function. The prize API verifies this before issuing a token. |
// Add this block near the top of the scratch card script const params = new URLSearchParams(window.location.search); const CONTEXT = { customerId: params.get('cid') || null, email: params.get('email') || null, campaign: params.get('campaign') || 'default', prizeToken: params.get('prize') || null, locale: params.get('locale') || 'de-DE', loyaltyTier: params.get('loyalty_tier') || 'standard', }; // If a prize token was passed, use it — skip local prize draw if (CONTEXT.prizeToken) { prize = decodePrizeToken(CONTEXT.prizeToken); } else { prize = weightedRandom(CONFIG.prizes); // fallback }
For production campaigns, never let the client determine the prize outcome. A simple API call at card-load time fetches a signed prize assignment for the specific customer, ensuring prize inventory is controlled server-side and tamper-proof.
async function fetchPrizeConfig(customerId, campaign) { try { const res = await fetch( `https://api.rituals.com/campaigns/scratch/prize` `?customer_id=${customerId}&campaign=${campaign}`, { headers: { 'X-API-Key': CONFIG.apiKey } } ); if (!res.ok) throw new Error('Prize API error'); return await res.json(); } catch (err) { // Graceful fallback — local weighted random console.warn('Prize API unavailable, using local fallback', err); return weightedRandom(CONFIG.prizes); } } // API response shape: // { // prize_id: "P-EUR-500", // value: "€500", // label: "The Ritual of Luxury", // win: true, // voucher_code: "LXRY-2026-XKQP", // redemption code // expires_at: "2026-03-31T23:59:59Z", // token: "eyJhbGciOiJIUzI1NiJ9..." // signed JWT // }
| Response field | Type | Used for |
|---|---|---|
| prize_id | string | Sent in all tracking events. Links the scratch interaction to the specific prize SKU in the backend. |
| voucher_code | string | Displayed to the customer after reveal. Can be pre-generated in Salesforce Commerce Cloud and returned here. |
| expires_at | ISO 8601 | Displayed in the result panel as a redemption deadline. Triggers an expiry reminder flow in Klaviyo. |
| token | JWT string | Signed by the server. Passed back when the customer redeems at checkout — proving the prize is genuine without another API call. |
The scratch card dispatches custom browser events on window and also calls window.postMessage for iFrame contexts. Connect your tag manager or CDP SDK by listening to these events once — no changes to the card code needed.
// Helper inside scratch card — call this at each lifecycle point function emit(eventName, detail = {}) { const payload = { ...detail, customer_id: CONTEXT.customerId, campaign: CONTEXT.campaign, timestamp: new Date().toISOString(), }; // 1. Dispatch as a browser custom event (for GTM dataLayer) window.dispatchEvent(new CustomEvent(eventName, { detail: payload })); // 2. postMessage to parent (for iFrame contexts) window.parent.postMessage({ event: eventName, data: payload }, '*'); // 3. Hightouch Events SDK — if loaded in same page context if (window.htevents) { window.htevents.track(eventName, payload); } } // Usage at prize reveal: emit('scratch:prize_revealed', { prize_id: prize.id, prize_value: prize.value, won: prize.win, voucher_code: prize.voucherCode || null, });
Hightouch acts as the reverse ETL layer — it reads from Rituals' data warehouse (e.g. BigQuery or Snowflake) and syncs customer attributes and computed audiences downstream to SFMC, the website, and any other activation channel. The scratch card connects to this in two directions: events flow into the warehouse via the Hightouch Events SDK, and audience definitions in Hightouch route the right follow-up journey in SFMC.
// Load the Hightouch Events SDK (add to <head> or via GTM) // https://hightouch.com/docs/events/sdks/javascript htevents.load('YOUR_HIGHTOUCH_WRITE_KEY'); // On card load — identify the customer from URL params htevents.identify(CONTEXT.customerId, { email: CONTEXT.email, loyalty_tier: CONTEXT.loyaltyTier, locale: CONTEXT.locale, }); // On prize reveal — track the event with full prize payload window.addEventListener('scratch:prize_revealed', ({ detail }) => { htevents.track('Scratch Prize Revealed', { campaign: detail.campaign, prize_id: detail.prize_id, prize_value: detail.prize_value, won: detail.won, voucher_code: detail.voucher_code || null, revealed_at: detail.timestamp, loyalty_tier: CONTEXT.loyaltyTier, }); // Also update profile traits for audience segmentation in Hightouch htevents.identify(CONTEXT.customerId, { scratch_last_campaign: detail.campaign, scratch_last_played_at: detail.timestamp, scratch_won: detail.won, scratch_prize_id: detail.prize_id, }); });
Events tracked via the Hightouch Events SDK land in your warehouse (BigQuery / Snowflake) within minutes. Hightouch then reads those rows through a model, computes audience membership, and syncs the relevant contacts directly into an SFMC Data Extension — which triggers the Journey Builder entry event automatically. No middleware needed.
scratch:card_opened fired but no scratch:first_scratch. Re-entry candidate — sync back to SFMC after 4h for a nudge send.%%ContactID%%, %%emailaddr%%, and a pre-assigned %%scratch_prize_token%% attribute. The prize token should be written to a Sendable Data Extension before the journey send — populated by the prize API or by a Hightouch sync from the warehouse.ContactKey. The scratch card API calls POST /interaction/v1/events on scratch:prize_revealed, injecting won, prize_id, and voucher_code as event data attributes. Journey Builder then evaluates a Decision Split on won = true / false to route contacts into the correct email sequence.won = true): (1) Immediate transactional win confirmation with %%voucher_code%% rendered via AMPscript — send within 60 seconds of API entry. (2) 48h reminder if the voucher hasn't been applied at checkout. (3) 5-day urgency email with dynamic expiry countdown using SFMC's DATEADD and NOW() functions.won = false): wait 1 hour, then send a consolation email with a small loyalty-tier discount code — populated from a separate Data Extension keyed on %%loyalty_tier%%. Add a suppression rule to exclude contacts who entered the winner branch.scratch_redeemed_at is set in the warehouse (triggered by a checkout event in SFCC), Hightouch syncs that attribute back to SFMC. Use a Journey update activity or Automation Studio query to exit contacts from the reminder branch once redeemed.import requests def get_sfmc_token(client_id, client_secret, subdomain): res = requests.post( f"https://{subdomain}.auth.marketingcloudapis.com/v2/token", json={ "grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret, } ) return res.json()["access_token"] def trigger_scratch_journey(contact_key, prize_data, token, subdomain): # Fire the API Entry event into Journey Builder res = requests.post( f"https://{subdomain}.rest.marketingcloudapis.com" f"/interaction/v1/events", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, json={ "ContactKey": contact_key, "EventDefinitionKey": "scratch-prize-revealed-v1", "Data": { "won": prize_data["win"], "prize_id": prize_data["prize_id"], "prize_value": prize_data["value"], "voucher_code": prize_data.get("voucher_code", ""), "expires_at": prize_data.get("expires_at", ""), "campaign": prize_data["campaign"], }, } ) return res.status_code # 201 = contact entered journey
In the win confirmation email body, render the prize value and voucher with: %%=v(@prize_value)=%% and %%=v(@voucher_code)=%% — populated from the Journey event data. Use DATEADD(NOW(), 14, 'D') to dynamically compute and display the 14-day expiry deadline.
| CRM Attribute | Source | Use case |
|---|---|---|
| scratch_participated | card_opened | Campaign reach metric. Build a Hightouch suppression audience (scratch_participated = true AND campaign = X) to exclude players from any retargeting sends for the same campaign. |
| scratch_engaged | first_scratch | Distinguishes passive openers from actively engaged customers. High-value signal for Hightouch lookalike audiences and SFMC re-engagement sends. |
| scratch_won | prize_revealed | Boolean. Gates winner vs. non-winner communications flows. Essential for suppression logic. |
| scratch_prize_id | prize_revealed | Links to the specific prize awarded. Enables prize-specific redemption reminder copy in SFMC Journey emails via %%prize_id%% AMPscript lookup against a Prize Content Data Extension. |
| scratch_voucher_code | prize_revealed | The actual code the customer needs to redeem. Store in CRM for repeat lookup (customer service use case). |
| scratch_redeemed_at | Checkout event | Set at purchase when the voucher code is applied in SFCC. Hightouch syncs this attribute back to SFMC Contact Builder — triggering journey exit from the reminder sequence automatically. |
| scratch_total_plays | replayed | Play count across campaigns. High scorers may indicate loyalty; suspiciously high counts flag for fraud review. |
customer_id + campaign. The prize API should return an error (and the card should show an "already played" state) on repeat attempts.localStorage or cookies. All state is in the URL and server. Fully compliant without a cookie banner — but review with legal if logging IP addresses.This guide was prepared by House of MAAD — Robin Haak's MarTech consultancy specialising in CDP implementations and personalisation systems. For technical implementation support, integration with Hightouch or Salesforce Marketing Cloud, or campaign strategy across Rituals' European markets, reach out via LinkedIn.
Prepared with the support of Claude Code — Anthropic's AI coding tool. Integration patterns, code samples, and architecture recommendations were drafted and iterated using Claude Code as a technical co-pilot. All implementation details should be validated by Rituals' engineering team before production deployment.