Place multiple Same Race Multis on the same race in one betslip, in a single API call. Each SRM is priced and placed independently - TAB returns one bet ID per SRM. Same behaviour as stacking SRMs in TAB's web betslip and clicking "Place all".
Bookmaker support: TAB only. Other bookmakers' APIs don't currently expose a stacked-bet shape; for those, place each SRM via a normal
category: "racing"+is_same_event_multi: truerequest against/v3/place_bet.
POST /v3/stacked_srmis the new home for this endpoint. It uses the same async contract as/v3/place_bet: a 202 response in ~100ms with acorrelation_id, then pollPOST /api/bet_status(or receive a signed webhook atcallback_url) for the actual stacked-result payload. Theresultfield carries the same shape the legacy/v2/place_betstacked_srm branch used to return inline - drop-in for existing parsing code.
If you only want one SRM, use POST /v3/place_bet with category: "racing" and is_same_event_multi: true - this stacked endpoint rejects single-bet requests with a 400.
POST /v3/stacked_srm {#v3-stacked-srm}| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | yes | Active TAB session ID. Non-TAB sessions return 400. |
track | string | yes | Track name (e.g. "Albany"). All bets in the stack must be on this track. |
race_num | int | yes | Race number on the track. |
race_type | string | no | "(R)" (default), "(G)" greyhound, or "(H)" harness. |
date | string | yes | Race date YYYY-MM-DD. |
country | string | no | Optional country override (e.g. "NZL"). |
random_delay | object | no | Optional client-side delay knob, passed through. |
bets | array | yes | List of SRM specs - see below. Min length: 2. |
callback_url | string | no | If set, the result is POSTed to this URL when ready (http:// or https://). If absent, poll for the result. |
bets[] entry| Field | Type | Required | Description |
|---|---|---|---|
stake | float | yes | Stake for this SRM (positive). |
legs | array | yes | List of leg specs (min 2). |
target_odds | float | no | Floor on combined SRM odds. If actual odds < target_odds, this bet is rejected (and the whole stack is aborted before placement). |
bet_type | string | no | "non_promo" (default) or "bonus" (uses a bonus bet token). |
use_bonus_bet | bool | no | Same as bet_type: "bonus". |
use_boost | bool | no | Apply TAB's odds multiplier if available for that SRM. |
legs[] entry (per SRM)| Field | Type | Required | Description |
|---|---|---|---|
runner | string | yes | Runner name. Matched fuzzily against TAB's race card. |
position | string | no | One of "win" (default), "top2", "top3", "top4". "2nd"/"3rd"/"4th" aliases work. |
session_id must resolve to a TAB session → otherwise 400.bets length >= 2 → for a single SRM, hit /v3/place_bet with category: "racing" and is_same_event_multi: true instead.bets[i].legs length >= 2.bets[i].stake is positive.Standard v3 async envelope:
{ "status": "pending", "correlation_id": "9b3c…uuid…", "session_id": "uuid", "bookie": "tab", "username": "user@example.com", "submitted_at": "2026-05-14T10:00:00.000Z", "timeout_at": "2026-05-14T10:05:00.000Z", "callback_url": null }
Retrieve the result by polling POST /api/bet_status with the
correlation_id, or via webhook if you passed callback_url. Signature
verification, retry behaviour, and status values are identical to every
other v3 endpoint - see the
Async API (v3) guide.
State-mutating - same hazard as
/v3/split_bonus_bet. Once the 202 lands, the bets are queued for TAB's betslip. Never re-submit on a network blip - re-poll/api/bet_statuswith thecorrelation_idinstead. Persist thecorrelation_idfrom the 202 immediately, and on any client-side error surface that ID to the user rather than re-firing the request.
result payload{ "success": true, "placed": 2, "failed": 0, "results": [ { "success": true, "bet_id": "19349371189856901", "odds": 12.09, "stake": 1.00, "market": "srm", "track": "Albany", "race_num": 8, "legs": [ {"runner": "Cheyne Bay", "position": "win", "number": 4, "odds": 3.9, "proposition_id": 167861}, {"runner": "Angel Shame", "position": "top2", "number": 7, "odds": 3.1, "proposition_id": 168871} ], "error": null }, { "success": true, "bet_id": "19349371189856902", "odds": 8.40, "stake": 1.00, "market": "srm", "track": "Albany", "race_num": 8, "legs": [...], "error": null } ] }
| Field | Description |
|---|---|
success | true if at least one bet placed. |
placed | Count of bets that placed successfully. |
failed | Count of bets that failed during confirm. |
results[] | One entry per submitted SRM, in order. bet_id is null on failure; error carries the reason. |
result.success: false, no results (or empty). Something failed pre-confirm: proposition resolution, target-odds floor, bonus-token allocation, or TAB enquiry/betslip status. Nothing was placed.result.success: true with failed > 0. TAB accepted the betslip but rejected one or more bets at placement. Successful bets are placed; failures show in results[i].error.curl -X POST https://api.b337.ai/v3/stacked_srm \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "session_id": "uuid-string", "track": "Albany", "race_num": 8, "race_type": "(R)", "date": "2026-04-26", "bets": [ { "stake": 1.0, "target_odds": 10.0, "legs": [ {"runner": "Cheyne Bay", "position": "win"}, {"runner": "Angel Shame", "position": "top2"} ] }, { "stake": 1.0, "target_odds": 6.0, "legs": [ {"runner": "Cheyne Bay", "position": "win"}, {"runner": "Buffalo Theory", "position": "top3"} ] } ] }'
import requests, time API_KEY = "YOUR_API_KEY" BASE = "https://api.b337.ai" HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"} # 1. Submit (202 in ~100ms) submit = requests.post( f"{BASE}/v3/stacked_srm", headers=HEADERS, json={ "session_id": SESSION_ID, "track": "Albany", "race_num": 8, "race_type": "(R)", "date": "2026-04-26", "bets": [ { "stake": 1.0, "target_odds": 10.0, "legs": [ {"runner": "Cheyne Bay", "position": "win"}, {"runner": "Angel Shame", "position": "top2"}, ], }, { "stake": 1.0, "target_odds": 6.0, "legs": [ {"runner": "Cheyne Bay", "position": "win"}, {"runner": "Buffalo Theory", "position": "top3"}, ], }, ], }, ).json() cid = submit["correlation_id"] timeout_at = submit["timeout_at"] # IMPORTANT: persist cid here before doing anything else. State-mutating - # if the loop below breaks, we re-poll, we never re-submit. deadline = time.mktime(time.strptime(timeout_at[:19], "%Y-%m-%dT%H:%M:%S")) # 2. Poll /api/bet_status until done while time.time() < deadline: r = requests.post( f"{BASE}/api/bet_status", headers=HEADERS, json={"correlation_ids": [cid]}, ).json() entry = r["statuses"][0] if entry["status"] == "completed": data = entry["result"] # same shape as the old v2 stacked_srm response print(f"placed={data['placed']} failed={data['failed']}") for i, bet in enumerate(data["results"]): if bet["success"]: print(f" bet {i}: OK id={bet['bet_id']} odds={bet['odds']}") else: print(f" bet {i}: FAIL {bet['error']}") break if entry["status"] in ("timeout", "unknown"): print(f"poll failed: {entry['status']}; correlation_id={cid} for manual recheck") break time.sleep(2)
{ "session_id": "...", "track": "Albany", "race_num": 8, "date": "2026-04-26", "bets": [ { "stake": 1.0, "target_odds": 10.0, "legs": [{"runner": "X"}, {"runner": "Y", "position": "top2"}] }, { "stake": 25.0, "bet_type": "bonus", "target_odds": 8.0, "legs": [{"runner": "X"}, {"runner": "Z", "position": "top3"}] } ] }
Bonus tokens: if multiple bets in the stack use
bet_type: "bonus", they're allocated different tokens serially before submission. If any bonus bet can't get a matching token (e.g. no token<= stake), the entire stack is rejected before placement (no partial bets).
bets[] must be on the same (track, race_num, race_type, date). Different races → use multiple separate calls./v3/place_bet with category: "racing" + is_same_event_multi: true.target_odds. If any bet's enquiry/betslip price falls below its floor, the whole stack is aborted before placement.bets table as its own row with bet_type=srm.category: "stacked_srm"⚠️ Deprecated - sunset 2026-05-17. The stacked_srm branch of
POST /v2/place_betstill works but now returns deprecation response headers:Deprecation: true Sunset: Sun, 17 May 2026 00:00:00 GMT Link: </v3/stacked_srm>; rel="successor-version"After 17 May 2026 the stacked_srm branch will return HTTP 410. Other categories on
/v2/place_bet(racing,sports,multi,betfair) are not affected by this sunset.Migrate to
POST /v3/stacked_srm- same payload minus thecategoryfield (the endpoint is the discriminator now), plus optionalcallback_url. The result payload shape is identical.
curl -X POST https://api.b337.ai/v2/place_bet \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "session_id": "uuid-string", "category": "stacked_srm", "track": "Albany", "race_num": 8, "race_type": "(R)", "date": "2026-04-26", "bets": [ {"stake": 1.0, "target_odds": 10.0, "legs": [{"runner": "Cheyne Bay", "position": "win"}, {"runner": "Angel Shame", "position": "top2"}]}, {"stake": 1.0, "target_odds": 6.0, "legs": [{"runner": "Cheyne Bay", "position": "win"}, {"runner": "Buffalo Theory", "position": "top3"}]} ] }'
Returns the same result-payload shape shown under
result payload above, but inline synchronously
(no correlation_id, no poll step).