Free round campaigns
API · Operator → Boss HubCreates a campaign that gives players free spins. You query a campaign’s state with GET /v1/freerounds/{id} and cancel its unused spins with DELETE /v1/freerounds/{id}. With /v1/freerounds/{id}/players you add, list and cancel individual players. Requires the promo module and the freerounds.write scope; this scope is not on by default, ask Boss Hub for it.
Headers
| Header | Description |
|---|---|
| X-Operator-Id | Your operator ID |
| X-Timestamp | Unix seconds. Rejected outside ±30 s |
| X-Signature | hex(HMAC_SHA256(secret, ts + "." + METHOD + "." + path?query + "." + body)) |
| X-Request-Id | Required on POST and DELETE. The same ID and body return the first response, so no second campaign is created |
| X-Api-Key | Optional. Selects the right secret when you have several keys |
{
"id": "cmp_mrd_aralik",
"name": "Aralık Hoş Geldin",
"site_id": "site_mrd01",
"template_id": null,
"games": ["sultans-ember"],
"spins": 20,
"bet_value": 0.20,
"currency": "USD",
"players": ["u_88213"],
"starts_at": "2026-12-01T00:00:00Z",
"valid_days": 7,
"reference": "crm-dec-welcome-88213",
"status": "scheduled",
"spins_used": 0,
"won_total": 0.00
}{
"code": "VALIDATION_ERROR",
"message": "The submitted data is invalid.",
"request_id": "req_01HF3R1B",
"details": { "reason": "BET_OVER_LIMIT", "max_usd": "5.00" }
}Campaign status
GET /v1/freerounds/{id} returns the campaign as it stands now. POST and GET responses contain the request body fields plus the following:
| Field | Type | Description |
|---|---|---|
| id | string | Campaign ID. Used as campaign_id in callbacks. |
| status | string | draft, scheduled, active, finished, cancelled. |
| spins_used | integer | Spins used. |
| won_total | decimal(18,2) | Total winnings, in the campaign currency (cancelled grants included). |
| approval_id | string | Only on a campaign over the budget warning: it returns as draft and the Owner’s approval is tracked with this ID. |
| already_processed | boolean | Only in the 200 response to a repeated request with the same reference and the same content: true. |
Safe retries
If you send a reference, you can safely retry the same request after a timeout. The same reference with the same content returns the first campaign with 200 and already_processed: true; you get the same campaign even if it was cancelled in the meantime; approval_id is returned while the approval is still pending. The order of games and players, a repeated player and the way the amount is written (0.5, 0.50) do not count as different content. The same reference with different content returns 409 REFERENCE_CONFLICT and no campaign is created. References are separate for Test and Live. A request rejected by validation before the campaign is written does not use up its reference. One exception: if no player can be granted (PLAYERS_REQUIRED), the campaign stays as a draft (with your reference, if you sent one); a retry with the same reference returns that draft, and every retry without a reference leaves a new draft. Cancel the draft with DELETE. X-Request-Id records expire, a reference does not.
{
"id": "cmp_mrd_aralik",
"name": "Aralık Hoş Geldin",
"site_id": "site_mrd01",
"template_id": null,
"games": ["sultans-ember"],
"spins": 20,
"bet_value": 0.20,
"currency": "USD",
"players": ["u_88213"],
"starts_at": "2026-12-01T00:00:00Z",
"valid_days": 7,
"reference": "crm-dec-welcome-88213",
"status": "scheduled",
"spins_used": 0,
"won_total": 0.00,
"already_processed": true
}{
"code": "REFERENCE_CONFLICT",
"message": "A campaign with different content was already created with this reference.",
"request_id": "req_01HF3R1C",
"details": { "reference": "crm-dec-welcome-88213" }
}Cancelling a campaign
DELETE /v1/freerounds/{id} cancels the campaign and every pending or active grant, and returns 204 with no body. If a player has an open round, the win of that round is still paid. The campaign.finished webhook is sent with status: cancelled. If the campaign started in the meantime, you get 409 CAMPAIGN_CHANGED: retry.
DELETE /v1/freerounds/cmp_mrd_aralik HTTP/1.1 Host: staging.example.com X-Operator-Id: op_meridyen X-Timestamp: 1762344300 X-Signature: 1fb2d7…6c9a X-Request-Id: req_01HF3R2C
Player grants
Every player in a campaign has their own grant with its own status. Only an active grant gives spins; a free round bet on a grant in any other status is rejected by Boss Hub and never reaches your wallet.
| Status | Meaning |
|---|---|
| pending | The campaign is a draft or scheduled; it becomes active when the campaign starts |
| active | Spins can be used |
| used | The spins are used up and the player has no open free round round |
| expired | The grant’s expires_at or the campaign end has passed |
| cancelled | The player’s grant or the whole campaign was cancelled |
| failed | The grant was written but the provider rejected the player; the reason is in fail_reason |
- One grant per player per campaign. A player repeated in one request becomes one grant. Adding a player again is not an error: it returns exists with the existing grant’s status; a cancelled, expired or used grant is not renewed either. To give a player spins again, create a new campaign.
- Players skipped at creation: a player whose currency none of the campaign’s games supports, or for whom the bet value is invalid in their currency, is not written to the campaign and is missing from the players array of the response; no reason is returned. Compare players with your request, or create the campaign and add the players with POST /v1/freerounds/{id}/players to get a result per player.
Adding players
POST /v1/freerounds/{id}/players adds players to a draft, scheduled or open campaign, up to 100 per request. The response is 200 with a result per player: added a new grant, exists the player already has a grant in this campaign (grants do not multiply, status is the existing grant’s status), failed no grant was given. failed reasons: CURRENCY_NOT_SUPPORTED none of the campaign’s games supports the player’s currency or there is no exchange rate, BET_INVALID the spin value rounds down to zero in the player’s currency, BET_NOT_OFFERED the converted value is not one of the provider’s bet values, PROVIDER_REJECTED: … the provider rejected the player. Top-level fields: added new grants written in this request, failed failures in this request, total valid grants now in the campaign (cancelled and failed excluded). A player with a record gets a grant in their own currency; a player with no record yet gets the campaign currency. The same X-Request-Id returns the first response.
POST /v1/freerounds/cmp_mrd_aralik/players HTTP/1.1
Host: staging.example.com
Content-Type: application/json
X-Operator-Id: op_meridyen
X-Timestamp: 1762344600
X-Signature: 7d41a0…b3e2
X-Request-Id: req_01HF3R3D
{
"players": ["u_90412", "u_88213", "u_77120"],
"expires_at": "2026-12-05T23:59:59Z"
}{
"added": 1,
"failed": 1,
"total": 2,
"items": [
{ "player_id": "u_90412", "result": "added", "status": "pending", "reason": null },
{ "player_id": "u_88213", "result": "exists", "status": "pending", "reason": null },
{ "player_id": "u_77120", "result": "failed", "status": null, "reason": "CURRENCY_NOT_SUPPORTED" }
]
}Listing players
GET /v1/freerounds/{id}/players returns the campaign’s grants. For the next page, send the next_cursor value of the response as cursor; null means the list has ended.
| Field | Type | Description |
|---|---|---|
| player_id | string | Player ID on the operator side. |
| status | string | Grant status, see the table above. |
| currency | ISO 4217 | Grant currency: the player’s currency, or the campaign’s if the player has no record yet. |
| spins_left | integer | Spins left. |
| won_total | decimal(18,2) | Total winnings from this grant, in currency. |
| expires_at | date-time | End of the grant: the campaign end unless the grant has its own. |
| cancelled_at | date-time | null | When it was cancelled. |
| fail_reason | string | null | Only failed: the provider’s reason. |
GET /v1/freerounds/cmp_mrd_hosgeldin/players?player_id=u_88213 HTTP/1.1 Host: staging.example.com X-Operator-Id: op_meridyen X-Timestamp: 1762344700 X-Signature: 0c5e19…94af
{
"items": [
{
"player_id": "u_88213",
"status": "active",
"currency": "USD",
"spins_left": 38,
"won_total": 38.40,
"expires_at": "2026-11-30T23:59:59.999Z",
"cancelled_at": null,
"fail_reason": null
}
],
"next_cursor": null
}Cancelling a single player
DELETE /v1/freerounds/{id}/players/{player_id} cancels the player’s unused spins and returns 204. The win of an open round is still paid. Only a pending or active grant can be cancelled; others get 409 GRANT_CLOSED with the grant’s status in details.status. In a campaign whose grants are run by the provider, the cancellation is passed on to the provider; if the provider cannot cancel a single player you get 409 PROVIDER_CANCEL_UNSUPPORTED and can cancel the whole campaign instead.
DELETE /v1/freerounds/cmp_mrd_hosgeldin/players/u_88213 HTTP/1.1 Host: staging.example.com X-Operator-Id: op_meridyen X-Timestamp: 1762344800 X-Signature: 5a9d33…e170 X-Request-Id: req_01HF3R4E
Effect on the wallet
- A free round bet arrives through bet with is_freeround: true, campaign_id set and amount: 0. Do not debit the balance. The spin value is in the freeround_bet_value field.
- The win arrives through a normal win; credit it to the balance.
- A spin is used up when the bet is OK and comes back when a rollback is OK. A rollback refunds no cash.
- A bet on a cancelled or expired grant never reaches you: Boss Hub rejects it.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
| INVALID_SIGNATURE | 401 | Signature could not be verified |
| TIMESTAMP_SKEW | 401 | X-Timestamp is outside ±30 s |
| IP_NOT_ALLOWED | 403 | IP is not on the allowlist |
| SCOPE_MISSING | 403 | The freerounds.write scope is missing |
| SCOPE_DENIED | 403 | The campaign or site_id is outside your key’s scope |
| MODULE_DISABLED | 403 | The promo module is disabled |
| NOT_FOUND | 404 | No such campaign, or it is in the other environment; the player has no grant in this campaign (cancelling a player); site_id is not yours (POST) |
| REQUEST_ID_CONFLICT | 409 | Same X-Request-Id, different body |
| REQUEST_IN_PROGRESS | 409 | The same X-Request-Id is still being processed |
| REFERENCE_CONFLICT | 409 | Same reference, different content |
| CAMPAIGN_CLOSED | 409 | Cancelling the campaign, adding players and cancelling a player: the campaign has finished or was already cancelled |
| CAMPAIGN_CHANGED | 409 | The campaign changed status in the meantime (started or opened); retry |
| GRANT_CLOSED | 409 | Cancelling a player: the grant is not pending or active |
| BUDGET_WARNING | 409 | Adding players: the estimated cost exceeds the budget warning |
| FREEROUND_ADD_PLAYERS_UNSUPPORTED | 409 | Adding players: the provider does not add players to an open campaign |
| PROVIDER_CANCEL_UNSUPPORTED | 409 | Cancelling a player: the provider does not support it |
| VALIDATION_ERROR | 422 | Missing or invalid field (details.fields), or a rule violation (details.reason: SPINS_OVER_LIMIT, BET_INVALID, BET_OVER_LIMIT, BET_NOT_OFFERED, GAME_NOT_ELIGIBLE, TEMPLATE_INVALID, CAMPAIGN_IN_PAST, PLAYERS_REQUIRED, EXPIRES_INVALID) |
| INVALID_CURSOR | 422 | Player list: cursor did not come from a previous response |
| CURRENCY_NOT_SUPPORTED | 422 | No exchange rate for the currency, or a game does not support it (details.game) |
| RATE_LIMITED | 429 | Rate limit exceeded |