Skip to Content
API ReferenceAggregation API

Aggregation API

Manage your operators from one backend integration: register an operator, list your accounts, and change each operator’s RTP, bet limits, theme, maximum exposure and round timeout. Your credential can access only the operators assigned to your aggregator.

Players still use the same session and iframe integration. The aggregation API manages operator accounts; each operator keeps its own wallet endpoints and webhook secret.

Before you start

Ask Maktub for your aggregation credential and API address. Existing operators can be assigned to your aggregator by Maktub; you do not need to recreate them.

Examples use https://server.maktub.bet. Use the address confirmed for your environment. Keep the aggregation credential on your backend, outside browser code, launch URLs and logs. Send it on every aggregation request:

Authorization: Bearer YOUR_AGGREGATION_CREDENTIAL Content-Type: application/json
CredentialUse it for
Aggregation credentialRegistering operators and reading or updating their configuration.
Operator webhook secretAuthenticating wallet calls and creating that operator’s player sessions.
Player tokenOpening the game for one player; returned by POST /session.

All amounts in this API are USD, regardless of a player’s currency. Wallet webhooks continue to use the session currency.

1. Register an operator

POST /aggregation/operators

Set MAKTUB_API_URL and MAKTUB_AGGREGATION_TOKEN in your backend environment:

curl --request POST "$MAKTUB_API_URL/aggregation/operators" \ --header "Authorization: Bearer $MAKTUB_AGGREGATION_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "externalOperatorId": "casino-42", "websiteUrl": "https://casino.example/", "webhookUrl": "https://wallet.example/main", "webhookRetryUrl": "https://wallet.example/retry" }'
FieldRequiredMeaning
externalOperatorIdYesYour stable operator reference. 1–64 letters, digits, underscores or hyphens; unique within your aggregator.
websiteUrlNoThe operator’s public website, using HTTP or HTTPS. Informational only; omit it, or send null, when you keep no public URL, and responses return null.
webhookUrlYesThe operator’s public HTTPS wallet endpoint.
webhookRetryUrlYesA different public HTTPS endpoint for wallet retries. Both endpoints share the same wallet ledger.
configurationNoInitial games, theme, maxExposure, roundTimeoutHours and roundTimeoutSettlement, following the update rules below.

Wallet URLs must not contain credentials or fragments. Local and private addresses are not accepted. Implement the wallet webhook contract on both endpoints before opening player sessions.

Success returns HTTP 201 with created: true, the generated clientId, a generated webhookSecret, and the operator configuration. The operator is active immediately with system defaults. Without initial configuration, the exposure cap and the round timeout are unset and each game uses its default settings. Registration does not test your wallet or create a player session.

Store the returned clientId and webhookSecret securely, associated with your externalOperatorId. Use those values in the session request. Use clientId, rather than externalOperatorId or operatorId, in the URLs below.

Retry a registration safely

If the connection fails before you receive a response, resend the same body with the same externalOperatorId. An identical retry returns HTTP 200, created: false, and the same operator, including after concurrent requests. A changed registration body under that reference returns HTTP 409; use PATCH for later configuration changes.

A retry can recover the original webhook secret after a lost response. If Maktub has changed that operator’s wallet secret or endpoints, it returns webhookSecret: null and credentialChanged: true; contact Maktub for the current wallet setup. GET and PATCH never return wallet secrets.

The response’s isStaging reflects the environment where the operator was registered. You cannot change it through this API. Use the agreed environment for production onboarding; if test and production share an operator registry, use separate external references for those registrations.

2. List and read operators

GET /aggregation/operators GET /aggregation/operators/:clientId
curl "$MAKTUB_API_URL/aggregation/operators" \ --header "Authorization: Bearer $MAKTUB_AGGREGATION_TOKEN"

The list response contains all your authorized operators:

{ "aggregatorId": "your-aggregator-id", "operators": [ { "operatorId": "maktub-operator-id", "clientId": "returned-client-id", "externalOperatorId": "casino-42", "websiteUrl": "https://casino.example/", "suspended": false, "isStaging": false } ] }

For an existing operator linked by Maktub, externalOperatorId may be null. Call GET with the returned clientId to read its full configuration. An operator outside your account and an unknown operator both return HTTP 404. Moving an operator to another aggregator removes it from your access.

3. Update configuration

PATCH /aggregation/operators/:clientId

This backend example changes Dice and the operator’s account theme, then reads the effective limit from the response:

const path = "/aggregation/operators/" + encodeURIComponent(clientId); const response = await fetch( process.env.MAKTUB_API_URL + path, { method: "PATCH", headers: { Authorization: `Bearer ${process.env.MAKTUB_AGGREGATION_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ maxExposure: 1000, roundTimeoutHours: 72, roundTimeoutSettlement: "cashout", games: [ { game: "dice", rtpPercent: 97, minBet: 0.1, maxBet: 100 }, ], theme: { buttonColor: "#22C993", darkBackground: "#101A18" }, }), }, ); const result = await response.json(); if (!response.ok) { const message = result.error ?? result.message; throw new Error(message ?? `Configuration failed (${response.status})`); } const dice = result.games.find((game) => game.game === "dice"); // dice.configuredMaxBet and dice.maxBet are both 100: Dice caps the win at // maxExposure, not the stake (see Maximum exposure below).

HTTP 200 returns the updated configuration. Omitted games and fields keep their previous values. An invalid field rejects the entire request before saving.

FieldRule
games[].gameA casino event key from the game catalog, such as coin or dice. Futures is outside this configuration API.
games[].rtpPercentOnly when the game’s rtp.configurable is true. Send 97 for 97%, not 0.97. rtp.rtpPresets tells the two kinds apart. When the entry lists it (paytable games such as keno, plinko, roulette, blackjack, launch or pulse), send one of those whole-number presets, 90, 91, … 99; any other value, such as 97.5, returns 400. When it is absent (edge games such as dice or limbo), send any value from rtp.minPercent to rtp.maxPercent, 90 to 99 inclusive. Do not test for minPercent/maxPercent: paytable games list them too, as the return range of their table. Send null to return any configurable game to its default. The backoffice offers the same choices for every game as whole-number presets only; an edge-game value set here that is not a whole number, such as 97.5, appears there as custom and is kept until the operator picks another.
games[].minBet, games[].maxBetSend both when changing bet limits. Finite, nonnegative USD amounts, minimum ≤ maximum, at most ten decimal places.
maxExposureA positive USD amount with at most ten decimal places, or null to remove the cap. See below.
roundTimeoutHoursA whole number of hours from 1 to 720 inclusive, or null to remove the timeout. Fractions, 0, negative numbers and strings return 400. See Round timeout.
roundTimeoutSettlement"cashout" or "rollback", or null to return to the default, "cashout". Any other value returns 400. It can be set without roundTimeoutHours; it is kept and has no effect until a timeout is set.
themeAccount defaults using the appearance theme fields. Invalid values return 400.

A paytable preset selects a generated paytable whose exact return lies between the preset and 0.05 percentage points above it (preset 96 returns 96.00% to 96.05%). Keno’s Bonus Mode keeps its discount against regular play, so it returns less than the preset: rtp.lightning states its range (83.95% to 88.81% at preset 90, 92.38% to 97.68% at 99). Each table keeps the default table’s top prize, except where a bet has a single paying outcome and the return can only rise through it: the roulette straight at 98% and 99%, the baccarat tie at every preset, Double’s green from 94%, and keno high risk with 2 or 3 picks at 99% (17.16× and 81.51× instead of 17.1× and 81.5×). A blackjack preset moves only the gross return of an ordinary win (2× by default, 1.813× at 90, 2.016× at 99); the 6:5 natural, insurance and the side bets keep their tables, and the figure is the main hand’s return under optimal play. Video poker is fixed: its RTP cannot be overridden, and any rtpPercent for it, null included, returns 400. A fixed RTP game can still have bet limits changed. Setting a game’s bet limits to 0 and 1000000 restores the platform defaults, including their currency-aware ceiling. Bull Run (bullrun) plays within Crash’s bet limits until you set its own; its minBet/maxBet in the response show the limits in force.

Use null to clear the whole theme, a theme field, or an individual component field. For example, {"theme":{"buttonColor":null}} restores the default button color while keeping other account settings. Session appearance values take precedence over account defaults. To see an account-theme change, launch a new session without overriding that field.

Configuration response

POST registration, GET of one operator and PATCH include these fields:

FieldMeaning
clientId, operatorId, aggregatorId, externalOperatorId, websiteUrlAccount identifiers and website, as in the list response.
suspended, isStagingCurrent operator status and test-environment marker.
currencyAlways USD for these configuration amounts.
revisionConfiguration version for reference; PATCH does not take a revision precondition. Avoid overlapping updates to the same fields.
maxExposureThe gross payout cap, or null when unset.
roundTimeoutHoursHours of inactivity after which an open round is settled, or null when unset (rounds never time out).
roundTimeoutSettlementHow a timed-out round settles: cashout (the default) or rollback.
themeThe operator’s configured theme defaults.
gamesAll 33 casino game configurations, even before an operator has any wagers.

Each games entry contains game, enabled, minBet, maxBet, configuredMinBet, configuredMaxBet and rtp. Use minBet/maxBet as the effective limits; the configured values are the saved limits before exposure.

For an edge game, rtp includes configurable: true, configuredRtpPercent (the RTP in force), and minPercent and maxPercent, the range rtpPercent accepts. For a paytable game, rtp includes configurable: true, rtpPresets (the values rtpPercent accepts), rtpPreset when one is set, and the exact return of the table in force, the preset’s or the default one: a percentage, a range across risks and rows (minPercent/maxPercent, here a return rather than an accepted input), or per-bet variants. Keno adds lightning, the minPercent/maxPercent range of its Bonus Mode. Until a preset is set, Launch and Pulse also carry configuredRtpPercent and certifiedRtpPercent for their default curve. Fixed games return configurable: false and their percentage, range or per-bet variants. An undeclared RTP is unavailable, not 0%. Paytable games report configurable: true and rtpPresets even with no preset set; before 2026-10-06 they reported configurable: false, so ignore keys you do not recognize rather than rejecting the response.

Maximum exposure

maxExposure limits the total payout of one casino round, including the returned stake. For example, a $100 payout on a $10 stake consumes $100 of exposure, not $90. It is not a daily limit or a shared pool across players. Futures is excluded.

maxExposure caps the payout, not your bet limits: in most games your configured minBet/maxBet apply unchanged, and a round is held under the cap at settlement, as described below. Six games still lower their maximum stake to fit it, because a round there can settle several bets or hands at once — Blackjack, Video Poker, Baccarat and Roulette — settles Double without a ceiling, or, in Hilo, could not continue after opening: their effective maximum is the smaller of your configured maximum and maxExposure / maximum return (for example $500,000 exposure allows a $13,888.8888888888 Roulette stake; a configured $100 maximum still stays $100). Their minimum is reduced only when it exceeds that maximum. Configured limits remain saved; raising or removing exposure restores the room they allow. A single-shot bet is never refused for what it could pay. Its own choices — a Limbo or Slide target, a Dice win chance, Keno Lightning, a Drop difficulty — keep their odds, and a win that would pay more than maxExposure pays exactly maxExposure: the credit and the response’s payout are the cap, payoutMultiplier is the cap divided by the stake, and the round’s state.exposureCap records the cap (limit) and the multiplier the outcome would have paid (uncappedMultiplier). A Plinko or Wire batch is one round: its balls share the cap in play order. The cap is applied in whole cents, rounded down, so a fractional maxExposure (for example after currency conversion) is never exceeded.

  • RTP presets: a preset that pays more on a single outcome than the default table (a roulette straight at 98% or 99%, the baccarat tie at any preset, Double’s green from 94%) has a higher maximum return, so the same exposure allows a smaller stake; minBet/maxBet in the response already reflect it.
  • Progressive games (Mines, Hilo, Tower, Soccer, Doors, Bridge, Chicken, Balloon, Stairs): a next step is refused with Maximum exposure reached; cash out before continuing if a winning step could exceed the round’s cap. The player can still cash out the amount already earned. The cap is the one in force when the round opened. A round whose first step could already exceed it does not open, and an automatic run is refused when its last pick could exceed it. Tower, Soccer, Doors, Bridge and Chicken price the next step at their most generous RTP, because their cashout reads the RTP in force when it happens.
  • Crash: Maktub supplies an automatic cashout no higher than the cap divided by the stake, rounded down to the supported multiplier precision. An earlier player-selected target remains earlier. A stake that leaves no cashout of at least 1.01× under the cap is refused with Maximum exposure exceeded. Bull Run lets a player hold two positions in one round, so each position gets the same automatic cashout on half the cap (after the session’s maxWin, when one is set): both positions can always open, and the round never pays more than the cap. A stake above half the cap divided by 1.01 is refused with Maximum exposure exceeded.

Send {"maxExposure":null} to remove the cap. Zero is invalid. Externally settled parimutuel operators cannot enable this setting.

When changes take effect

Bet limits and exposure can take up to 60 seconds to reach gameplay. Realtime connections refresh their limits before accepting a new placement. Lowering exposure never truncates a payout already earned under the previous limit.

RTP retains each game’s existing behavior. Some multistep games read current RTP again at cashout. Crash and Slide retain their engine’s RTP until the engine is recreated, currently through a worker restart. Saving RTP through this API does not trigger that restart; coordinate it with Maktub when changing those games.

Round timeout

roundTimeoutHours settles a round the player left open. It applies to multi-step games: Mines, Tower, HiLo, Stairs, Chicken, Bridge, Doors, Soccer, Balloon, Blackjack and Video Poker. Crash, Slide and Double rounds end on their own and are not affected. Instant games have no open rounds.

The timeout counts from the round’s last recorded activity, the player’s last action. Rounds already open when you set the timeout are covered. An expired round is settled within a few minutes after it expires, not to the second.

roundTimeoutSettlement chooses what the round becomes:

  • cashout (default):
    • Mines, Tower, HiLo, Stairs, Chicken, Bridge, Doors, Soccer and Balloon cash out at the current value, sending <game>_win, or <game>_close when that game’s cash-out value is zero. A round with no move yet returns the stake: <game>_win for the stake.
    • Blackjack: insurance, if offered, is declined, every open hand stands and the dealer plays. The round settles as a normal round, with blackjack_win or blackjack_close.
    • Video Poker: the dealt hand is kept and paid by the paytable, with videopoker_win or videopoker_close.
  • rollback: the round’s full stake is returned with <game>_win for the stake, under the same betId, carrying settlement: "rollback" so you can show it as a rollback. No debit is reversed and no negative amount is sent. In round history the round shows settled, with payout equal to the stake.

If the round was played with a free bet, a refund sends no money: the free bet is restored to the player and the round closes the way a lost free-bet round does.

These webhooks arrive with no player present, often after the player’s session has expired; your wallet must accept them. They carry the round’s betId and a new transactionId.

Send {"roundTimeoutHours":null} to remove the timeout. Changes to either field take effect within 60 seconds, like other settings.

Look up a transaction

GET /aggregation/operators/:clientId/transactions/:transactionId

Returns how one wallet webhook of that operator ended, by the transactionId it carried. The record exists for operators that registered a retry endpoint, from the first send until 30 days after its final state.

{ "transactionId": "9f2b1c04-6d51-4a83-b0e2-7c1f8a3d5e60", "betId": "6650a3f1e4b0c912d8a74b21", "event": "dice_win", "game": "dice", "kind": "credit", "userId": "test-player", "amount": 19.8, "state": "confirmed", "attempts": 1, "firstSentAt": "2026-10-09T12:00:00.000Z", "lastSentAt": "2026-10-09T12:00:00.000Z", "lastStatus": 200, "lastFailure": null }
FieldMeaning
kinddebit, credit, close or placement (a free bet’s zero-value _bet).
amountThe action amount sent, in the session’s currency.
statesent (no answer yet), confirmed (your 200), refused (a final non-5xx answer), retryable or retrying (being resent), failed, abandoned (resends stopped) or manual_review.
attemptsDeliveries so far, the first included.
lastStatus / lastFailureYour last HTTP status, or timeout / transport when there was no answer.

An unknown transactionId, or one that belongs to another operator, returns 404 with Transaction not found.

Errors and recovery

Aggregation errors return {"error":"description"}. Infrastructure-level responses can use message instead. Check the HTTP status first.

StatusNext action
400Correct the fields, RTP range, bet limits or URLs. Unknown fields and changes to fixed RTP are rejected.
401Check the aggregation credential and environment. Ask Maktub whether it was renewed or revoked.
404Check clientId and current operator assignment.
409Reuse the original registration body or PATCH the existing operator; do not invent a new external reference for a retry.
429Slow requests and retry with backoff.
503 or a lost connectionRetry registration with its original identity/body. For PATCH, read the current configuration before retrying if another editor may have changed it.

Maktub manages aggregation credentials. Renewal invalidates the old credential; revocation removes API access. Neither changes the operators’ wallet secrets.

Before going live, register a test operator, verify that list/read return only your accounts, update and read back its configuration, then follow the wallet and gameplay checks using a disposable player. A successful registration is not evidence that the wallet integration has passed.

Game-by-game RTP

GET /aggregation/operators/:clientId/games GET /aggregation/operators/:clientId/games/:game PATCH /aggregation/operators/:clientId/games/:game

These endpoints are a game-by-game view of the same RTP settings, for building an RTP screen in your own back office. They use the same credential, the same operator access and the same validation as PATCH /aggregation/operators/:clientId.

curl "$MAKTUB_API_URL/aggregation/operators/$CLIENT_ID/games" \ --header "Authorization: Bearer $MAKTUB_AGGREGATION_TOKEN"

The list returns all 33 casino games. GET of one game returns the same entry, plus clientId and revision:

{ "clientId": "returned-client-id", "revision": 4, "games": [ { "game": "dice", "enabled": true, "configurable": true, "mode": "range", "rtpPercent": 97, "defaultRtpPercent": 99, "isDefault": false, "accepts": { "minPercent": 90, "maxPercent": 99 }, "rtp": { "kind": "runtime", "configurable": true, "configuredRtpPercent": 97, "minPercent": 90, "maxPercent": 99 } }, { "game": "keno", "enabled": true, "configurable": true, "mode": "preset", "rtpPercent": null, "defaultRtpPercent": null, "isDefault": true, "accepts": { "presets": [90, 91, 92, 93, 94, 95, 96, 97, 98, 99] }, "rtp": { "kind": "baked-range", "minPercent": 98.6538461538, "maxPercent": 99.068863945, "configurable": true } }, { "game": "videopoker", "enabled": true, "configurable": false, "mode": "fixed", "rtpPercent": null, "defaultRtpPercent": null, "isDefault": true, "accepts": null, "rtp": { "kind": "undeclared", "configurable": false } } ] }

The keno rtp object above is shortened; responses carry the full RTP object.

FieldMeaning
moderange: send any rtpPercent within accepts.minPercent–accepts.maxPercent. preset: send one of accepts.presets. fixed: the RTP cannot be changed.
rtpPercentThe current setting; sending it back changes nothing. For a range game, the RTP in force, including the default. For a preset game, the selected preset, or null while the game uses its default paytable. Always null for fixed.
defaultRtpPercentThe RTP a range game returns to on reset. null for preset games, whose default paytable return is stated in rtp while isDefault is true, and for fixed games.
isDefaulttrue when no RTP has been set for the game.
rtpThe exact return of the game as configured, identical to the game’s rtp in the configuration response. Show this figure to players and staff.

To change one game, send only rtpPercent:

curl --request PATCH "$MAKTUB_API_URL/aggregation/operators/$CLIENT_ID/games/keno" \ --header "Authorization: Bearer $MAKTUB_AGGREGATION_TOKEN" \ --header 'Content-Type: application/json' \ --data '{"rtpPercent": 96}'

HTTP 200 returns the updated entry. Send {"rtpPercent": null} to return the game to its default. A value outside accepts, a fixed game, an empty body or any other field returns 400 and saves nothing. An unknown game returns 404. To change several games in one request, or RTP together with bet limits, use PATCH /aggregation/operators/:clientId. The timing rules in When changes take effect apply to both.

Last updated on