Issue a free-round grant
POST/free-rounds
Issue free rounds (free spins / free tickets) to a player on one or more
games (ADR-048). count is per game - games:[A,B] + count:10
issues 10 rounds on each (20 total); the response itemises the provider
ref per game. Not tied to a session, but each game must pass the same
compliance + capability gate as a launch.
Requires the write API-key scope (the standard operator key scope;
the granular free_rounds.write is also accepted for bonus-only keys)
and an Idempotency-Key header (financial idempotency - same key + same
body replays the cached result; same key + different body → 409). Off by
default per provider until onboarded.
Mobule issuance, including a previously issued grant replay, requires
current support approval for the exact organization bound to the operator
key. For a platform key, the approval of the platform’s organization covers
its brands; a suspended or retired brand is refused.
Denial returns PROVIDER_ACCESS_REQUIRED (403) before provider I/O;
authorization uncertainty returns PROVIDER_ACCESS_UNAVAILABLE (503).
Existing free-round history and cancellation remain available after revoke.
Mobule free rounds are pull-activated. Issuing makes no provider call:
we mint one freerounds_id per game (32 lowercase hex, returned as that
game’s provider_grant_ref), the player’s next POST /v1/sessions launch
of the game carries it to Mobule, and Mobule’s server then activates the
rounds (all rounds of that game are accounted at activation) and, once the
player has played them, completes them. The win is credited once, as a
freespin transaction linked to free_round_grant_id, and reaches your
wallet as a WIN with is_free: true and free_round_grant_id set; wins
are not credited during play. A Mobule grant requires bet_amount plus
integer provider_params.betlevel and provider_params.rate (both at
least 1) and rejects coin_level (400 invalid_stake alongside
bet_amount, bet_amount_required without it); a missing or
non-integer betlevel or rate is 400 provider_params_required. The
two integers are passed to Mobule as supplied, never derived from
bet_amount. A game can be cancelled until
Mobule activates its rounds, which follows the player’s launch (see
POST /v1/free-rounds/{grant_id}/cancel). The win of rounds Mobule
activated is credited even when it arrives after end_at, after the
grant expired or after a cancel. Off until enabled for Mobule in the
environment (403 free_rounds_disabled). The same switch governs the
launch: while free rounds are disabled for the environment, a launch
carries no freerounds_id and is ordinary paid play, even for a grant
issued earlier; rounds already carried to Mobule still settle.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Unique key for idempotent retry semantics. A retry with the same key and the same body returns the cached response.
Reuse with a DIFFERENT body is endpoint-specific, because the two
implementations differ. POST /v1/free-rounds hashes the request body
(src/platform_api/free_rounds_flows.py, _request_hash) and answers a
mismatch with 409 idempotency_conflict. POST /v1/sessions uses the
Redis guard (src/infra/idempotency.py), which does not compare
bodies: the cached response is replayed and the new body is not
processed, except for a cached Mobule launch, whose retry body (game,
player and a platform’s sub_operator_ref) is re-read to reauthorize
it: a mismatch answers 403 and storage uncertainty 503 (see
POST /v1/sessions). Never reuse a key across logically different
operations.
A request arriving while the same key is still in flight returns 409 idempotency_in_progress. Over the length limit returns 400 invalid_idempotency_key; missing entirely returns 400 missing_idempotency_key.
Request Bodyrequired
Section titled “Request Bodyrequired”object
apparatPlatform accounts only. Which brand’s players receive the grant.
Same rules and error codes as sub_operator_ref on
POST /v1/sessions (required for platform keys, rejected for
others); the grant lands on the brand’s identity so settlement
routes to the same brand the session did.
Because the grant is keyed on the brand, the same ref must be
sent to GET /v1/free-rounds and
POST /v1/free-rounds/{grant_id}/cancel to see or cancel it.
Our games.id UUIDs. Every game must support free rounds.
Rounds PER GAME (not total). games:[A,B] + count:10 ⇒ 20 total.
EURISO 3166-1 alpha-2 - gated at issue time (ADR-046).
TrueLabs stake level. Provide exactly one of coin_level / bet_amount. Rejected for Mobule (400).
Stake per round in MAJOR units. Required for Apparat (voucher stake) and for Mobule (nominal stake per round, stored on the grant as bet_amount and internally on its activation and win records, never sent to Mobule; Mobule’s betlevel/rate come from provider_params, never from this amount).
Provider-specific issuance parameters, passed through as supplied.
Mobule (required): betlevel (integer, at least 1, Mobule bet level)
and rate (integer, at least 1, Mobule denomination, the coin value).
Both are returned to Mobule verbatim when it activates the rounds
(freerounds.activate); missing or non-integer values are a 400
provider_params_required. We never derive them from bet_amount.
object
Responses
Section titled “Responses”Idempotent replay of a previously issued grant.
object
object
Per-game provider ref: Apparat voucherId; for Mobule the freerounds_id we minted (32 lowercase hex), carried on the player’s game launch and activated by Mobule once.
Grant issued for all games.
object
object
Per-game provider ref: Apparat voucherId; for Mobule the freerounds_id we minted (32 lowercase hex), carried on the player’s game launch and activated by Mobule once.
Grant partially issued - some games failed (see per-game status).
object
object
Per-game provider ref: Apparat voucherId; for Mobule the freerounds_id we minted (32 lowercase hex), carried on the player’s game launch and activated by Mobule once.
Validation failed, missing Idempotency-Key, or invalid stake (Mobule also bet_amount_required / provider_params_required).
object
object
Correlation ID for support escalation.
object
Missing, invalid, or revoked API key.
object
object
Correlation ID for support escalation.
object
Scope missing, provider disabled, jurisdiction blocked, or player self-excluded.
object
object
Correlation ID for support escalation.
object
Idempotency-Key conflict. Two causes, distinguished by the error
field: idempotency_in_progress when a request with the same key is
still in flight (retry shortly), and idempotency_conflict when the
key was already used with a different request body. Only
POST /v1/free-rounds detects the second case; POST /v1/sessions does
not compare bodies and replays its cached response instead, after
reauthorizing a cached Mobule launch (see POST /v1/sessions).
POST /v1/free-rounds answers with the flat FreeRoundError body;
POST /v1/sessions with a flat {error, message} body without
status, and a Retry-After header. A platform’s brand selector
answers the nested Error object on either endpoint.
On POST /v1/free-rounds for Mobule, whose issuance makes no provider
call, an idempotency_in_progress that persists means the issuance
stopped after creating the grant: its replay never gets a result, and
the grant stays pending_issue, never reaches a launch and is closed
as expired by the expiry sweep (every 30 minutes) after its
end_at. Issue a new grant under a new key once it has closed (see
POST /v1/free-rounds/{grant_id}/cancel).
The flat error body of the free-rounds endpoints’ own refusals (those of POST /v1/free-rounds/{grant_id}/cancel, and the idempotency_* 409s of POST /v1/free-rounds), not the nested Error object a platform’s brand selector answers with: error is the machine-readable code and status repeats the HTTP status.
object
The code, e.g. grant_not_found, not_cancellable, issuance_in_progress, provider_error, provider_unavailable.
issuance_in_progress409The grant’s status (409 only).
Mobule 409 only - the state of each game of the grant.
object
object
object
Correlation ID for support escalation.
object
Headers
Section titled “Headers”Seconds to wait before retrying; sent by POST /v1/sessions only.
Game does not support free rounds, abuse limit exceeded, or bet outside limits.
object
object
Correlation ID for support escalation.
object
Rate limit exceeded; consult Retry-After.
object
object
Correlation ID for support escalation.
object
Headers
Section titled “Headers”Seconds to wait before retry.
Provider rejected the issuance.
object
object
Correlation ID for support escalation.
object
Provider access authorization storage is temporarily unavailable.
object
object
Correlation ID for support escalation.