Headless scratch card integration
The headless path gives you the scratch-card module as five HTTP APIs and nothing else. You design the card, you animate the scratch, you decide where rewards live. Cheggout decides who is eligible and what they win.
Choose this path when the card has to look like your product, when you are on a surface with no SDK, or when you want the reveal to happen on your backend rather than in a client.
Everything here runs against the Rewards Center product line:
| Environment | Base URL |
|---|---|
| Production | https://rewardscenter.cheggout.com/api/ |
| UAT | https://rewardscenter-uat.cheggout.com/api/ |
The full flow
Two tokens, two scopes
The most common headless mistake is using the wrong token. There are two, they are not interchangeable, and the API will reject the wrong one.
| Token | Minted by | Scope | Used by |
|---|---|---|---|
referenceKey | getSecuredReferenceToken | Your channel. Not a user | GetReward |
token | GetAuthToken | One user | FetchRewardInfo, GetAllScratchCardsByUserId |
Both are presented as Authorization: Bearer <token>. Passing a reference token to
FetchRewardInfo is rejected. See Tokens.
Step 1 — Mint and cache the channel token
Run this on a scheduled job, hourly, on your backend. It is not part of the runtime UI path; if your card issuance is waiting on a token mint, your architecture is wrong.
This call uses the signed envelope.
curl -X POST https://rewardscenter-uat.cheggout.com/api/getSecuredReferenceToken \
-H "Content-Type: application/json" \
-d '{
"CHANNELID": "YOURCHANNEL",
"SIGN": "REPLACE_WITH_SIGNATURE",
"BDATA": "eyAiYk5hbWUiOiAiWU9VUkNIQU5ORUwiIH0="
}'
The BDATA above is the Base64 of:
{ "bName": "YOURCHANNEL" }
Note the payload key is bName on this endpoint, and it is case-sensitive.
Verify and decode the response BDATA:
{
"referenceKey": "REPLACE_WITH_TOKEN",
"expireDate": "<expiry timestamp>"
}
Cache referenceKey in a store all your application instances share. Refresh hourly. On a 401,
re-mint once, single-flight, and retry the original call.
Treat the token as an opaque credential — no token format is documented, so do not parse it.
TBD The expireDate format is not documented consistently. Refresh on your own
hourly schedule rather than parsing it.
Full contract: getSecuredReferenceToken.
Step 2 — Issue cards when the event happens
GetReward takes a plain JSON body — no envelope — authenticated with the channel token.
curl -X POST https://rewardscenter-uat.cheggout.com/api/GetReward \
-H "Authorization: Bearer <referenceKey>" \
-H "Content-Type: application/json" \
-d '{
"triggerEventType": "YOURCHANNEL_CAMPAIGN",
"subTriggerEventType": "BILLPAY",
"amount": 499.00,
"extTransactionId": "PUB_TXN_20260731_000117",
"channelName": "YOURCHANNEL",
"dateandtime": "31072026141530",
"virtualId": "pub_8f14e45fceea167a",
"deviceType": "web",
"additionalParams": "{\"city\":\"Bengaluru\",\"pincode\":\"560001\"}"
}'
| Field | Type | Required | Notes |
|---|---|---|---|
triggerEventType | string | Yes | Predefined campaign or trigger code |
channelName | string | Yes | Your channel |
extTransactionId | string | Yes | Unique alphanumeric. Your idempotency key |
virtualId | string | Yes | Your pseudonymous user id |
dateandtime | string | Yes | Documented format DDMMYYYYHHMMSS |
amount | number | See note | Transaction amount, two decimals |
additionalParams | string | No | A JSON string, not an object. All inner keys optional |
subTriggerEventType | string | No | Finer-grained sub-category. Seen in examples |
deviceType | string | No | Seen in examples |
additionalParams accepts these optional inner keys, all used for targeting at reveal: from,
to, state, district, city, pincode, ipaddress, lat, long, gender, age.
amount is marked both mandatory and optional in the source contract. Always send the real
amount when you have it, because campaigns may target on it. TBD The behaviour
when amount is omitted is not documented — do not assume it defaults to zero, or to anything else.
dateandtime is documented as DDMMYYYYHHMMSS, but examples in circulation use other formats.
Send the documented format and confirm during UAT. TBD
Both are tracked in Open Items.
Success
{
"status": true,
"message": "success",
"rewards": [
{
"rewardId": 121783,
"isAlreadyRewarded": false,
"createdDateTime": "2026-07-31 14:15:31.402",
"trackLink": ["https://tracking.cheggout.com/Tracker/Impression?REPLACE"]
}
],
"clicktrack": ["https://tracking.cheggout.com/Tracker/clicks?REPLACE"]
}
No cards
{ "status": false, "message": "failure", "rewards": [] }
This is not an error. It means the user is not eligible right now. Render nothing.
Full contract: GetReward.
Step 3 — Render and fire the impression
Render an unscratched card for each entry in rewards where isAlreadyRewarded is false. The
moment a card is actually visible, GET its trackLink URL.
curl -s -o /dev/null "https://tracking.cheggout.com/Tracker/Impression?REPLACE"
Fire it once per genuine view. Fire on visibility — an intersection observer, a viewability callback — not on fetch. See Tracking for the full contract and for the alternative pseudo-XML tracking-string shape you will encounter on other Cheggout endpoints.
Step 4 — Mint a user token
Reveal needs a user-scoped token. Mint it on your backend with the signed envelope, cache it per
user, and re-mint on 401.
curl -X POST https://rewardscenter-uat.cheggout.com/api/GetAuthToken \
-H "Content-Type: application/json" \
-d '{
"CHANNELID": "YOURCHANNEL",
"SIGN": "REPLACE_WITH_SIGNATURE",
"BDATA": "<base64 of the payload below>"
}'
{
"virtualId": "pub_8f14e45fceea167a",
"channelName": "YOURCHANNEL"
}
The payload key here is channelName — not bName. Decoded response:
{ "token": "<user token>", "expireDate": "<expiry timestamp>" }
Full contract: GetAuthToken.
Step 5 — Reveal
FetchRewardInfo has an unusual shape: it carries a BDATA field but no SIGN. Here BDATA
is an encoding layer, not a signature. Authentication comes entirely from the bearer user token.
curl -X POST https://rewardscenter-uat.cheggout.com/api/FetchRewardInfo \
-H "Authorization: Bearer <user token>" \
-H "Content-Type: application/json" \
-d '{
"CHANNELID": "YOURCHANNEL",
"BDATA": "<base64 of the payload below>"
}'
{
"rewardId": "121783",
"virtualId": "pub_8f14e45fceea167a",
"channelName": "YOURCHANNEL"
}
GetReward returns rewardId as a number. FetchRewardInfo expects it as a string. Cast
explicitly rather than letting your JSON library decide.
An optional entryPoint field is accepted in the payload.
The response is { "CHANNELID": "CHEGGOUT", "BDATA": "<base64>" }. Decode BDATA to get one of
three payloads.
Coupon reward — couponType is "Brand", points is 0, and siteId, offerId and
promoLink are present:
{
"rewardId": 121783,
"transactionId": "PUB_TXN_20260731_000117",
"offerTitle": "Flat 20% off at Example Coffee",
"description": "Valid on orders above 300",
"terms": "<ul><li>One use per customer</li><li>Not valid with other offers</li></ul>",
"couponCode": "REPLACE_WITH_CODE",
"endDate": "2026-09-30 23:59:59.000",
"couponType": "Brand",
"isSeen": true,
"points": 0,
"siteId": 4021,
"offerId": 88123,
"promoLink": "https://click.cheggout.com/REPLACE",
"couponImage": "https://cdn.example.com/coupon.png",
"redeemSteps": "<ul><li>Copy the code</li><li>Apply at checkout</li></ul>",
"bannerDisplayUrl": "https://cdn.example.com/banner.png",
"iconImage": "https://cdn.example.com/icon.png",
"details": "<p>Offer details</p>",
"siteName": "Example Coffee",
"coverImage": "https://cdn.example.com/cover.png",
"rewardImage": "https://cdn.example.com/reward.png",
"createdDateTime": "2026-07-31 14:15:31.402",
"bgColor": "#F3E9D2",
"copyClick": ["https://tracking.cheggout.com/Tracker/clicks?REPLACE"],
"redeemClick": ["https://tracking.cheggout.com/Tracker/clicks?REPLACE"]
}
Points reward — couponType is "points", couponCode is empty, and points carries the
awarded value.
Miss — a business-failure payload:
{ "status": false, "message": "BETTER LUCK NEXT TIME" }
The other business-failure messages are SCRATCHCARDID NOT FOUND, CHEGCUSTOMERID NOT FOUND,
VIRTUALID NOT FOUND, BANKNAME NOT FOUND and REWARD NOT FOUND.
Beta The decoded reveal schema above was recovered from platform behaviour and is pending final engineering sign-off. Parse defensively and tolerate additional fields.
Reveal is one-shot. Calling FetchRewardInfo again with the same rewardId returns
REWARD NOT FOUND, not the original reward. Write the decoded payload to your own store inside the
same request handler that receives it, before returning anything to the client.
A reveal that times out may have succeeded. Never blind-retry it — a retry that lands after a
successful first call returns REWARD NOT FOUND and the user loses a reward that was genuinely
allocated. Reconcile instead: call
GetAllScratchCardsByUserId and check whether
the card now shows isSeen: true. If it does, the reveal happened and the reward is in that
response. Only reveal again if the card is still unscratched.
Full contract: FetchRewardInfo.
Step 6 — List the user's cards
For a "My Rewards" screen, list everything the user has. This is a GET with the user token.
curl -X GET "https://rewardscenter-uat.cheggout.com/api/GetAllScratchCardsByUserId?page=1&limit=20" \
-H "Authorization: Bearer <user token>"
The response is a bare JSON array. There is no envelope, no status, no message, no total
count.
[
{
"scratchcardId": 55110,
"rewardId": 121784,
"campaignId": "YOURCHANNEL_CAMPAIGN",
"isSeen": false,
"trackLink": ["https://tracking.cheggout.com/Tracker/Impression?REPLACE"]
},
{
"scratchcardId": 55109,
"rewardId": 121783,
"campaignId": "YOURCHANNEL_CAMPAIGN",
"isSeen": true,
"offerTitle": "Flat 20% off at Example Coffee",
"couponCode": "REPLACE_WITH_CODE",
"couponType": "Brand",
"endDate": "2026-09-30 23:59:59.000",
"promoLink": "https://click.cheggout.com/REPLACE",
"copyClick": ["https://tracking.cheggout.com/Tracker/clicks?REPLACE"],
"redeemClick": ["https://tracking.cheggout.com/Tracker/clicks?REPLACE"]
}
]
Unscratched cards come first, then scratched ones, paginated across both. Pagination is 1-based.
TBD There is no total count and no hasMore flag, so detect the end of the list
by receiving fewer items than limit. No default or maximum for limit is documented. Tracked
in Open Items.
Full contract: GetAllScratchCardsByUserId.
Build guide
A working order that avoids rework.
1. Get the envelope right first
Implement the signed envelope and prove it against
getSecuredReferenceToken before touching anything else. Almost every first-integration failure is
a signing failure, and Signature Mismatch! tells you nothing about which of the four common causes
you hit.
2. Build the token layer as infrastructure
Two caches, not one:
- A single channel-scoped
referenceKey, refreshed hourly by a job. - A per-user
tokencache keyed byvirtualId.
Both need single-flight re-minting on 401 and a hard cap of one retry. Do not mint a token inside
a request handler that a user is waiting on unless the cache is cold.
3. Issue cards from a verified event
Call GetReward from your backend, after your own order validation, using an extTransactionId
derived from your order. Never let a client choose it.
4. Handle the empty case before the happy case
Write the "no cards" branch first. It is the most common response in a freshly configured environment, and building it first stops you from misreading a correctly-configured campaign as a broken integration.
5. Wire tracking as you build the UI, not afterwards
Impression on visibility, click on scratch. Retro-fitting tracking is how integrations ship without it. See Tracking.
6. Make reveal a server-side, write-first operation
Client asks your backend to reveal. Your backend calls FetchRewardInfo, writes the result, then
responds. The client never calls Cheggout's reveal directly, and never holds a reward that is not
also in your database.
7. Read My Rewards from your own store
Use GetAllScratchCardsByUserId to reconcile and to backfill, not as the primary read path for
rewards the user has already revealed.
8. Test the awkward paths
| Scenario | Expected behaviour |
|---|---|
Duplicate GetReward with the same extTransactionId | Same card set returned, no new cards |
| Reveal called twice | Second call returns REWARD NOT FOUND; your app still shows the reward from your store |
Reveal returns BETTER LUCK NEXT TIME | Miss UI, card marked revealed, no alert raised |
| Reveal times out | No blind retry. Reconcile via GetAllScratchCardsByUserId: if the card is now isSeen: true the reveal succeeded and the reward is in that response; only reveal again if it is still unscratched |
| Token expired mid-flow | One transparent re-mint, then success |
| Campaign paused | Empty issuance response, nothing rendered |
See Testing and the Go-live checklist.
API reference
| Endpoint | Auth | Purpose |
|---|---|---|
| getSecuredReferenceToken | Signed envelope | Mint the channel token |
| GetReward | Bearer channel token | Issue temporary cards |
| GetAuthToken | Signed envelope | Mint a user token |
| FetchRewardInfo | Bearer user token | Reveal a card |
| GetAllScratchCardsByUserId | Bearer user token | List a user's cards |
Next
- Tracking — the impression and click contract in detail.
- Trigger events — the codes that connect events to campaigns.
- API conventions — shared error, retry and idempotency behaviour.