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:

  1. The response arrives within the timeout for that callback.
  2. The HTTP status is 2xx (200 OK is the convention).
  3. The response body is one of:
    • empty, or any non-JSON body (it is ignored), or
    • a JSON object (Content-Type: application/json) without an error key.

A response fails when any of the following hold:

Failure semantics by status class:

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:

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:

Summary Checklist