Skip to Content
API ReferenceFree Bets

Free Bets

Issue free bets to your users directly from your backend. A free bet is a game-specific, fixed-stake wager: the player plays without their balance being debited, and on a win the full payout (stake × multiplier) is credited to their balance through your normal wallet flow.

When a user has free bets available for a game, the SDK automatically shows a FREE BETS section above the bet controls with the stake value, the remaining count, and a toggle. While the toggle is on, the bet amount is locked to the free bet stake.

Important: This request must be made from your server side — it is authenticated with your webhook secret, which must never be exposed in client-side code.

Issue Free Bets

POST https://server.maktub.bet/freebets/issue Content-Type: application/json x-operator-secret: <your webhook secret>

Authentication uses your webhook secret (the same secret Maktub uses to sign webhooks to your server), sent in the x-operator-secret header. Operators without a configured webhook secret cannot use this endpoint — contact Maktub to set one.

Request Body

FieldTypeRequiredDescription
clientIdstringYour unique customer/client identifier provided by Maktub
userIdstringThe unique identifier for the end user in your system. The user does not need to have opened a game yet.
gamestringThe game the free bets are valid for (e.g. "dice", "mines", "crash"). Free bets are always game-specific.
betAmountnumberStake per free bet in USD. Must be within the game’s bet limits.
quantitynumberNumber of free bets to issue (integer, 1–1000)
idempotencyKeystringA unique key for this issuance. Retrying with the same key returns the original grant instead of issuing again.
expiresAtstringISO 8601 date. Free bets unused after this time become unusable. Default: no expiry.
{ "clientId": "your-client-id", "userId": "user-123", "game": "dice", "betAmount": 0.5, "quantity": 14, "idempotencyKey": "promo-2026-06-user-123", "expiresAt": "2026-07-01T00:00:00Z" }

Example (curl)

curl -X POST https://server.maktub.bet/freebets/issue \ -H "Content-Type: application/json" \ -H "x-operator-secret: $MAKTUB_WEBHOOK_SECRET" \ -d '{ "clientId": "your-client-id", "userId": "user-123", "game": "dice", "betAmount": 0.5, "quantity": 14, "idempotencyKey": "promo-2026-06-user-123" }'

Response

{ "freeBetId": "665f1c2e8b3a4d0012ab34cd", "game": "dice", "betAmount": 0.5, "quantity": 14, "remainingCount": 14, "expiresAt": null }
FieldTypeDescription
freeBetIdstringIdentifier of the grant
gamestringGame the free bets are valid for
betAmountnumberStake per free bet, in USD
quantitynumberTotal free bets issued in this grant
remainingCountnumberFree bets left in this grant
expiresAtstring | nullExpiry, or null for no expiry

Errors

All rejected requests return a non-200 status.

StatusMeaning
400Validation error — unsupported game, stake outside the game’s bet limits, invalid quantity, past expiresAt, missing idempotencyKey
401Invalid credentials — unknown clientId, wrong or missing x-operator-secret, or no webhook secret configured
429Rate limited — too many requests, or too many failed authentication attempts

Supported games

All Maktub original games support free bets except Real Estate, Futures, and the per-position chip games (Baccarat, Roulette — they have no single fixed stake):

coin, defuse, diamonds, dice, drop, keno, launch, limbo, plinko, pulse, safe, spark, volt, wheel, wire, balloon, blackjack, bridge, cases, chicken, doors, hilo, mines, russianRoulette, soccer, stairs, tower, videopoker, crash, slide, double

Cancel Free Bets

Revoke the unused free bets in a grant you previously issued. Cancellation is by the same idempotencyKey you used to issue, and only affects free bets that haven’t been played yet — rounds already consumed are unaffected.

POST https://server.maktub.bet/freebets/cancel Content-Type: application/json x-operator-secret: <your webhook secret>

Same authentication as /freebets/issue — your webhook secret in the x-operator-secret header.

Request Body

FieldTypeRequiredDescription
clientIdstringYour unique customer/client identifier
idempotencyKeystringThe idempotencyKey of the grant to cancel (the same one you passed to /freebets/issue)
{ "clientId": "your-client-id", "idempotencyKey": "promo-2026-06-user-123" }

Example (curl)

curl -X POST https://server.maktub.bet/freebets/cancel \ -H "Content-Type: application/json" \ -H "x-operator-secret: $MAKTUB_WEBHOOK_SECRET" \ -d '{ "clientId": "your-client-id", "idempotencyKey": "promo-2026-06-user-123" }'

Response

{ "freeBetId": "665f1c2e8b3a4d0012ab34cd", "game": "dice", "cancelledCount": 13 }
FieldTypeDescription
freeBetIdstringIdentifier of the cancelled grant
gamestringGame the grant was valid for
cancelledCountnumberNumber of still-unused free bets that were revoked. 0 if the grant was already fully used or already cancelled.

Cancellation is a soft cancel: the grant’s remaining count is set to 0 so it can no longer be played, but the record is kept for your audit and idempotency. Calling cancel again with the same key is safe and simply returns cancelledCount: 0.

Errors

StatusMeaning
400Validation error — missing clientId or idempotencyKey
401Invalid credentials — unknown clientId, wrong or missing x-operator-secret
404No grant found for that clientId + idempotencyKey
429Rate limited — too many requests, or too many failed authentication attempts

How free bets are reported

A free bet never debits the player’s balance, but every consumed free bet — win or loss — sends a placement webhook so you can keep your own free-bet counter in sync. On a win, the full payout (stake × multiplier) is then credited through your normal flow.

Placement — <game>_bet (every free bet)

When a free bet is placed, a <game>_bet event is sent with a zero bet amount (no balance is debited) plus the free-bet fields. This fires once per consumed free bet, whether it ultimately wins or loses:

{ "event": "dice_bet", "userId": "user-123", "customerId": "your-client-id", "betId": "665f1d8e8b3a4d0012ab34ce", "freeBet": true, "freeBetId": "665f1c2e8b3a4d0012ab34cd", "freeBetsRemaining": 13, "roundClosed": false, "actions": [{ "type": "bet", "amount": 0 }] }
FieldTypeDescription
freeBetbooleanAlways true on a free-bet round.
freeBetIdstringThe grant this round consumed (the same id returned by /freebets/issue). Use it to attribute the round to a specific grant.
freeBetsRemainingnumberFree bets left in that grant for this (user, game) after this round. Decrement your counter on this event.

The bet action amount is always 0 — do not debit the player.

The placement also carries roundClosed, on exactly the same rule as a paid bet: true when this event is already the round’s last one (an instant game that lost — no <game>_win will follow), false while the round is still open or a credit is on its way.

Win — <game>_win

If the free bet wins, you also receive the normal credit event with the full payout and the same free-bet fields (so a win still reconciles even though there was no real debit):

{ "event": "dice_win", "userId": "user-123", "customerId": "your-client-id", "betId": "665f1d8e8b3a4d0012ab34ce", "freeBet": true, "freeBetId": "665f1c2e8b3a4d0012ab34cd", "freeBetsRemaining": 13, "actions": [{ "type": "win", "amount": 0.99 }] }

Counting rule: decrement your free-bet counter on the <game>_bet placement event — it always fires, on wins and losses alike. Treat the <game>_win as payout only: it carries the same freeBetId/freeBetsRemaining for reconciliation, so counting it again would double-count.

Losing free bets send no win — the remaining count is decremented and the player’s balance is untouched. They still get exactly one terminal event, the same as a paid round (see the one-terminal-event guarantee):

  • instant games — the placement <game>_bet itself is terminal, and carries roundClosed: true;
  • multi-round games — the placement reports roundClosed: false, and the round is closed later by a <game>_close. That close is opt-in: without it, a losing multi-round free bet sends the placement and nothing else.

Multiplayer games (Crash, Slide, Double): fully supported. Both the placement (crash_bet / slide_bet / double_bet) and the credit (crash_win / slide_resolve / double_resolve) carry freeBet, freeBetId, and freeBetsRemaining, exactly like every other game.

Player experience

  • The FREE BETS section appears automatically in every supported game while the user has free bets for that game, showing the stake and the remaining count.
  • Toggling free bets on locks the Bet Amount input to the stake; each bet consumes one free bet.
  • The stake is enforced server-side from the grant — the client cannot change it.
  • For multi-round games (Mines, Blackjack, Crash…), one free bet covers the initial stake; cashouts pay in full. Actions that require an additional stake (Blackjack double, split, insurance, and side bets) are not available on free bet rounds.
  • When the count reaches zero the section disappears (or shows the next active grant, if any).

Issuing from the Backoffice

Free bets can also be issued directly from the Maktub Backoffice without any API calls:

  1. Navigate to Player Activity.
  2. Find the player you want to reward.
  3. Click Freebets in the Actions column.
  4. Select a game, enter the stake and quantity, and optionally set an expiry.
  5. Click Issue Freebets — the grant is created instantly.

The dialog also shows the player’s existing active grants so you can see what’s already assigned before issuing more.

Notes

  • Free bets can be issued via the API (above) or directly from the Backoffice Player Activity page.
  • Multiple grants for the same user and game stack; they are consumed oldest-first.
  • If a free bet round fails before it is placed (for example a validation error), the free bet is automatically returned to the user’s pool.
  • Free bets work in both wallet modes (webhook and aggregator/wallet-provider).
  • A player can place a free bet even with zero cash balance — the stake comes from the grant, never the wallet.
  • Unused free bets can be revoked any time with the cancel endpoint, or they simply expire (if expiresAt was set).
Last updated on