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.
| Event | Fire when | URL source |
|---|---|---|
| Impression | The card becomes visible to the user | trackLink on the card |
| Click | The user taps or scratches the card | clicktrack 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:
| Field | Fire when |
|---|---|
copyClick | The user copies the coupon code |
redeemClick | The 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>
| Tag | Meaning |
|---|---|
<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 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
| Moment | Fire | Do not fire |
|---|---|---|
| Issuance response received | Nothing | Impression — the card is not on screen yet |
| Card enters the viewport | Impression, once | |
| Card scrolls out and back in | Nothing | A second impression for the same view session |
| User scratches | Click | |
| Reveal returns a miss | Nothing extra | The click already fired and is still correct |
| User copies the code | copyClick | |
| User taps redeem | redeemClick | |
| Card re-displayed from your store later | Impression, as a new view | Click, 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
- Headless
- Android — Kotlin
- iOS — Swift
- Web widget
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.
The SDK provides a helper that parses the tagged string and fires the right URL:
CheggoutUtils.Companion.trackLink(href, virtualId)
Call it when a Cheggout creative you render yourself becomes visible. On tap, pass href to the SDK
on launch as documented.
TBD Whether the SDK also fires the click URL itself is not documented. Confirm with Cheggout, and do not assume it is handled for you.
CheggoutApp.impression(userID: virtualId, impressionInfo: href)
Call it when a Cheggout creative you render yourself becomes visible. On tap, pass href to the SDK
on launch as documented.
TBD Whether the SDK also fires the click URL itself is not documented. Confirm with Cheggout, and do not assume it is handled for you.
The widget fires impression and click tracking for the banners and cards it renders. You do not fire anything for widget-rendered surfaces.
The widget also writes interaction state to localStorage, which you can read to avoid re-triggering
a banner for the same article — see Web integration.
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.
-
copyClickandredeemClickare 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
- Lifecycle — where each tracking event sits in the journey.
- Advertisement tracking — the same contract for banners and stores.
- Core concepts — tracking links in the shared vocabulary.