Free rounds
Free rounds (free spins) let a player spin without staking their own cash: the stake is funded by a grant you issue, and the winnings are credited to the player. There are two sides, and only one of them is new work. Issuing is an API call, POST /v1/free-rounds. Receiving happens on the callback endpoint you already built: free spins arrive as ordinary bet and win callbacks carrying is_free: true, and there is no new endpoint to implement. If your wallet endpoint is not built yet, start with Callbacks.
The one rule that matters most: never debit the player’s real-money balance for a bet callback where is_free is true. The grant funds the stake; the amount on a free bet is the nominal stake for your reporting, not a charge. A naive integration that debits it silently takes real money from players on every free spin.
Issuing free rounds
Section titled “Issuing free rounds”Grant a player free rounds on one or more games. Generate and persist one unique AGGREGATOR_OPERATION_ID, then reuse it and the identical body only for retries. Replace the sample game and dates with enabled games and the approved campaign window; the dates below are illustrative. Platforms must also send the brand’s sub_operator_ref:
curl -X POST https://api.aggregator.gg/v1/free-rounds <<EOF \ --header @- \ -H "Idempotency-Key: ${AGGREGATOR_OPERATION_ID:?Set a persisted unique ID for this grant; reuse only for its retries}" \ -H "Content-Type: application/json" \ -d '{ "provider_code": "truelabs", "player_id": "player-123", "games": ["550e8400-e29b-41d4-a716-446655440000"], "count": 10, "currency": "EUR", "country": "DE", "coin_level": 1, "start_at": "2026-11-01T00:00:00Z", "end_at": "2026-11-10T00:00:00Z" }'Authorization: Bearer ${AGGREGATOR_API_KEY:?Set the issued key in the protected environment}EOFThe response returns a grant_id and a per-game status.
What the request means:
countis per game.games: [A, B]withcount: 10issues 10 rounds on each, 20 in total.- The grant is not tied to a session. Each game must support free rounds and passes the same jurisdiction check as a launch. Check
free_rounds_supporton the game (fromGET /v1/gamesorGET /v1/games/{id}) before issuing; a game where it isfalseis rejected at grant time. - Issuance is idempotent. The
Idempotency-Keyheader is required: the same key with the same body replays the cached result, and the same key with a different body returns409. Retry an ambiguous issuance with the same key and body and inspect the recorded grant result; never mint a fresh key to bypass an unresolved outcome. See Idempotency. - Availability is per provider. Free rounds must be enabled for the provider on your account first; until then issuance returns
403.
Step check: issue one grant, then repeat the exact same request with the same Idempotency-Key. The second response carries the same grant_id and no second grant appears in the list call below.
Stake models
Section titled “Stake models”Provide exactly one of coin_level or bet_amount. Which one, and the valid values, depend on the game, so call GET /v1/games/{game_id}/stakes?currency=EUR first and branch on the mode it returns:
mode |
Meaning | What you send |
|---|---|---|
enumerated |
The platform holds the valid list. | Pick one of the listed stakes and send the field named by stake_field: coin_level for TrueLabs, bet_amount for Apparat. |
provider_validated |
The platform has no list for this currency, so stakes is empty. |
Send an appropriate positive bet_amount in the documented units; the provider validates it at issue time and the platform mirrors its accept or reject. |
unsupported |
The game has no free-round stake model. | Do not issue on this game. |
bet_amount is a decimal string in major units, unlike callback amounts in integer minor units. Do not reuse the wallet conversion rule here. Mobule requires bet_amount plus explicit positive integer provider_params.betlevel and provider_params.rate; do not derive those two integers from the amount.
Step check: for one game of each provider you use, the stakes call returns a mode your issuing code has a branch for, and an issuance with a listed stake succeeds.
Managing grants
Section titled “Managing grants”- List grants with
GET /v1/free-rounds. - Cancel the unplayed remainder of a grant with
POST /v1/free-rounds/{grant_id}/cancel.
Cancellation is provider-dependent: inspect the HTTP result and each game’s state. Already activated or played rounds can still settle after a cancel or expiry. Mobule activates a game’s grant after launch and cannot cancel that game once activated. Unsupported cancellation can return 501; failed or ambiguous cancellation is not proof that no liability remains.
Step check: issue a small grant, cancel it, and verify the returned per-game states against the list call and provider evidence. Account for already activated games and outstanding wins rather than expecting all callbacks to stop.
What changes in callbacks
Section titled “What changes in callbacks”A free-round callback is the standard callback payload plus three fields:
| Field | Type | Description |
|---|---|---|
is_free |
boolean |
true when this callback belongs to a free round. This is the field you branch on. Absent or false on ordinary cash play. |
free_round_grant_id |
string |
The grant_id this spin belongs to; use it to group a campaign’s activity. May be empty when the spin is not tied to a specific grant. |
free_round_kind |
string or null |
The kind you set when issuing: freespin, freeticket, or voucher. |
action, transaction_type, transaction_id, amount, currency, player_id, provider_code, and session_id keep their normal callback meanings; amount is an integer in minor units, while identifiers and action fields are strings. Branch on transaction_type plus is_free, never on action. Signatures do not change either: every free-round callback carries X-SIGNATURE over the raw body, verified the same way as always.
Balance handling
Section titled “Balance handling”| Callback | is_free |
What to do |
|---|---|---|
bet |
true |
Do not touch the real-money balance. The grant funds the stake. Record the spin for reporting if you wish; amount is the nominal stake, not a charge. |
win |
true |
Credit the player by amount, exactly like a normal win. Optionally book it as bonus funds under your own wagering rules. |
bet or win |
absent or false |
Ordinary cash play: debit or credit as specified in Callbacks. |
Apply this table inside the complete wallet settlement boundary: authenticate raw bytes, validate trusted player/session context, atomically deduplicate and commit the movement with its original response, then return HTTP 200. A simple if/else balance update is not a complete callback handler. Refunds reverse only a recorded debit; a free bet does not justify a new cash credit.
The net effect of a correctly handled free round: the player risks nothing and keeps the winnings. Whether free-round winnings land as withdrawable cash or as restricted bonus funds subject to wagering is entirely your promotional accounting; Aggregator.gg imposes no model, and crediting them as cash is valid.
Step check: run one free-round campaign against a test player and confirm the real-money balance never decreased, every free win credited, and your reporting shows the nominal stakes under the grant_id.
Settlement shapes
Section titled “Settlement shapes”The callback sequence varies by game, and your handler must cope with both shapes:
- Both legs. A free
bet(is_free: true, nominal stake) followed, on a winning spin, by a freewin(is_free: true). - Win only. A single free
wincarrying the total winnings, withfinished: trueand no freebetbefore it. A zero-win batch still settles: you may receive a freewinwithamount: 0.
Use trusted integration/organization context, provider_code and transaction_id as the persistent identity, with is_free deciding the wallet effect. Do not require a matching free bet before accepting a free win. A retried free-round settlement carries the same transaction_id, so your standard deduplication covers it with no extra work.
A bet leg from a per-spin provider (record, do not debit):
{ "action": "BET", "transaction_type": "bet", "is_free": true, "free_round_grant_id": "9f1c0b3a-2d4e-4a6b-8c0d-1e2f3a4b5c6d", "free_round_kind": "freespin", "transaction_id": "provider-unique-tx-id", "amount": 20, "currency": "EUR", "player_id": "player-123", "provider_code": "apparat", "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}The free win (credit the player):
{ "action": "WIN", "transaction_type": "win", "is_free": true, "free_round_grant_id": "9f1c0b3a-2d4e-4a6b-8c0d-1e2f3a4b5c6d", "free_round_kind": "freespin", "transaction_id": "provider-unique-tx-id-2", "amount": 450, "currency": "EUR", "player_id": "player-123", "provider_code": "truelabs", "finished": true, "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}Checklist additions
Section titled “Checklist additions”Before a free-round campaign goes to real players, in addition to the standard go-live checklist:
- Your handler reads
is_freeon every callback and branches on it. - A
betwithis_free: truedoes not debit the player’s real-money balance. - A
winwithis_free: truecredits the player, as cash or as bonus funds per your promo rules. - You do not require a preceding free
betbefore accepting a freewin. - You handle a free
winwithamount: 0. - Your callback identity combines trusted integration/organization context,
provider_codeand opaquetransaction_id, with atomic movement and response storage. - You verified the signature on a free-round callback with a known body and secret pair.
For Mobule, issuance stores a pull-activated grant without an immediate provider call. A later supported launch carries the grant reference, and completion delivers a free win, including an eligible late win after expiry or cancellation. Check provider approval and environment enablement: a launch with free rounds disabled can be ordinary paid play even if an earlier grant exists. See the issue contract before launching a campaign.