The Cloudbet Trading API lets developers fetch live sports odds and place bets programmatically, without going through the website or app. This guide covers the current v4 endpoints for placing straight (single) bets and accumulators, reading bet responses, and tracking settlement — with working curl and Python examples for every step.
Contents
- 1 TL;DR
- 2 Why a new guide?
- 3 Before you start
- 4 Step 1 — Fetch odds from the Feed API
- 5 Step 2 — Check your balance (Account API)
- 6 Step 3 — Place a straight (single) bet
- 7 Step 4 — Place an accumulator (multiple)
- 8 Step 5 — Track status, settlement and history
- 9 Migrating from the old (v3) guide
- 10 Frequently asked questions
- 10.1 How do I authenticate with the Cloudbet Trading API?
- 10.2 What’s the difference between the Trading API key and the Affiliate API key?
- 10.3 How do I place an accumulator bet through the API?
- 10.4 What replaced the old /v3/bets/place endpoint?
- 10.5 How do I check whether my bet was accepted or settled?
- 10.6 Which currencies does the Cloudbet Trading API support?
- 10.7 Can I test the Cloudbet API without real money?
- 11 Additional resources
- 12 Feedback & questions
TL;DR
- The Cloudbet Trading API is now on v4, and bet placement is split into two endpoints:
POST /pub/v4/bets/place/straightfor single bets andPOST /pub/v4/bets/place/multiplefor accumulators (multiples). - Accumulators are native now — send two or more legs in one request via
selections, and stake the whole slip in a single call. - The request format changed. Price-change handling lives in a nested
priceChangeobject, and the bet details live in aselectionobject (or aselectionsarray for multiples). Responses now report astateand, on rejection, areofferStake. - One endpoint handles all bet tracking:
GET /pub/v4/bets, filterable by bet ID, reference ID, order ID, settlement status and time range. - The Feed API (
/pub/v2/odds/...) and Account API (/pub/v1/account/...) are unchanged. - Auth is still a JWT X-API-Key header. A Trading key unlocks
/pub/v4/betsand real-time odds; an Affiliate key can also fetch odds, but responses may be cached and it can’t place bets. - Every example below is shown in curl first (the wire contract) and then Python with
requests(the version you’d actually build a bot on).
Why a new guide?
Our original Golang tutorial from 2021 placed bets against the old /pub/v3/bets/place endpoint. That endpoint has been superseded. The Trading API is now on v4, with native accumulator support, an idempotency model built around your own reference IDs, and a reoffer mechanism when a bet can’t be filled as requested.
If you’re migrating from the old guide, the moves are: switch to the two new placement endpoints, restructure your request body (details below), and read state instead of status on the response. We’ll flag each difference as we go.
Before you start
You’ll need a Cloudbet account with a Trading API key.
- Go to the API key section in your account menu to generate a key — no deposit required.
- Keep it safe. You can revoke and regenerate it if it leaks — but there’s a penalty for rotating keys too often, so don’t do it casually.
Two keys, two jobs. A Trading key gives you real-time odds and the ability to place bets on the /pub/v4/bets endpoints. An Affiliate key can also fetch odds, but responses may be cached up to a minute behind live, and it can’t place bets. For a trading bot, you want the Trading key.
Every request includes your key in an X-API-Key header:
curl
-H "X-API-Key: <API Key>"
Setting up in Python. Every Python example below reuses this one session, which carries your key and the standard headers on every request:
Python
import requests
BASE = "https://sports-api.cloudbet.com"
session = requests.Session()
session.headers.update({
"X-API-Key": "<API Key>",
"accept": "application/json",
"Content-Type": "application/json",
})
From here on, each example shows the curl call first, then the Python equivalent using this session.
Want to test without risking funds? Ask Cloudbet support (CS) to enable PLAY_EUR test funds on your account. Every call below works identically with PLAY_EUR as the currency.
Step 1 — Fetch odds from the Feed API
Placing a bet starts with finding the selection you want and its current price. The Feed API is unchanged (/pub/v2/odds/...), so we’ll keep this brief.
List available sports:
curl
curl -X GET "https://sports-api.cloudbet.com/pub/v2/odds/sports" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>"
Python
sports = session.get(f"{BASE}/pub/v2/odds/sports").json()["sports"]
Get all markets for a single event:
curl
curl -X GET "https://sports-api.cloudbet.com/pub/v2/odds/events/5945044" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>"
Python
event = session.get(f"{BASE}/pub/v2/odds/events/5945044").json()
Building a market URL
Inside an event, markets break down by period, and each contains selections. The two things you carry into a bet are the price (decimal odds) and the market URL, which identifies the exact selection:
<marketKey>/<outcome>?<params>
If there are no params, omit the query string entirely and use <marketKey>/<outcome>. Examples:
soccer.match_odds/home— no paramssoccer.total_goals/over?total=3— with params
Handicap note: for handicap markets, a line is identified by its params. Home and away outcomes share the same market URL for the same handicap line — the handicap value is dictated by the home team value and inverted on the away side.
Step 2 — Check your balance (Account API)
The Account API is also unchanged (/pub/v1/account/...).
List your currencies:
curl
curl -X GET "https://sports-api.cloudbet.com/pub/v1/account/currencies" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>"
Python
currencies = session.get(f"{BASE}/pub/v1/account/currencies").json()["currencies"]
Check the balance for one currency:
curl
curl -X GET "https://sports-api.cloudbet.com/pub/v1/account/currencies/PLAY_EUR/balance" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>"
Python
balance = session.get(f"{BASE}/pub/v1/account/currencies/PLAY_EUR/balance").json()
print(balance["amount"])
Response
{ "amount": "984.69" }
Step 3 — Place a straight (single) bet
POST /pub/v4/bets/place/straight. The v4 body is more structured than the old v3 flat payload — note especially the nested priceChange and selection objects.
curl
curl -X POST "https://sports-api.cloudbet.com/pub/v4/bets/place/straight" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>" \
-H "Content-Type: application/json" \
-d '{
"referenceId": "550e8400-e29b-41d4-a716-446655440000",
"currency": "PLAY_EUR",
"stake": "1.1",
"acceptPartialStake": true,
"priceChange": {
"value": "BETTER"
},
"selection": {
"eventId": "5945044",
"marketUrl": "soccer.total_goals/over?total=3",
"price": "1.2"
}
}'
Python
import uuid
payload = {
"referenceId": str(uuid.uuid4()), # fresh UUID per bet — repeats return 400
"currency": "PLAY_EUR",
"stake": "1.1",
"acceptPartialStake": True,
"priceChange": {"value": "BETTER"},
"selection": {
"eventId": "5945044",
"marketUrl": "soccer.total_goals/over?total=3",
"price": "1.2",
},
}
bet = session.post(f"{BASE}/pub/v4/bets/place/straight", json=payload).json()
print(bet["state"], bet.get("betId"))
Request fields:
referenceId(required) — a UUID you generate. It makes the request idempotent: submitting the samereferenceIdtwice returns 400. Use a fresh UUID for every new bet.currency(required) — e.g.PLAY_EUR,BTC,USDT.stake(required) — decimal string, up to your balance or the selection’s current limit. Limits rise toward kick-off and are highest just before and during in-play.acceptPartialStake— iftrue, accept a partial fill when the full stake can’t be placed.priceChange(required) — how to handle a price move between request and acceptance:value:NONE(reject any change),BETTER(accept only equal/better prices), orSLIPPAGE_TOLERANCE(accept a lower price within a tolerance).slippageToleranceRatio— required withSLIPPAGE_TOLERANCE; the maximum acceptable price movement ratio, e.g.0.01.
selection(required) —eventId,marketUrl(from Step 1), and theprice(decimal odds) you’re taking.betslipId(optional) — a UUID to group several bets under one betslip.
Reading the response
{
"betId": "55803465-d3fa-4517-9d6b-3a6f97a59332",
"referenceId": "550e8400-e29b-41d4-a716-446655440000",
"positionId": "428d3c97e8d791390a8f28460ecb45744929e1c5",
"state": "PENDING_ACCEPTANCE",
"currency": "PLAY_EUR",
"stake": "1.1",
"priceChange": { "value": "BETTER" },
"selection": {
"eventId": "5945044",
"marketUrl": "soccer.total_goals/over?total=3",
"price": "1.2"
}
}
The field to watch is state (this replaces v3’s status):
PENDING_ACCEPTANCE— being processed; poll again shortly for updates.ACCEPTED— accepted and ready to be placed.REJECTED— failed validation or wasn’t accepted (seerejectionCode).COMPLETED— event finished and the bet is settled.CANCELLED— cancelled by an admin or a player.
In pre-match, acceptance usually takes a few seconds. During in-play it can take up to 10 seconds, and during a danger situation (say, a free kick in soccer) we may hold the bet for up to 5 minutes to protect both sides. If a major event occurs while it’s held, the bet is rejected; otherwise it’s accepted.
When a bet is rejected
A rejected bet carries a rejectionCode explaining why. Common ones:
INSUFFICIENT_FUNDSPRICE_ABOVE_MARKETSTAKE_ABOVE_MAXSTAKE_BELOW_MINLIABILITY_LIMIT_EXCEEDEDMARKET_SUSPENDEDRESTRICTEDVERIFICATION_REQUIREDDUPLICATE_REQUEST
When a stake- or price-related rejection is recoverable, the response includes a reofferStake (a suggested stake to retry with) and sometimes a reofferMaxStake (the maximum you’re allowed for that wager). To retry, resubmit with the corrected values and a fresh referenceId.
Step 4 — Place an accumulator (multiple)
POST /pub/v4/bets/place/multiple. This is the headline v4 addition. Instead of firing off several singles, send all legs in one request via a selections array (minimum 2 legs), with a single stake for the whole slip.
curl
curl -X POST "https://sports-api.cloudbet.com/pub/v4/bets/place/multiple" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>" \
-H "Content-Type: application/json" \
-d '{
"referenceId": "9f1c2d40-1a2b-4c3d-9e8f-abc123456789",
"currency": "PLAY_EUR",
"stake": "1.0",
"acceptPartialStake": true,
"priceChange": {
"value": "BETTER"
},
"selections": [
{ "eventId": "25892195", "marketUrl": "soccer.match_odds/home", "price": "2.76" },
{ "eventId": "25892196", "marketUrl": "soccer.match_odds/away", "price": "2.76" }
]
}'
Python
import uuid
payload = {
"referenceId": str(uuid.uuid4()),
"currency": "PLAY_EUR",
"stake": "1.0",
"acceptPartialStake": True,
"priceChange": {"value": "BETTER"},
"selections": [
{"eventId": "25892195", "marketUrl": "soccer.match_odds/home", "price": "2.76"},
{"eventId": "25892196", "marketUrl": "soccer.match_odds/away", "price": "2.76"},
],
}
bet = session.post(f"{BASE}/pub/v4/bets/place/multiple", json=payload).json()
print(bet["state"])
The top-level fields (referenceId, currency, stake, acceptPartialStake, priceChange) behave exactly as they do for a straight bet. The difference is selections — an array where each leg is { eventId, marketUrl, price }.
The response mirrors the straight-bet response (betId, state, etc.), but returns selections as an array. On a rejection, each leg can carry its own rejectionCode — so you can see, for example, that one leg was STAKE_BELOW_MIN while another hit LIABILITY_LIMIT_EXCEEDED.
Step 5 — Track status, settlement and history
One endpoint covers it all: GET /pub/v4/bets. It returns straight and multiple bets in reverse-chronological order, and is how you confirm whether a PENDING_ACCEPTANCE bet was accepted, and later whether it settled as a win or loss.
Check specific bets right after placement (by bet ID):
curl
curl -X GET "https://sports-api.cloudbet.com/pub/v4/bets?betIds=55803465-d3fa-4517-9d6b-3a6f97a59332" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>"
Recover a bet by your own reference ID — useful if the placement response was lost to a network failure:
curl
curl -X GET "https://sports-api.cloudbet.com/pub/v4/bets?referenceIds=550e8400-e29b-41d4-a716-446655440000" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>"
Pull settled bets within a time range:
curl
curl -X GET "https://sports-api.cloudbet.com/pub/v4/bets?isSettled=true&from=2026-07-01T00:00:00Z&to=2026-07-14T23:59:59Z" \
-H "accept: application/json" \
-H "X-API-Key: <API Key>"
The same three calls in Python — pass query parameters via params (array filters like betIds also accept a list):
Python
# Confirm acceptance by bet ID
session.get(f"{BASE}/pub/v4/bets",
params={"betIds": "55803465-d3fa-4517-9d6b-3a6f97a59332"}).json()
# Recover a bet by your own reference ID
session.get(f"{BASE}/pub/v4/bets",
params={"referenceIds": "550e8400-e29b-41d4-a716-446655440000"}).json()
# Pull settled bets within a time range
session.get(f"{BASE}/pub/v4/bets", params={
"isSettled": "true",
"from": "2026-07-01T00:00:00Z",
"to": "2026-07-14T23:59:59Z",
}).json()
Key query parameters:
betIds— array of bet IDs to fetch. If set, other filters are ignored.referenceIds— array of your reference IDs. If set, other filters are ignored. Handy when you never got a bet ID back. Note: reference-ID lookups are only guaranteed for about a day.orderIds— array of order IDs; returns all non-rejected bets for the order. Can’t be combined withbetIdsorreferenceIds(that returns 400).isSettled—true(settled only),false(unsettled only), or omit for all bets except rejected (the default).limit— default 20.offset— default 0.from/to— RFC3339 timestamps.fromdefaults to 90 days ago;todefaults to now.
betIds and referenceIds are mutually exclusive.
Migrating from the old (v3) guide
| Old (v3) | New (v4) |
|---|---|
| POST /pub/v3/bets/place | POST /pub/v4/bets/place/straight |
| (no accumulator support) | POST /pub/v4/bets/place/multiple |
| GET /pub/v3/bets/{id}/status, GET /pub/v3/bets/history | GET /pub/v4/bets (filter by betIds, isSettled, from/to, etc.) |
| Flat body: top-level eventId, marketUrl, price, acceptPriceChange | Nested selection (or selections) object + nested priceChange object |
| Response status | Response state |
| Feed API /pub/v2/odds/… | unchanged |
| Account API /pub/v1/account/… | unchanged |
Play nice with the endpoint. Don’t fire abusive requests at the betting endpoints. If your rejection rate gets too high (roughly more than 80 rejections in your last 100 bets), you may be flagged for review. Query prices sensibly and place a bet only when you have reasonable confidence it’s placeable.
Frequently asked questions
How do I authenticate with the Cloudbet Trading API?
Pass your API key in an X-API-Key header on every request. Generate the key from the API key section of your account — no deposit required. A Trading key is required for the /pub/v4/bets endpoints.
What’s the difference between the Trading API key and the Affiliate API key?
A Trading key lets you read real-time odds and place bets. An Affiliate key can also fetch odds, but responses may be cached up to a minute behind live, and it can’t place bets. If you’re building a betting bot, use the Trading key.
How do I place an accumulator bet through the API?
Send a POST to /pub/v4/bets/place/multiple with a selections array of at least two legs — each { eventId, marketUrl, price } — and a single top-level stake for the whole slip. The API returns one bet ID covering the accumulator.
What replaced the old /v3/bets/place endpoint?
Placement is now split into POST /pub/v4/bets/place/straight for singles and POST /pub/v4/bets/place/multiple for accumulators. The request body is also restructured: price handling moved into a nested priceChange object and the wager details into a selection (or selections) object, and the response now reports state instead of status.
How do I check whether my bet was accepted or settled?
Call GET /pub/v4/bets. Filter by betIds right after placement to confirm acceptance, or by isSettled=true with a from/to window to pull settled bets. If you never received a bet ID (for example, a dropped connection), you can look the bet up by your own referenceIds for about a day.
Which currencies does the Cloudbet Trading API support?
A range of cryptocurrencies, stablecoins and fiat currencies, plus the PLAY_EUR test currency. Rather than hard-coding a list, call GET /pub/v1/account/currencies to get the currencies currently enabled on your account.
Can I test the Cloudbet API without real money?
Yes. Ask Cloudbet support (CS) to enable PLAY_EUR test funds on your account. Every endpoint in this guide works identically with PLAY_EUR set as the currency.
Additional resources
- Cloudbet API docs (Swagger)
- Cloudbet API GitHub — protobuf schemas, code and response samples
- Cloudbet Wiki (API section) — markets, handicap odds, margins, test funds
- MCP integration — query Cloudbet sports data via AI using the Model Context Protocol
Feedback & questions
For technical questions and feature requests, use GitHub Discussions.


