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| Credential | Use it for |
|---|---|
| Aggregation credential | Registering operators and reading or updating their configuration. |
| Operator webhook secret | Authenticating wallet calls and creating that operator’s player sessions. |
| Player token | Opening 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/operatorsSet 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"
}'| Field | Required | Meaning |
|---|---|---|
externalOperatorId | Yes | Your stable operator reference. 1–64 letters, digits, underscores or hyphens; unique within your aggregator. |
websiteUrl | No | The 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. |
webhookUrl | Yes | The operator’s public HTTPS wallet endpoint. |
webhookRetryUrl | Yes | A different public HTTPS endpoint for wallet retries. Both endpoints share the same wallet ledger. |
configuration | No | Initial 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/:clientIdcurl "$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/:clientIdThis 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.
| Field | Rule |
|---|---|
games[].game | A casino event key from the game catalog, such as coin or dice. Futures is outside this configuration API. |
games[].rtpPercent | Only 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[].maxBet | Send both when changing bet limits. Finite, nonnegative USD amounts, minimum ≤ maximum, at most ten decimal places. |
maxExposure | A positive USD amount with at most ten decimal places, or null to remove the cap. See below. |
roundTimeoutHours | A 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. |
theme | Account 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:
| Field | Meaning |
|---|---|
clientId, operatorId, aggregatorId, externalOperatorId, websiteUrl | Account identifiers and website, as in the list response. |
suspended, isStaging | Current operator status and test-environment marker. |
currency | Always USD for these configuration amounts. |
revision | Configuration version for reference; PATCH does not take a revision precondition. Avoid overlapping updates to the same fields. |
maxExposure | The gross payout cap, or null when unset. |
roundTimeoutHours | Hours of inactivity after which an open round is settled, or null when unset (rounds never time out). |
roundTimeoutSettlement | How a timed-out round settles: cashout (the default) or rollback. |
theme | The operator’s configured theme defaults. |
games | All 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/maxBetin 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 continuingif 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’smaxWin, 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 withMaximum 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>_closewhen that game’s cash-out value is zero. A round with no move yet returns the stake:<game>_winfor 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_winorblackjack_close. - Video Poker: the dealt hand is kept and paid by the paytable, with
videopoker_winorvideopoker_close.
- Mines, Tower, HiLo, Stairs, Chicken, Bridge, Doors, Soccer and Balloon
cash out at the current value, sending
rollback: the round’s full stake is returned with<game>_winfor the stake, under the samebetId, carryingsettlement: "rollback"so you can show it as a rollback. No debit is reversed and no negative amount is sent. In round history the round showssettled, withpayoutequal 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/:transactionIdReturns 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
}| Field | Meaning |
|---|---|
kind | debit, credit, close or placement (a free bet’s zero-value _bet). |
amount | The action amount sent, in the session’s currency. |
state | sent (no answer yet), confirmed (your 200), refused (a final non-5xx answer), retryable or retrying (being resent), failed, abandoned (resends stopped) or manual_review. |
attempts | Deliveries so far, the first included. |
lastStatus / lastFailure | Your 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.
| Status | Next action |
|---|---|
400 | Correct the fields, RTP range, bet limits or URLs. Unknown fields and changes to fixed RTP are rejected. |
401 | Check the aggregation credential and environment. Ask Maktub whether it was renewed or revoked. |
404 | Check clientId and current operator assignment. |
409 | Reuse the original registration body or PATCH the existing operator; do not invent a new external reference for a retry. |
429 | Slow requests and retry with backoff. |
503 or a lost connection | Retry 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/:gameThese 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.
| Field | Meaning |
|---|---|
mode | range: send any rtpPercent within accepts.minPercent–accepts.maxPercent. preset: send one of accepts.presets. fixed: the RTP cannot be changed. |
rtpPercent | The 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. |
defaultRtpPercent | The 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. |
isDefault | true when no RTP has been set for the game. |
rtp | The 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.