Scratch card lifecycle
A card moves through a fixed sequence of states, and each transition has a rule attached. Getting these right is most of the work of a scratch-card integration, because the failure modes are quiet: a card that reveals twice loses a reward, and a card that never fires an impression is invisible to reporting.
This page is surface-agnostic. It describes what happens regardless of whether you are using the SDK, the widget, or the raw APIs.
The states
| State | Meaning | Who moves it |
|---|---|---|
| Triggered | A qualifying event was reported to Cheggout | You |
| Not eligible | No campaign matched. A normal outcome, not an error | Cheggout |
| Issued | One or more temporary cards exist. No reward is attached yet | Cheggout |
| Displayed | The card is visible to the user; the impression URL has been fired | You |
| Scratching | The user tapped or scratched; the click URL has been fired | You |
| Revealed | Cheggout picked a reward and returned it. isSeen is now true | Cheggout |
| No reward | The reveal returned BETTER LUCK NEXT TIME | Cheggout |
| Redeemed | The user used the code at the merchant | The merchant |
| Expired | The revealed coupon passed its endDate | Time |
| Lapsed | An issued card was never opened and campaign rules retired it | Cheggout |
The full flow
Step by step
1. Trigger
Something rewardable happens: a payment settles, a transfer completes, an article is read to the end. You report it with a trigger event code — a predefined value agreed with Cheggout that maps to a campaign. See Trigger events.
You also send a virtualId for the user and an extTransactionId that uniquely identifies the
event.
Bind virtualId and extTransactionId to an order your backend has already validated. Never accept
either value straight from client input. See
Security best practices.
2. Temporary card issue
Cheggout evaluates targeting and eligibility and returns zero or more temporary cards. Each
carries a rewardId and a trackLink, and nothing else of substance — there is no coupon behind it
yet.
An empty result is the normal shape of "this user does not get a card right now":
{ "status": false, "message": "failure", "rewards": [] }
Do not surface this as an error. Render nothing.
3. Display and impression
When the card becomes visible to the user, fire its impression URL. This is not optional — see Tracking. Impressions are the basis of attribution, billing and every report you will later be shown.
Fire on visibility, not on fetch. A card fetched into a list that the user never scrolls to has not been seen.
4. The user scratches
The scratch gesture does two things at once:
- Fires the click tracking URL.
- Triggers the reveal request.
5. Reveal and late-binding assignment
Only now does Cheggout choose a reward. It draws from the campaign pool, applying the targeting parameters you sent back at step 1 along with current budget, frequency and probability rules.
The outcome is one of three shapes:
| Outcome | Response | What to render |
|---|---|---|
| Coupon reward | couponType: "Brand", a couponCode, points: 0 | The coupon, terms, redeem steps, merchant branding |
| Points reward | couponType: "points", empty couponCode, points carries the value | The points award |
| Miss | { "status": false, "message": "BETTER LUCK NEXT TIME" } | A friendly miss state |
The full decoded reveal schema is on FetchRewardInfo. It is marked Beta — it was recovered from platform behaviour and is pending final engineering sign-off, so treat unknown extra fields as additive rather than assuming a closed schema.
6. Reward shown
Persist what you receive before you render it. This is the hard requirement of the whole module, and it is explained in full below.
7. Click through to the merchant
The reveal payload carries promoLink for coupon rewards, plus copyClick and redeemClick
tracking arrays. When the user copies the code or taps redeem, fire the matching tracking URL and
send them to the merchant.
8. Redemption
Redemption happens on the merchant's side, against couponCode, subject to terms and endDate.
Cheggout does not report redemption back to you through any documented API.
TBD There is no publisher-facing redemption or settlement reporting API. Redemption and conversion reporting is currently delivered out of band. Tracked in Open Items.
The one-shot reveal
Revealing a card is destructive. Calling reveal a second time for the same
rewardIdreturnsREWARD NOT FOUND— not the reward you received the first time.
This is the most common way a scratch-card integration loses user rewards. It usually happens like this: the reveal succeeds, the response reaches the client, the app crashes or the network drops before the reward is written down, the user reopens the card, and the reward is gone forever.
The rule: persist the reveal response server-side, in the same operation that receives it, before you return anything to the client.
For re-display, do not call reveal again. Read from your own store, or list the user's cards with GetAllScratchCardsByUserId, which returns unscratched cards first and then scratched ones carrying their full reward detail.
BETTER LUCK NEXT TIME is not an error
A miss arrives as a business-failure payload:
{ "status": false, "message": "BETTER LUCK NEXT TIME" }
It means no reward was allocated, for any of several ordinary reasons: the campaign is inactive, a frequency cap was hit, the reward pool is empty, the probability roll missed, or the campaign budget is exhausted. Cheggout does not tell you which.
Treat it as a successful reveal with a null reward:
- Do not log it as an exception or page an on-call engineer.
- Do not retry it. The card is consumed either way.
- Do mark the card as revealed in your own store, so the user is not shown an unscratched card again.
- Do design the miss UI deliberately. It is a frequent outcome, not an edge case.
The other business-failure messages are genuine problems and are worth alerting on:
SCRATCHCARDID NOT FOUND, CHEGCUSTOMERID NOT FOUND, VIRTUALID NOT FOUND,
BANKNAME NOT FOUND, REWARD NOT FOUND. See Error codes.
Idempotency
Idempotency behaves differently at each end of the lifecycle.
| Operation | Replay behaviour |
|---|---|
| Issue cards for an event | Idempotent. The same extTransactionId returns the same mapped set of cards rather than creating new ones |
| Reveal a card | Not idempotent. The second call returns REWARD NOT FOUND |
| List a user's cards | Idempotent — a read |
| Fire a tracking URL | Fire once per real event. Repeated firing distorts reporting |
Because issuance is idempotent, retrying after a network timeout is safe and correct. Reuse the same
extTransactionId on the retry — that is the entire point of the key.
After a card in a set has been scratched, a repeat issuance call for the same extTransactionId
returns unscratched cards first, then scratched ones, with the scratched entries carrying their
full reward detail. Do not assume a stable array order; key off rewardId and isAlreadyRewarded.
The key is what makes retries safe. If the same value is used for two genuinely different transactions, the second one silently returns the first one's cards and the user is under-rewarded. Generate it from something that is already unique in your system.
Expiry
Three different clocks apply, and they are frequently confused.
| What expires | Governed by | Documented value |
|---|---|---|
| The revealed coupon | The reward's endDate field, returned at reveal | Present in the payload, format YYYY-MM-DD HH:mm:ss.SSS |
| An unopened card | Campaign rules | TBD |
| Your auth tokens | Token lifetime | Reference token refreshed hourly; widget token documented as valid one hour |
TBD Unopened-card expiry is campaign-configured and is not exposed in any
documented response field. There is no expiryDate on a temporary card, and no documented rule
for how long an issued-but-unscratched card remains revealable. What is missing:
- Whether unopened cards expire at all, and on what schedule.
- Whether an expired card disappears from the user's card list or returns a distinct error at reveal.
- Whether expiry is configurable per campaign or is a platform default.
Confirm your campaign's expiry policy with Cheggout campaign operations during onboarding. Until then, do not build UI that displays a countdown on an unscratched card. Tracked in Open Items.
For token lifetimes, see Tokens — the exact expireDate format
is also unconfirmed.
Lifecycle checklist
Before you call a scratch-card integration finished:
- An empty issuance response renders nothing and logs nothing alarming.
- Impressions fire on visibility, not on fetch.
- Clicks fire on the scratch gesture.
- The reveal response is persisted server-side before it reaches the client.
- Re-opening a revealed card reads from your store, never from reveal.
- A reveal that times out is never blind-retried. The outcome is unknown, and a retry after a
successful first call returns
REWARD NOT FOUNDand loses the reward. Reconcile viaGetAllScratchCardsByUserId— if the card is nowisSeen: truethe reveal succeeded and the reward is in that response. -
BETTER LUCK NEXT TIMEhas a designed UI state and does not alert. - Retries reuse the original
extTransactionId. - Coupon
endDateis respected in "My Rewards" style listings.