← Home ← Live Demo
Technical Integration Guide

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.

Single-file HTML No dependencies iFrame-embeddable Hightouch-ready SFMC Journey Builder GDPR-compliant
1

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.

📁
Single File Deployment
One HTML file. Copy it to any static host. No npm, no build pipeline, no server-side rendering required.
🔗
URL-Parameterised
All customer context — ID, campaign, prize assignment — arrives via query string. Works with any email or SMS platform.
📡
Event-Driven
The card emits custom browser events at every step. Plug your tag manager or CDP SDK in once and capture all interactions.
🎁
API Prize Config
Optionally fetch prize configuration from a REST endpoint per customer — enabling server-side prize control and fraud prevention.
2
📧
SFMC
Journey Builder send
🔗
Personalised URL
?cid=%%ContactID%%&prize=P01
🎴
Scratch Card
scratch.rituals.com
🔵
Hightouch
Profile sync + audiences
Browser Events
card_opened · scratched
prize_revealed
📬
SFMC Journey
Winner / non-winner flows
Prize API (Optional)
GET /api/prizes?customer_id=123 → server-controlled prize pool
Design principle

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.

3

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.

1
Connect the repository
Push index.html to a private GitHub repo. Connect the repo to Netlify — it will auto-deploy on every commit to main.
2
Add custom domain
In Netlify → Domain Management, add scratch.rituals.com. Add a CNAME record in DNS pointing to your Netlify URL. SSL is provisioned automatically.
3
Set environment variables
In Netlify → Environment Variables, set 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.

Shell · Coolify Deploy
# 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.

1
Upload as a Static Asset
In Business Manager → Content → Static Files, upload index.html to /scratch-card/. It will be served at https://www.rituals.com/on/demandware.static/scratch-card/index.html.
2
Or create a Content Asset
Create a Page Designer page with a Custom HTML component embedding the scratch card as an iFrame. This gives you the Rituals header/footer wrapper with no additional development.
4

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.

HTML · iFrame Embed (rituals.com campaign page)
<!-- 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>
Cross-origin messaging

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.

5

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.

Example URL · SFMC AMPscript personalisation
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.
email 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.
JavaScript · Reading params inside the card
// 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
}
6

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.

JavaScript · Fetching prize config from API
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.
7

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.

👁️
scratch:card_opened
Fired immediately when the card loads and the customer context is resolved. Use for reach / open-rate measurement.
customer_id campaign loyalty_tier locale
scratch:first_scratch
Fired on the first scratch gesture. Distinguishes engaged users (who actually scratched) from those who only opened the card.
customer_id campaign timestamp
🎁
scratch:prize_revealed
Fired when the prize is fully revealed (70% threshold). The most important event — triggers all downstream flows.
customer_id campaign prize_id prize_value won voucher_code revealed_at
🛒
scratch:cta_clicked
Fired when the customer clicks the "Shop Now" or redemption CTA in the result panel. Measures conversion from win to purchase intent.
customer_id prize_id cta_destination
🔄
scratch:replayed
Fired when the customer clicks "New Card". Flag this customer segment — repeated plays may indicate prize-seeking behaviour worth monitoring.
customer_id play_count
JavaScript · Emitting events from the card
// 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,
});
8

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.

JavaScript · Hightouch Events SDK — identity + event tracking
// 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,
  });
});
How Hightouch routes the data

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.

Hightouch Audience Segments to configure
🏆 scratch_won = true
Winners who haven't redeemed yet. Sync to SFMC DE → triggers winner Journey. Re-evaluate daily to catch late redeemers.
😔 scratch_won = false
Non-winners. Suppress from prize journeys. Sync to a separate DE to trigger the consolation flow in Journey Builder.
👀 Opened but not scratched
scratch:card_opened fired but no scratch:first_scratch. Re-entry candidate — sync back to SFMC after 4h for a nudge send.
💎 Loyalty tier × prize tier
Platinum winners of top prizes. Flag in Hightouch for VIP outreach. Exclude from standard winner journey and route to a bespoke concierge flow.
9
1
Build the personalised send URL in Content Builder
In your email template, use AMPscript to inject %%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.
2
Set up a Journey Builder API Entry event
Create a Journey with an API Event entry source keyed on 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.
3
Winner journey — 3-email redemption sequence
Branch 1 (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.
4
Non-winner consolation journey
Branch 2 (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.
5
Redemption suppression via Hightouch → SFMC sync
When 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.
Python · Server-side SFMC Journey Builder API entry call
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
AMPscript in the winner email

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.

10
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.
11
🔐
Server-side prize assignment
Never determine the prize in client-side JavaScript alone. Always have the server assign and sign the prize token. The client only renders what the server decided.
✍️
HMAC URL signing
Sign the URL parameters server-side with an HMAC-SHA256 secret. Verify the signature before issuing any prize token — prevents parameter manipulation.
1️⃣
One play per customer
Track played status in the backend, keyed by customer_id + campaign. The prize API should return an error (and the card should show an "already played" state) on repeat attempts.
⏱️
Token expiry
Prize tokens should expire after a short window (15 minutes). If the customer takes longer, show a "session expired" message and require re-entry from the email link.
🚦
Rate limiting
Rate-limit the prize API to 1 request per customer per campaign. Add IP-based rate limiting as a secondary layer for bot protection.
🇪🇺
GDPR / Cookie compliance
The scratch card stores nothing in 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.
12
Need help with implementation?

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.