Skip to main content

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

Burn, then deliver. Never the other way round.

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"
}
}
Persist before you render

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.

errorCodeMeaningCoinsUser message
COUPON_EXHAUSTEDStock ran out between your check and your callRefund"This offer just sold out"
NON_ELIGIBLETargeting rules exclude this userRefund"Not available for your account"
MAX_LIMIT_REACHEDPer-user purchase_limit reachedRefund"You've already claimed this offer"
INVALID_OFFEROffer withdrawn or unknownRefund + refresh catalogue"This offer is no longer available"
INVALID_REQUESTMalformed requestRefund + alertGeneric failure
UNAUTHORIZEDCredential problemRefund + alertGeneric failure
INTERNAL_ERRORPlatform-side failureRetry once, then refundGeneric failure
TimeoutUnknown outcomeReconcile — 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 offerId has 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:

ColumnWhy
extTransactionIdThe join key
virtualIdWhich user
offerIdWhich offer
coinsUsedWhat they paid
Burn stateCommitted, refunded
Delivery statePending, delivered, failed, reconciled
coupon_codeWhat they received
TimestampsRequested, 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