Callback Retries and Idempotency
The same callback may be delivered more than once. This is a normal part of reliable delivery, not an error in the SportAPI Coupon Settlement System.
For example, the partner may successfully store the batch, but its HTTP 200 response may be lost in transit. The system cannot know whether the request was processed, so it sends the same batch again.
The handler must be idempotent: a repeated batch must not modify coupons, bets, or balances again.
Two reasons why a coupon may appear again
It is important to distinguish:
| Situation | coupon_code | batchId | Required action |
|---|---|---|---|
| Repeated delivery of the same HTTP batch | Same | Same | Do not repeat business operations; return HTTP 200. |
| New settlement state of the coupon | Same | New | Update the coupon and bets as a new version. |
The same coupon_code may legitimately arrive several times with different batchId values, especially for an accumulator.
Purpose of batchId
batchId identifies a specific callback batch version.
The following is guaranteed:
- repeated HTTP delivery of one batch retains the same
batchId; - a new settlement state receives a new
batchId; - one batch may contain up to 100 coupons;
batchIdis not a coupon or bet ID.
Create a unique index on batchId in the incoming callback table.
Example:
CREATE TABLE coupon_callback_batches (
batch_id VARCHAR(64) PRIMARY KEY,
received_at TIMESTAMP NOT NULL,
coupon_count INTEGER NOT NULL
);
This is only a structure example. The partner chooses the table names and data types.
Why coupon_code is not sufficient
Do not use coupon_code as the unique delivery ID:
coupon_code = 000000000272
batchId = version_A
coupon_code = 000000000272
batchId = version_B
Both versions belong to the same coupon but contain different bet states. If the second version is rejected as a duplicate based on coupon_code, the local record will not be updated.
Use:
batchIdto deduplicate HTTP batches;coupon_codeto find and update a coupon;uuidto update a specific bet within the coupon.
Transactional processing
Registering batchId, updating coupons, and applying financial operations must be performed in one transaction.
Recommended algorithm:
- verify the HMAC signature;
- parse the JSON;
- validate
couponCountand thecouponsarray; - begin a transaction;
- attempt to insert
batchIdwith a unique constraint; - if
batchIdalready exists, do not repeat business operations; - process every entry in
coupons; - upsert coupons by
coupon_code; - upsert bets by the combination
coupon_code + uuid; - apply the required financial operations;
- commit the transaction;
- return HTTP
200.
Simplified flow:
BEGIN
INSERT batchId
if it already exists:
COMMIT
return 200
FOR EACH coupon:
UPSERT coupon BY coupon_code
FOR EACH event:
UPSERT event BY coupon_code + uuid
APPLY financial changes idempotently
COMMIT
return 200
Do not commit batchId in a separate transaction before storing the coupons. If the application stops after recording batchId but before updating the coupons, a repeated delivery will be incorrectly treated as fully processed.
Atomic batch processing
A batch may contain multiple coupons. HTTP 200 means that the partner successfully committed the entire batch.
If any entry cannot be stored:
- roll back the transaction;
- do not return HTTP
200; - return a temporary server status for which a retry is supported.
If the architecture cannot update all systems in one transaction, first store the incoming batch atomically in the partner’s internal inbox queue, then process the business operations from that queue idempotently.
Idempotency of financial operations
A unique batchId protects against repeated batch delivery, but financial operations require additional protection at the coupon level.
The reason is that one coupon may receive several different batchId values, such as intermediate and final snapshots.
The partner must separately record:
- whether the current final result has already been processed;
- whether the repeated debit on transition to status
15has already been performed; - whether the new
realWinafter recalculation has already been credited.
One possible approach is to store a local coupon settlement generation number:
coupon_code = 000000000272
settlement_generation = 0
On the first transition to status 15, increment the number:
settlement_generation = 1
Unique keys for financial operations can then be formed as follows:
000000000272:1:recalculation_debit
000000000272:1:final_credit
This protects the balance not only from a repeated batchId, but also from repeated processing of the same state in different parts of the system.
The partner chooses the specific implementation. The main rule is that one settlement transition must create no more than one financial operation of each type.
When retries occur
Delivery is retried after:
- a timeout;
- a connection failure or another transport error;
- HTTP
500; - HTTP
502; - HTTP
503; - HTTP
504.
These conditions are considered temporary.
Retry schedule
The first delivery is attempted immediately. After a failure, the following intervals are used:
1st attempt: immediately
2nd attempt: after 1 minute
3rd attempt: after 5 minutes
4th attempt: after 15 minutes
5th attempt: after 1 hour
No more than five HTTP delivery attempts are made, including the first one.
After all attempts are exhausted, automatic delivery stops. For recovery and reconciliation, use the coupon read methods described in Polling fallback.
Timeout
The complete callback HTTP request is given no more than 10 seconds.
The partner handler must have time to:
- verify the signature;
- store the batch atomically;
- return HTTP
200.
Perform long-running additional operations only after the batch has been stored reliably in the partner’s internal queue.
If the endpoint processes the batch but does not return a response in time, the request may be delivered again with the same batchId.
Behavior for HTTP responses
| Response or condition | Delivery behavior |
|---|---|
HTTP 200, empty body | Success. |
HTTP 200, success: true, processed = couponCount | Success. |
HTTP 200, success: false | Final error without an automatic retry. |
HTTP 200, processed < couponCount | Final error without an automatic retry. |
HTTP 401 | Invalid or missing signature; final error. |
HTTP 403 | Request rejected by IP filtering; final error. |
HTTP 500, 502, 503, 504 | Temporary error; delivery is retried. |
| Timeout or transport error | Delivery is retried. |
| Any other HTTP status | Final error without an automatic retry. |
The current contract requires HTTP 200 specifically. Responses 201, 202, and 204 are not considered successful and are not retried automatically.
Responding to a temporary error
Do not return HTTP 200 with this body:
{
"success": false
}
This response is treated as final and does not start a retry.
If the batch cannot be stored temporarily, return a supported temporary HTTP status, for example:
HTTP/1.1 503 Service Unavailable
After the endpoint recovers, the system retries delivery according to the schedule.
Successful acknowledgement
Minimum response:
HTTP/1.1 200 OK
The body may be empty.
Recommended extended response:
{
"success": true,
"processed": 3
}
processed must equal the received couponCount.
For a repeated batchId that has already been fully processed, also return HTTP 200. Do not repeat storage or financial operations.
Common integration errors
- A unique constraint exists only on
coupon_code. batchIdis stored before the business data in a separate transaction.- HTTP
200is returned before the transaction is committed. - Funds are credited again for a repeated
batchId. - Every new
batchIdis automatically treated as a new financial result. - Only
coupons[0]is processed, while the remaining batch entries are skipped. - A temporary error returns
success: falsewith HTTP200. - The endpoint returns
204even though the contract requires200. - Long-running business processing exceeds the timeout and causes a retry.
Checklist
batchIdhas a unique constraint.- A repeated
batchIdreturns HTTP200without repeated processing. - One
coupon_codemay have multiple versions. - The entire
couponsarray is processed atomically. - Coupons are updated by
coupon_code. - Bets are updated by
coupon_code + uuid. - Financial operations have their own unique keys.
- HTTP
200is returned only after reliable storage. - A temporary error returns
500,502,503, or504. success: falseis not used to request an automatic retry.
Next section: Polling fallback.