Skip to main content

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:

EnvironmentBase URL
Productionhttps://rewardscenter.cheggout.com/api/
UAThttps://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.

TokenMinted byScopeUsed by
referenceKeygetSecuredReferenceTokenYour channel. Not a userGetReward
tokenGetAuthTokenOne userFetchRewardInfo, 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.

getSecuredReferenceToken
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.

GetReward
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\"}"
}'
FieldTypeRequiredNotes
triggerEventTypestringYesPredefined campaign or trigger code
channelNamestringYesYour channel
extTransactionIdstringYesUnique alphanumeric. Your idempotency key
virtualIdstringYesYour pseudonymous user id
dateandtimestringYesDocumented format DDMMYYYYHHMMSS
amountnumberSee noteTransaction amount, two decimals
additionalParamsstringNoA JSON string, not an object. All inner keys optional
subTriggerEventTypestringNoFiner-grained sub-category. Seen in examples
deviceTypestringNoSeen 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.

Two documented ambiguities

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.

Impression
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.

GetAuthToken
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>"
}'
BDATA payload
{
"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.

FetchRewardInfo
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>"
}'
BDATA payload
{
"rewardId": "121783",
"virtualId": "pub_8f14e45fceea167a",
"channelName": "YOURCHANNEL"
}
rewardId changes type between endpoints

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 rewardcouponType 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 rewardcouponType 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.

Persist before you respond

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.

GetAllScratchCardsByUserId
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 token cache keyed by virtualId.

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

ScenarioExpected behaviour
Duplicate GetReward with the same extTransactionIdSame card set returned, no new cards
Reveal called twiceSecond call returns REWARD NOT FOUND; your app still shows the reward from your store
Reveal returns BETTER LUCK NEXT TIMEMiss UI, card marked revealed, no alert raised
Reveal times outNo 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-flowOne transparent re-mint, then success
Campaign pausedEmpty issuance response, nothing rendered

See Testing and the Go-live checklist.

API reference

EndpointAuthPurpose
getSecuredReferenceTokenSigned envelopeMint the channel token
GetRewardBearer channel tokenIssue temporary cards
GetAuthTokenSigned envelopeMint a user token
FetchRewardInfoBearer user tokenReveal a card
GetAllScratchCardsByUserIdBearer user tokenList 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.