Skip to main content

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

StateMeaningWho moves it
TriggeredA qualifying event was reported to CheggoutYou
Not eligibleNo campaign matched. A normal outcome, not an errorCheggout
IssuedOne or more temporary cards exist. No reward is attached yetCheggout
DisplayedThe card is visible to the user; the impression URL has been firedYou
ScratchingThe user tapped or scratched; the click URL has been firedYou
RevealedCheggout picked a reward and returned it. isSeen is now trueCheggout
No rewardThe reveal returned BETTER LUCK NEXT TIMECheggout
RedeemedThe user used the code at the merchantThe merchant
ExpiredThe revealed coupon passed its endDateTime
LapsedAn issued card was never opened and campaign rules retired itCheggout

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.

Report verified events only

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":

A normal not-eligible response
{ "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:

  1. Fires the click tracking URL.
  2. 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:

OutcomeResponseWhat to render
Coupon rewardcouponType: "Brand", a couponCode, points: 0The coupon, terms, redeem steps, merchant branding
Points rewardcouponType: "points", empty couponCode, points carries the valueThe 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 rewardId returns REWARD NOT FOUNDnot 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.

OperationReplay behaviour
Issue cards for an eventIdempotent. The same extTransactionId returns the same mapped set of cards rather than creating new ones
Reveal a cardNot idempotent. The second call returns REWARD NOT FOUND
List a user's cardsIdempotent — a read
Fire a tracking URLFire 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.

Never reuse an extTransactionId across different events

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 expiresGoverned byDocumented value
The revealed couponThe reward's endDate field, returned at revealPresent in the payload, format YYYY-MM-DD HH:mm:ss.SSS
An unopened cardCampaign rulesTBD
Your auth tokensToken lifetimeReference 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 FOUND and loses the reward. Reconcile via GetAllScratchCardsByUserId — if the card is now isSeen: true the reveal succeeded and the reward is in that response.
  • BETTER LUCK NEXT TIME has a designed UI state and does not alert.
  • Retries reuse the original extTransactionId.
  • Coupon endDate is respected in "My Rewards" style listings.

Next