Callback Testing Contract
This page defines the exact contract used to verify a client callback implementation: the test payloads Scout sends to your endpoints and the validation rules that decide whether a response passes. Scout's verification tooling checks your integration against this contract, so a passing implementation here means a working integration.
All verification runs use the staging environment and the staging JWT secret.
Hosted Validator
You can run every check on this page yourself, without waiting for Scout, using the hosted Callback Validator:
https://customer-game.api.scoutgg-stg.net/callback-validator
Use the staging URL above — test callbacks are delivered from staging only. The tool first verifies a JWT produced by your backend against the shared secret (signature, algorithm, expiry and required claims), then sends the test payloads below to your callback URL and shows the pass/fail result together with your endpoint's actual response. Sending test callbacks requires a valid token from the first step, so only holders of the shared secret can trigger them.
Callback delivery is deliberately restricted to staging: on production deployments the tool refuses to send test callbacks (403 validator.callbacks_disabled) and only the token check remains available. Run your verification against staging, with your staging secret and a staging callback URL.
What Scout Sends
Every callback is an HTTPS POST with a JSON body and the following headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
Accept |
application/json |
Authorization |
Bearer customer {token} — a JWT signed with your shared secret; role is user for Place Bet (triggered by a user action) and admin for all other callbacks (Refund Bet, Refund Tournament, Settle Bet) |
X-Request-Id |
A UUID identifying this delivery; log it and include it when reporting issues |
X-Callback-Event |
The event that triggered the callback (see table below) |
| Callback | X-Callback-Event |
Endpoint | Timeout |
|---|---|---|---|
| Place Bet | create:fantasy_team |
https://{your_server}/bets |
2 minutes |
| Refund Bet | destroy:fantasy_team |
https://{your_server}/bets/cancel |
2 minutes |
| Refund Tournament | cancel:tournament |
https://{your_server}/bets/cancel (or a separate endpoint, if registered) |
5 minutes |
| Settle Bet | winners:tournament |
https://{your_server}/winners |
5 minutes |
Payloads may contain additional top-level or nested fields beyond those documented (for example country, state or reporting fields). Your endpoint must tolerate and ignore unknown fields — rejecting a payload because of an unrecognised field is a validation failure.
What Counts as a Pass
A callback delivery passes when all of the following hold:
- The response arrives within the timeout for that callback.
- The HTTP status is 2xx (
200 OKis the convention). - The response body is one of:
- empty, or any non-JSON body (it is ignored), or
- a JSON object (
Content-Type: application/json) without anerrorkey.
A response fails when any of the following hold:
- The HTTP status is not 2xx.
- The status is 2xx but the JSON body contains an
errorkey — a200 OKcarrying{"error": ...}is treated as a failure, not a success. - The connection times out or is refused.
Failure semantics by status class:
- 4xx — business rejection. The action is not retried; the
errorvalue may be shown to the user. Use402with{"error": "insufficient_funds"}when the user cannot afford the bet. - 5xx / timeout — technical failure. The outcome is treated as unknown: Place Bet entries are rolled back, and the callback is re-sent later, so the endpoint must be idempotent.
Expected response shapes
| Callback | Minimal passing response | Optional response fields |
|---|---|---|
| Place Bet | 200 OK, empty body or {} |
secrets (object): opaque state stored per bet and echoed back in Settle/Refund payloads. custom (object): stored on the bet for reporting. |
| Refund Bet | 200 OK, empty body or {} |
None — the body is ignored unless it contains error. |
| Refund Tournament | 200 OK, empty body or {} |
None — the body is ignored unless it contains error. |
| Settle Bet | 200 OK, empty body or {} |
None — the body is ignored unless it contains error. |
Test Payloads
The verification scenario is one user entering one paid tournament, the tournament settling with a cash win, and the refund paths. The same identifiers are used consistently across the four payloads so bookkeeping can be traced end to end: user u-1001, tournament 90210, bet 5100001, buy-in 5.00 EUR with rake 0.50.
The rake is charged on top of the buy-in: the total amount debited from the user is always bet.amount + bet.rake — here 5.00 + 0.50 = 5.50 EUR. Where the payload carries pool.cost (5.5), that is this same total per entry. Refunds credit the same total back.
1. Place Bet — POST https://{your_server}/bets
{
"bets": [
{
"user": {
"client": "customer",
"uid": "u-1001",
"name": "test_user_01",
"ip": "203.0.113.10"
},
"pool": {
"client": "customer",
"game": "fantasy",
"uid": 90210,
"currency": "EUR",
"name": "Verification Test Tournament",
"overdraw": "100.00",
"category": "admin_created",
"amount": 5.0
},
"bet": {
"uid": 5100001,
"uuid": "90210|f47ac10b-58cc-4372-a567-0e02b2c3d479",
"base_amount": 5.0,
"amount": 5.0,
"rake": 0.5,
"currency": "EUR"
}
}
]
}
Pass: 200 OK after debiting 5.50 EUR (5.00 buy-in + 0.50 rake) from user u-1001. Optionally return {"secrets": {"txn": "your-transaction-ref"}} — in production the platform stores this object and echoes it back in the Settle Bet and Refund Bet payloads. The validator does not verify the echo; its Settle Bet payload carries an empty secrets object.
Also sent by the validator: a free-to-play variant (amount, base_amount, rake all 0.0), which must return 200 OK without debiting anything.
Verify yourself — the validator sends deterministic payloads but cannot see your ledger, so these checks are on you:
- Re-send the identical payload (trigger the same scenario again) and confirm it returns
200 OKwithout a second debit (idempotency onbet.uid). - Your endpoint rejects a user with insufficient funds with
402and{"error": "insufficient_funds"}(any snake_cased key; the status code must be 4xx). The validator has no unfunded-user scenario — pointing it at such an endpoint state shows the402as a failed delivery, which is the correct production semantics for a business rejection.
2. Refund Bet — POST https://{your_server}/bets/cancel
{
"pools": [
{
"pool": {
"client": "customer",
"status": "waiting",
"game": "fantasy",
"uid": 90210,
"currency": "EUR",
"category": "admin_created",
"overdraw": "100.00"
},
"bets": [
{
"user": {
"client": "customer",
"uid": "u-1001",
"name": "test_user_01"
},
"bet": {
"uid": 5100001,
"amount": 5.0,
"rake": 0.5,
"currency": "EUR",
"base_amount": 5.0,
"base_rake": 0.5,
"uuid": "90210|f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
}
]
}
]
}
Pass: 200 OK after crediting the full 5.50 EUR (5.00 buy-in + 0.50 rake) back to user u-1001.
Verify yourself: re-send the identical payload and confirm it returns 200 OK without a second credit.
3. Refund Tournament — POST https://{your_server}/bets/cancel
Same wire shape as Refund Bet with the tournament cancelled and the richer pool/bet fields:
{
"pools": [
{
"pool": {
"client": "customer",
"game": "fantasy",
"gameType": "football",
"status": "cancelled",
"uid": 90210,
"currency": "EUR",
"category": "admin_created",
"cost": 5.5,
"overdraw": "100.00"
},
"bets": [
{
"user": {
"client": "customer",
"uid": "u-1001",
"name": "test_user_01"
},
"bet": {
"uid": 5100001,
"uuid": "90210|f47ac10b-58cc-4372-a567-0e02b2c3d479",
"currency": "EUR",
"amount": 5.0,
"rake": 0.5,
"base_amount": 5.0,
"base_rake": 0.5
}
}
]
}
]
}
Pass: 200 OK after refunding every bet in the payload exactly once (5.50 EUR per bet: amount + rake).
4. Settle Bet — POST https://{your_server}/winners
{
"pools": [
{
"pool": {
"client": "customer",
"game": "fantasy",
"uid": 90210,
"name": "Verification Test Tournament",
"currency": "EUR",
"overdraw": "100.00",
"category": "admin_created",
"cost": 5.5,
"amount": 5.0,
"gameType": "football",
"overlay": false,
"guaranteed": true,
"distribution": "normal",
"tags": [],
"competitions": [
{
"sport": "football",
"name": "Verification League",
"period": 2026,
"uid": "61cad74a46cef3b89efbc8fc"
}
]
},
"bets": [
{
"user": {
"client": "customer",
"uid": "u-1001",
"name": "test_user_01",
"ip": "203.0.113.10"
},
"bet": {
"uid": 5100001,
"uuid": "90210|f47ac10b-58cc-4372-a567-0e02b2c3d479",
"currency": "EUR",
"amount": 5.0,
"rake": 0.5,
"payout": "50.00",
"base_payout": "50.00",
"rank": 1,
"score": 46,
"secrets": {}
}
},
{
"user": {
"client": "customer",
"uid": "u-1002",
"name": "test_user_02",
"ip": "203.0.113.11"
},
"bet": {
"uid": 5100002,
"uuid": "90210|9b2e61ad-4c3f-4d1e-8a6c-2f60cf1f2a55",
"currency": "EUR",
"amount": 5.0,
"rake": 0.5,
"payout": "0.00",
"base_payout": "0.00",
"rank": 2,
"score": 31,
"secrets": {}
}
}
]
}
]
}
Pass: 200 OK after crediting 50.00 EUR to user u-1001 and nothing to u-1002 (participants with payout: "0.00" are informational and must not be credited).
Verify yourself: re-send the identical payload and confirm it returns 200 OK without a second payout.
Production behaviour not exercised by the validator:
- If your Place Bet response returned
secrets, production echoes the same object inbet.secretshere. The validator's payload always carries an emptysecretsobject and does not verify the echo. - For mixed-payout tournaments production sends
distribution: "mixed"payloads withqualifier_ticketandpayout_textwinners — see Mixed Payouts; the pass rule is the same, plus no cash credit for bets carrying a ticket or manual prize (payoutis"0.00"). The validator has no mixed-payout scenario.
Summary Checklist
- Respond within 2 minutes (Place/Refund Bet) or 5 minutes (Settle, Refund Tournament).
- Return
2xxwith an empty body, a non-JSON body, or a JSON object without anerrorkey. - Never return
2xxwith{"error": ...}— it counts as a failure. - Reject business errors with
4xx+{"error": "snake_cased_key"}; reserve5xxfor technical faults. - Process each
bet.uidat most once per action — duplicates must succeed without side effects. - Ignore unknown fields anywhere in the payload.