End-to-End Example: Recovery After a Missed Callback
Callbacks speed up result delivery, but they are not the only source of data. The current coupon state is stored in the SportAPI Coupon Settlement System and is available through the Client API.
If a callback was not delivered or processed, the partner must:
restore the endpoint
↓
determine the outage period
↓
retrieve current coupons through the API
↓
idempotently update coupons and bets
↓
perform only missing financial operations
↓
return to the normal callback + polling flow
When This Scenario Is Needed
Use this guide if:
- the callback endpoint was unavailable;
- the request exceeded the 10-second timeout;
- the application returned a temporary HTTP error;
- the callback was rejected because of an invalid signature or IP filter;
- the application returned HTTP
200before reliably storing the data; success: falseor an incompleteprocessedvalue was returned;- all automatic delivery attempts were exhausted;
- the partner’s internal handler temporarily stopped updating coupons;
- you need to check whether any settlement results were missed.
First Determine the Type of Problem
| Situation | Automatic retry |
|---|---|
| Timeout or transport error | Yes |
HTTP 500, 502, 503, 504 | Yes |
HTTP 200 with an empty body | No: delivery is considered successful |
HTTP 200, success: true, processed = couponCount | No: delivery is considered successful |
HTTP 200, success: false | No: final error |
HTTP 200, processed < couponCount | No: final error |
HTTP 401 or 403 | No: final error |
HTTP 201, 202, 204 | No: not considered successful, but not retried automatically |
| Any other HTTP status | No: final error |
If the handler temporarily cannot store a batch, it must return 500, 502, 503, or 504. Do not return HTTP 200 with success: false expecting a retry.
Automatic Retry Schedule
No more than five HTTP deliveries are attempted after a temporary error:
1st: immediately
2nd: after 1 minute
3rd: after 5 minutes
4th: after 15 minutes
5th: after 1 hour
After all attempts are exhausted, automatic delivery of that batch stops. Recover its data through the API read methods.
Step 1. Restore the Callback Endpoint
Check that:
- the endpoint is accessible externally;
- production uses HTTPS;
- the route accepts
POSTandapplication/json; - the body is read as raw bytes;
X-Coupon-Signatureis verified before JSON parsing;- the correct
callback_secretis used; - the IP allowlist, if enabled, contains the current address provided by the manager;
- the transaction is committed before HTTP
200is returned; - the complete handler finishes within 10 seconds;
batchIdhas a unique constraint.
Do not disable HMAC verification to speed up recovery.
If the batch is already fully stored and arrives again with the same batchId, do not repeat processing; return HTTP 200.
Step 2. Determine the Outage Boundaries
Record:
- the time of the last known successful callback;
- the time when the endpoint was restored;
- the duration of the outage;
- whether fallback polling ran successfully;
- which coupons remained active or were waiting for a new result;
- which
batchIdvalues had been stored before the outage.
Add a small time margin before the beginning of the period. Overlap is safe when processing is idempotent.
Choosing a Recovery Method
| Situation | Recommended method |
|---|---|
| Interruption of no more than 120 minutes | GET /api/partner/coupons/calculated |
| Interruption over 120 minutes, coupon codes known | POST /api/partner/coupons/results with coupon_ids |
| Coupons created during the outage must be recovered | POST /api/partner/coupons/results with dates |
| One problematic coupon | GET /api/partner/coupons/get |
| Unfinished coupons must be reconciled | GET /api/partner/coupons/active |
All these methods require a Bearer JWT.
Scenario A. The Interruption Did Not Exceed 120 Minutes
Assume:
callback unavailable for 40 minutes
current time: 14:00
Request coupons settled during the last 50 minutes:
BASE_URL="https://coupon-api.example.com"
TOKEN="<jwt-token>"
curl --request GET \
--url "$BASE_URL/api/partner/coupons/calculated?time=50" \
--header "Accept: application/json" \
--header "Authorization: Bearer $TOKEN"
The additional 10 minutes overlap the outage boundary.
This method:
- filters by final settlement time;
- does not depend on coupon creation time;
- does not return active coupons;
- returns an array of complete models directly in
body; - limits the window to a maximum of 120 minutes.
Response Example
{
"code": 1,
"body": [
{
"coupon_code": "000000000272",
"amount": 10,
"real_win": 18.5,
"calculate_coef": 1.85,
"status": 2,
"calculate_date": 1784973600000,
"events_data": [
{
"id": 912,
"status": 1,
"calc_coef": 1.85,
"calculate_date": 1784973600000
}
]
}
],
"error_code": null,
"error_message": null,
"date": 1784973600100,
"time_ms": 12,
"path": "/api/partner/coupons/calculated"
}
Process every entry in the body array.
An empty array with code = 1 is a successful response and means that no matching coupons exist in the window.
Scenario B. The Interruption Exceeded 120 Minutes
One /calculated request cannot cover a period longer than 120 minutes.
Retrieve coupon codes from the local database for coupons:
- that were active before the outage;
- that have no confirmed current final result;
- that are waiting after status
15; - whose state is uncertain.
Split the list into batches of no more than 100 codes.
curl --request POST \
--url "$BASE_URL/api/partner/coupons/results" \
--header "Authorization: Bearer $TOKEN" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"coupon_ids": [
"000000000272",
"000000000273",
"000000000274"
]
}'
Response:
{
"code": 1,
"body": {
"query_type": "ids",
"coupons": []
},
"error_code": null,
"error_message": null,
"date": 1784973600100,
"time_ms": 10,
"path": "/api/partner/coupons/results"
}
Important behavior:
- unknown coupon codes and codes owned by another client are skipped;
- response order may differ from
coupon_ids; - the number of results may be smaller than the number of codes;
- match results by
coupon_code, not by array index.
If Some Coupon Codes Were Not Stored
To recover coupons created during the outage, search by creation period:
curl --request POST \
--url "$BASE_URL/api/partner/coupons/results" \
--header "Authorization: Bearer $TOKEN" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"start_date": 1784880000000,
"end_date": 1784966400000
}'
Each interval:
- must be positive;
- cannot exceed 24 hours;
- refers to creation time, not settlement time.
For a longer period, make several requests using intervals no longer than 24 hours.
Important. A search by creation date will not find an older coupon merely because it was settled during the outage. Use
coupon_idsfor older known coupons.
Scenario C. One Problematic Coupon
COUPON_CODE="000000000272"
curl --request GET \
--url "$BASE_URL/api/partner/coupons/get?coupon_code=$COUPON_CODE" \
--header "Accept: application/json" \
--header "Authorization: Bearer $TOKEN"
This method returns one complete current model directly in body.
Use it:
- when a user contacts support;
- when the callback and local database disagree;
- to obtain
amount, which is absent from callbacks; - for a targeted check after status
15.
Reconciling Active Coupons
After recovery, request:
curl --request GET \
--url "$BASE_URL/api/partner/coupons/active" \
--header "Accept: application/json" \
--header "Authorization: Bearer $TOKEN"
Compare the response with coupons considered active in the local system.
If a completed coupon remains active only in the local database, retrieve it by coupon_code or through /results.
Do not use /active instead of searching for settled results: completed coupons are not included.
Step 3. Apply the Current State Idempotently
API read responses do not contain batchId. They return the current coupon state.
For each coupon:
- find the local record by
coupon_code; - update the coupon fields;
- iterate over all of
events_data; - update bets by
coupon_code + id; - determine whether a new financial action is required;
- commit the data and financial operation in one transaction.
Bet ID correspondence:
API: events_data[].id = 912
callback: events_data[].uuid = "912"
Do not create separate records for the same ID merely because its type differs.
Separate Idempotency for Financial Operations
Unique batchId values protect only against repeated delivery of one callback.
The following sequence is possible after recovery:
1. The result is obtained through polling.
2. The partner credits real_win.
3. A callback with a batchId unknown to the partner arrives later.
4. The callback contains the settlement result that was already applied.
If only batchId is checked, the payout will be credited twice.
Financial actions need their own unique key, for example:
coupon_code + settlement_generation + operation_type
Example:
000000000272:0:final_credit
When the same financial state appears again:
- update any required data;
- register the new
batchIdif this is a new callback; - do not repeat the debit or credit.
Applying Coupon Statuses
| Coupon status | Meaning | Recovery action |
|---|---|---|
0 | The coupon is active or an accumulator is partially settled | Update the data; do not make a payout. |
2 | Win | Credit real_win once if this result has not yet been processed. |
4 | Loss | Record the result; the credit is 0. |
8 | Return | Credit real_win once if this result has not yet been processed. |
15 | Coupon returned for recalculation | Run the separate recalculation flow. |
Do not determine the result from real_win alone. Always check status.
Recovering Status 15
If the API returns:
coupon status = 15
corresponding bet status = 4
and the previous final result was already processed financially:
- check whether the repeated deduction for this transition has already been made;
- if not, deduct
amountonce again; - move the local coupon into a state awaiting a new result;
- do not make a final credit while status is
15; - continue checking the coupon by
coupon_code; - after the next final status, credit the new
real_winonce.
If the repeated deduction was already made through a callback or an earlier polling request, do not make it again.
Special Case: Losing Accumulator
An accumulator loss may be known before all bets have been settled:
coupon status = 4
bet statuses = [1, 2, 0]
The financial result is already a loss.
Later, the complete model may become:
coupon status = 4
bet statuses = [1, 2, 1]
Update the last bet, but do not create a second financial loss operation.
If Callback and API States Disagree
Do not determine the current state only by HTTP request arrival order.
For example, an older callback may arrive late after polling. If the states differ:
- do not perform a financial action immediately;
- request the current model through
GET /api/partner/coupons/get; - synchronize the local coupon with the API response;
- apply only the missing financial operation.
calculate_date is useful for reconciliation, but the business decision must consider the coupon status, bet statuses, and the settlement cycle already processed.
Step 4. Return to Normal Operation
After reconciliation:
- make sure the callback endpoint responds reliably;
- verify HMAC on new callbacks;
- continue deduplication by
batchId; - start regular fallback polling;
- use overlapping time windows.
Recommended example:
every 5 minutes:
GET /api/partner/coupons/calculated?time=10
Do not advance the local polling window if a request ends with a transport or server error. First repeat the check successfully with overlap.
How to Confirm Recovery Is Complete
Check that:
- all coupons in the period are matched by
coupon_code; - all locally active coupons have been reconciled;
- every
events_dataentry is updated; - no final statuses remain unprocessed;
- no status
15remains without a scheduled follow-up check; - every financial operation has a unique record;
- rerunning the procedure does not change the balance again;
- new callbacks are accepted and receive HTTP
200; - fallback polling is running on schedule again.
The last point is especially important: the recovery procedure must be idempotent. Running it again with the same data must not create new debits or credits.
Common Mistakes
- Requesting
/calculated?time=120and assuming it covers a multi-day outage. - Searching for results only by coupon creation time.
- Matching a
/resultsresponse by array position. - Processing only the first coupon or first bet.
- Creating separate records for callback and polling data.
- Treating every unknown
batchIdas a new financial result. - Crediting a result again after it was already applied through polling.
- Ignoring status
15. - Overwriting a newer state with an older callback.
- Returning HTTP
200before reliably storing the batch. - Disabling HMAC during emergency recovery.
Short Runbook
1. Fix the callback endpoint.
2. Determine the start and end of the outage.
3. If the outage <= 120 minutes:
call /calculated with a safety margin.
4. If the outage > 120 minutes:
request known coupon_code values in batches of up to 100.
5. If necessary, recover created coupons using windows of up to 24 hours.
6. Reconcile /active.
7. Upsert coupons by coupon_code.
8. Upsert bets by coupon_code + id.
9. Apply only missing financial operations.
10. Resume callbacks and overlapping polling.
Checklist
- The reason for the missed callback has been identified.
- HMAC verification was not disabled.
- The outage period is known.
- A short outage is covered by a window no longer than 120 minutes.
- A long outage is recovered using known
coupon_codevalues. - Requests by code are split into batches of no more than 100 entries.
- Creation periods are split into intervals of no more than 24 hours.
- Responses are matched by
coupon_code. - All coupons and bets are processed.
- API
idis matched with callbackuuid. - Financial idempotency does not rely only on
batchId. - Status
15is handled in a separate cycle. - Repeating the procedure does not change the balance.
- The callback responds with HTTP
200again. - Fallback polling is running again with overlap.
More information:
- Retries and Idempotency;
- Fallback Polling;
- Retrieving Coupons by List and Period;
- Active and Recently Settled Coupons.
Next section: Endpoint Reference.