Skip to main content

Scratch card tracking

Cheggout returns tracking URLs with every card. You fire them. An integration that renders cards perfectly and never fires tracking is not a working integration — it is invisible to attribution, billing and every report you will later be shown.

This page covers the contract, both response shapes you will encounter, and what to do on each surface.

The contract

Two events, two rules.

EventFire whenURL source
ImpressionThe card becomes visible to the usertrackLink on the card
ClickThe user taps or scratches the cardclicktrack on the response

Firing is a plain GET. There is no authentication, no body, and the response is not interesting — you are pinging a pixel-style endpoint.

curl -s -o /dev/null "https://tracking.cheggout.com/Tracker/Impression?REPLACE"

The two words that carry the weight are visible and taps.

  • Visible means on screen, not fetched. A card loaded into a list the user never scrolls to has not been seen, and firing an impression for it inflates your numbers and degrades your own reporting.
  • Taps means the user acted. Fire on the scratch gesture, at the same moment you initiate the reveal.

Two response shapes

Cheggout returns tracking information in two different formats depending on which endpoint you called. Both appear in a scratch-card integration, so handle both.

Shape 1 — arrays

The rewards APIs return arrays of URLs:

{
"rewards": [
{
"rewardId": 121783,
"isAlreadyRewarded": false,
"trackLink": ["https://tracking.cheggout.com/Tracker/Impression?REPLACE"]
}
],
"clicktrack": ["https://tracking.cheggout.com/Tracker/clicks?REPLACE"]
}

Note the asymmetry: trackLink is per card, clicktrack sits at the top level of the response. Fire an impression per visible card, and the click URL when a card in that set is scratched.

The reveal payload adds two more arrays, both single-element:

FieldFire when
copyClickThe user copies the coupon code
redeemClickThe user taps redeem and goes to the merchant

The listing API returns trackLink on unscratched cards and copyClick / redeemClick on scratched ones.

Treat all of these as arrays of one-or-more and iterate. Do not index [0] blindly.

Shape 2 — the tagged string

Banner and store endpoints return tracking as a pseudo-XML tagged string, carried in href or trackLink. It is a single string containing several URLs plus display metadata:

<click>https://example.com/destination</click>
<track>https://tracking.cheggout.com/Tracker/Impression?REPLACE</track>
<clicktrack>https://tracking.cheggout.com/Tracker/clicks?REPLACE</clicktrack>
<browser>Native</browser>
<expiryduration>60</expiryduration>
<id>placement_channel_unit</id>
<redirectURL>https://example.com/landing</redirectURL>
TagMeaning
<click>The destination the user should be sent to
<track>The impression URL. Fire on visibility
<clicktrack>The click URL. Fire on tap
<browser>Native or Custom — how the destination should be opened
<expiryduration>Present with an observed value of 60. TBD Its meaning is not documented
<id>Identifier for the tracked unit
<redirectURL>Redirect target

You will meet this shape in a scratch-card integration when you render the banner creative returned by the SDK or server issuance call in your own UI.

It is not valid XML

It has no root element and the tag set is not formally specified. Do not hand it to a strict XML parser. Extract the tag you need — a bounded regular expression or a simple scan is appropriate — and ignore tags you do not recognise, because the set may grow.

When to fire, precisely

MomentFireDo not fire
Issuance response receivedNothingImpression — the card is not on screen yet
Card enters the viewportImpression, once
Card scrolls out and back inNothingA second impression for the same view session
User scratchesClick
Reveal returns a missNothing extraThe click already fired and is still correct
User copies the codecopyClick
User taps redeemredeemClick
Card re-displayed from your store laterImpression, as a new viewClick, unless they act again

The single most common bug is firing the impression when the response arrives rather than when the card is rendered. It is easy to write, it inflates impressions, and it makes your click-through rate look broken.

Firing on each surface

Fire the URLs yourself with a plain GET. Use a viewability signal, not a render callback.

// Impression on genuine visibility
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const url = entry.target.dataset.trackLink;
navigator.sendBeacon
? navigator.sendBeacon(url)
: fetch(url, { mode: 'no-cors', keepalive: true });
observer.unobserve(entry.target); // once per view session
});
}, { threshold: 0.5 });

document.querySelectorAll('[data-track-link]').forEach((el) => observer.observe(el));
// Click at the moment of the scratch gesture
function onScratch(card) {
clicktrack.forEach((url) => navigator.sendBeacon(url));
return revealCard(card.rewardId); // your backend
}

Fire tracking from the client, not from your backend when the response is assembled. Only the client knows what was actually seen.

Headless parsing guidance

If you receive the tagged string, extract only what you need.

function tag(trackString, name) {
const match = trackString.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
return match ? match[1].trim() : null;
}

const impressionUrl = tag(href, 'track');
const clickUrl = tag(href, 'clicktrack');
const destination = tag(href, 'click');
const openNatively = tag(href, 'browser') === 'Native';

Rules that will save you an incident:

  • Never throw on a missing tag. Not every response carries every tag. A missing <track> means no impression to fire, not a broken response.
  • Never fail the render because tracking could not be parsed. Log it and show the card.
  • Fire and forget. Use sendBeacon, or a fetch with a short timeout that you do not await. Tracking must never block or delay the user's interaction.
  • Do not retry aggressively. A failed tracking call is a lost data point, not a lost reward. One retry at most.
  • Do not log the full URL at info level. Tracking URLs carry identifiers.

Why it matters

Tracking is not telemetry you can add later. Four things depend on it:

Attribution. Cheggout cannot tell whether a campaign worked without impressions and clicks. A campaign that appears to have zero engagement gets deprioritised or paused, and your users stop seeing rewards.

Billing and settlement. Commercial terms across Cheggout's modules are engagement-based — CPE, cost per engagement, appears as a field on the offer catalogue. Impressions and clicks are the measured units.

Your own reporting. Every number you will be shown in a review — reach, click-through rate, reward take-up — derives from these calls. Missing impressions make your click-through rate look impossibly good; missing clicks make your campaign look dead.

Optimisation. Cheggout's targeting improves on observed behaviour. An integration that reports nothing gets no benefit from it.

TBD There is no publisher-facing reporting or reconciliation API. You cannot currently query what Cheggout recorded against what you fired, which means a tracking bug can run undetected. Until reporting APIs exist, instrument your own counters — impressions fired, clicks fired, reveals performed — and compare them against the figures Cheggout reports to you. Tracked in Open Items.

Checklist

  • Impressions fire on viewport visibility, not on response receipt.
  • One impression per genuine view session, not per re-render.
  • Clicks fire on the scratch gesture, alongside the reveal request.
  • copyClick and redeemClick are wired on the revealed reward screen.
  • Tracking arrays are iterated, not indexed at zero.
  • The tagged string is parsed leniently and never breaks a render.
  • Tracking calls are fire-and-forget and never block the UI.
  • You count what you fire, so you can reconcile against Cheggout's figures.

Next