Coupon delivery
The purchase flow. This is the part of the deals module that deserves most of your engineering attention, because every failure path involves a user whose coins are already gone.
The prescribed ordering
Delivering before the burn is committed lets a user obtain inventory without paying — a crash or a dropped connection between the two leaves them with a coupon and their coins.
Burning without a coupon or a refund is equally unacceptable in the other direction. Every failure path must end in a retry with the same reference, or a refund.
The request
curl -X POST https://hotdeals.cheggout.com/api/fetchCouponCode \
-H "Content-Type: application/json" \
-H "X-CHANNELID: YOURCHANNEL" \
-H "Authorization: CheggoutAppKey REPLACE_APP_KEY:REPLACE_SEC_KEY" \
-d '{
"offerId": "517",
"virtualId": "pub_8f14e45fceea167a",
"channelName": "YOURCHANNEL",
"extTransactionId": "PUB_OFFERBUY_20260731_000045",
"coinsUsed": "10"
}'
extTransactionId must be unique per claim — not per user, not per offer. It is both your
idempotency key and the join key you will reconcile on later.
The response
{
"status": 200,
"message": "Fetched Successfully",
"data": {
"id": "517",
"coupon_code": "REPLACE-COUPON-CODE",
"coupon_validity": "2026-12-31T23:59:59.000Z",
"extTransactionId": "PUB_OFFERBUY_20260731_000045",
"purchase_time": "2026-07-31T14:15:31.000Z",
"url": "https://click.cheggout.com/REPLACE"
}
}
coupon_code is the user's purchased good. Write it to your store — with coupon_validity,
extTransactionId and purchase_time — before you render anything. If your process dies between
receiving and storing, the user paid for something you can no longer show them.
Handling every failure
Each of these leaves the user's coins burned. There is no outcome that leaves them whole without either a successful retry or a refund.
errorCode | Meaning | Coins | User message |
|---|---|---|---|
COUPON_EXHAUSTED | Stock ran out between your check and your call | Refund | "This offer just sold out" |
NON_ELIGIBLE | Targeting rules exclude this user | Refund | "Not available for your account" |
MAX_LIMIT_REACHED | Per-user purchase_limit reached | Refund | "You've already claimed this offer" |
INVALID_OFFER | Offer withdrawn or unknown | Refund + refresh catalogue | "This offer is no longer available" |
INVALID_REQUEST | Malformed request | Refund + alert | Generic failure |
UNAUTHORIZED | Credential problem | Refund + alert | Generic failure |
INTERNAL_ERROR | Platform-side failure | Retry once, then refund | Generic failure |
| Timeout | Unknown outcome | Reconcile — see below | "We're checking, one moment" |
Timeouts and reconciliation
A timeout is the one case where you genuinely do not know what happened. Do not guess.
Call getUserCoupons for that offer and user, and
look for your extTransactionId in the returned coupons[]. It is echoed on every coupon, so the
match is exact.
- Found — delivery succeeded. Persist the coupon and keep the coins burned.
- Not found — delivery did not complete. Retry with the same
extTransactionId, or refund.
Run this as a scheduled job over anything your ledger recorded as uncertain, not only inline.
Idempotency
Replaying extTransactionId returns the previously processed result, or is rejected as a duplicate.
TBD The published contract is ambiguous about which. Until it is clarified:
- Always retry with the same value. Generating a new one for the same claim risks issuing two coupons for one payment.
- Never reuse a value across different offers. A replay under a different
offerIdhas undefined behaviour. - Keep your own delivery ledger. Record every attempt, its reference, and its outcome. Reconcile against it rather than trusting platform idempotency alone.
A delivery ledger
The minimum you need to reconcile and to answer a customer dispute:
| Column | Why |
|---|---|
extTransactionId | The join key |
virtualId | Which user |
offerId | Which offer |
coinsUsed | What they paid |
| Burn state | Committed, refunded |
| Delivery state | Pending, delivered, failed, reconciled |
coupon_code | What they received |
| Timestamps | Requested, delivered, refunded |
Reconcile daily: every burn should have either a delivered coupon or a refund. Anything else is an open item and should alert.
Free offers
Offers with amount: 0 cost no coins. The flow is the same minus the burn — and consequently minus
the refund, which makes the error handling considerably simpler. If you are integrating
incrementally, free offers are a good first milestone.
Next
- My Coupons — showing and reconciling delivered coupons.
fetchCouponCode- Error codes