Betfair API Guide 2026: Authentication, Betting, and Rate Limits
Quick Answer
The Betfair Exchange API is free, officially supported, and documented at developer.betfair.com. Get your API key from the Developer Portal, authenticate using certificate login for bots, and use betfairlightweight or Flumine in Python to place bets programmatically. Rate limit: 1,000 requests/hour. Use the Streaming API for real-time data.
The Betfair Exchange API is the foundation of every serious betting automation project. This guide covers everything from getting your first API key to placing bets programmatically — with working Python code at every step.
The Betfair Exchange API is free to use for personal accounts, supports full bet placement automation, and is documented at developer.betfair.com. It is the only major bookmaker API that explicitly permits automated betting in its terms of service.
Getting Your API Key
Betfair provides two types of API keys: a delay key (free, 1-second data delay, available immediately) and a live key (real-time data, requires a funded account with at least one bet placed). For production automation, you need the live key.
- 1 Register at betfair.com. Create a Betfair Exchange account and complete identity verification. You need a verified account to access the Developer Portal.
- 2 Go to the Developer Portal. Navigate to developer.betfair.com and log in with your Betfair credentials. This is separate from the main Betfair site.
- 3 Create an application. Click "My API Access" then "Create Application". Give it a descriptive name (e.g., "football-arb-bot"). You receive your API key immediately.
- 4 Note both keys. You get a delay key (for testing) and a live key (for production). Store both securely — treat them like passwords. Never commit them to version control.
- 5 Fund your account and place one bet. To activate the live key, you need a funded account with at least one bet placed. A £1 bet on any market is sufficient.
Authentication: Certificate vs SSOID
The Betfair API supports two authentication methods. For automated scripts, certificate authentication is the correct choice. SSOID (interactive login) is for manual sessions only.
Certificate authentication (recommended for bots)
Certificate authentication uses a self-signed SSL certificate to authenticate non-interactive logins. Your script logs in automatically without requiring a browser or manual input. This is the only reliable method for long-running automation scripts.
Step 1: Generate a self-signed certificate
# Generate private key + self-signed certificate (valid 10 years)
openssl req -x509 -nodes -days 3650 \
-newkey rsa:2048 \
-keyout client-2048.pem \
-out client-2048.crt
# You'll be prompted for certificate details.
# Country, organisation etc. can be anything — Betfair doesn't validate them. Step 2: Upload the certificate to Betfair
Go to developer.betfair.com, navigate to your application, and upload the client-2048.crt file. Keep the client-2048.pem private key on your server — never share it.
Step 3: Authenticate in Python
import betfairlightweight
trading = betfairlightweight.APIClient(
username="your_betfair_username",
password="your_betfair_password",
app_key="your_live_api_key",
certs="/path/to/certs/", # folder containing client-2048.pem and client-2048.crt
)
trading.login()
print("Logged in. Session token:", trading.session_token) SSOID (interactive login — not for bots)
SSOID is a session token obtained via the standard Betfair login flow. It expires after 4 hours of inactivity and requires manual re-authentication. Use it for testing and exploration, not for production scripts. Certificate auth handles session renewal automatically.
Reading Market Data
The Betfair API provides two ways to read market data: the REST API (polling) and the Streaming API (WebSocket push). For real-time automation, always use the Streaming API.
Listing football markets (REST API)
import betfairlightweight
from betfairlightweight.filters import market_filter
trading = betfairlightweight.APIClient(...)
trading.login()
# List all Premier League Match Odds markets
markets = trading.betting.list_market_catalogue(
filter=market_filter(
event_type_ids=["1"], # 1 = Football
competition_ids=["10932509"], # Premier League competition ID
market_types=["MATCH_ODDS"],
market_countries=["GB"],
),
market_projection=["COMPETITION", "EVENT", "MARKET_START_TIME", "RUNNER_DESCRIPTION"],
max_results=50,
)
for market in markets:
print(f"{market.event.name} — {market.market_name} — {market.market_start_time}")
for runner in market.runners:
print(f" {runner.runner_name}") Real-time odds via Streaming API
The Streaming API pushes market updates via WebSocket. It does not count against the 1,000 requests/hour rate limit. Flumine uses the Streaming API internally — this is one of the main reasons to use Flumine over raw API calls.
from flumine import Flumine, clients
from flumine.streams.datastream import DataStream
import betfairlightweight
trading = betfairlightweight.APIClient(
username="your_username",
password="your_password",
app_key="your_live_key",
certs="/path/to/certs/",
)
trading.login()
# Flumine uses the Streaming API automatically
framework = Flumine(client=clients.BetfairClient(trading))
# Add a strategy that processes streaming updates
class OddsMonitor(flumine.BaseStrategy):
def process_market_book(self, market, market_book):
for runner in market_book.runners:
if runner.ex.available_to_back:
best_back = runner.ex.available_to_back[0].price
print(f"{runner.selection_id}: best back = {best_back}")
framework.add_strategy(OddsMonitor(
market_filter={"event_type_ids": ["1"]},
))
framework.run() Placing Bets Programmatically
The Betfair API's placeOrders endpoint places back and lay bets. You specify the market ID, selection ID, side (BACK or LAY), price, and size.
Placing a back bet (raw API)
from betfairlightweight.resources.bettingresources import (
PlaceInstruction, LimitOrder
)
# Place a £10 back bet on selection 12345 at odds 2.0
instructions = [
PlaceInstruction(
order_type="LIMIT",
selection_id=12345,
side="BACK",
limit_order=LimitOrder(
size=10.0,
price=2.0,
persistence_type="LAPSE", # Cancel if unmatched at market start
),
)
]
result = trading.betting.place_orders(
market_id="1.234567890",
instructions=instructions,
customer_ref="my-bet-001", # Optional: your own reference
)
for order in result.instruction_reports:
print(f"Status: {order.status}")
print(f"Bet ID: {order.bet_id}")
print(f"Average price matched: {order.average_price_matched}") Placing bets via Flumine (recommended)
from flumine.order.trade import Trade
from flumine.order.order import LimitOrder
class FootballBackStrategy(flumine.BaseStrategy):
def process_market_book(self, market, market_book):
for runner in market_book.runners:
if runner.status != "ACTIVE":
continue
best_back = runner.ex.available_to_back
if not best_back:
continue
price = best_back[0].price
# Only back if price is above our threshold
if price < 2.5:
continue
# Check we haven't already placed a bet on this runner
if market.context.get(f"bet_placed_{runner.selection_id}"):
continue
trade = Trade(
market_id=market.market_id,
selection_id=runner.selection_id,
handicap=runner.handicap,
strategy=self,
)
order = LimitOrder(
price=price,
size=10.0,
persistence_type="LAPSE",
)
trade.place_order(order)
market.place_order(order)
# Mark as placed to avoid duplicates
market.context[f"bet_placed_{runner.selection_id}"] = True Error Handling and Rate Limits
The Betfair API returns structured error codes. Handling them correctly is the difference between a script that runs for months and one that crashes after 20 minutes.
Rate limits
| Limit type | Value |
|---|---|
| REST API requests | 1,000 per hour (standard accounts) |
| Streaming connections | 10 concurrent connections |
| Streaming subscriptions | 200 markets per connection |
| placeOrders per second | 5 requests per second |
| listMarketBook batch size | 40 markets per call |
| listRunnerBook batch size | 40 runners per call |
Common error codes and how to handle them
import betfairlightweight
from betfairlightweight.exceptions import (
APIError,
LoginError,
SessionTokenError,
)
import time
import logging
logger = logging.getLogger(__name__)
def safe_place_order(trading, market_id, instructions, max_retries=3):
"""Place an order with retry logic for transient errors."""
for attempt in range(max_retries):
try:
result = trading.betting.place_orders(
market_id=market_id,
instructions=instructions,
)
return result
except SessionTokenError:
# Session expired — re-login and retry
logger.warning("Session expired, re-logging in...")
trading.login()
continue
except APIError as e:
error_code = e.error_code
if error_code == "TOO_MUCH_DATA":
# Reduce batch size and retry
logger.warning("TOO_MUCH_DATA — reduce batch size")
time.sleep(1)
continue
elif error_code == "RATE_LIMIT_EXCEEDED":
# Back off and retry
wait = 2 ** attempt # Exponential backoff: 1s, 2s, 4s
logger.warning(f"Rate limit exceeded — waiting {wait}s")
time.sleep(wait)
continue
elif error_code == "MARKET_SUSPENDED":
# Market suspended — do not retry
logger.info("Market suspended, skipping order")
return None
elif error_code == "INSUFFICIENT_FUNDS":
# Not enough balance — stop
logger.error("Insufficient funds")
raise
else:
logger.error(f"Unhandled API error: {error_code}")
raise
logger.error(f"Failed after {max_retries} attempts")
return None Common pitfalls
Using the delay key in production
The delay key returns 1-second-old data. For live arb execution, this is too slow. Always use the live key in production.
Polling instead of streaming
Polling listMarketBook every second burns through your 1,000 requests/hour limit in 17 minutes. Use the Streaming API for real-time data.
Not handling session expiry
Betfair sessions expire after 4 hours of inactivity. Your script must catch SessionTokenError and re-authenticate automatically.
Placing duplicate orders
Without deduplication logic, a strategy can place multiple orders on the same runner in the same market. Track placed bets in market.context.
Ignoring LAPSE vs PERSIST
LAPSE orders are cancelled at market start if unmatched. PERSIST orders remain in-play. For pre-match strategies, always use LAPSE.
Not testing in simulation mode
Flumine has a simulation mode (SimulatedBetfairClient) that replays historical data without placing real bets. Always test there first.
The Betfair Streaming API pushes real-time market updates via WebSocket and does not count against the 1,000 requests/hour rate limit. It is the correct approach for any automation that requires live odds monitoring.
Full Working Example: Football Value Strategy
This complete example uses Flumine to monitor Premier League Match Odds markets and back selections where the best available back price exceeds a configurable threshold. It includes authentication, error handling, and logging.
"""
football_value_strategy.py
Monitors Premier League Match Odds markets and backs selections
where the best back price exceeds a minimum threshold.
Requirements:
pip install flumine betfairlightweight
Usage:
python football_value_strategy.py
"""
import logging
import flumine
from flumine import Flumine, clients
from flumine.order.trade import Trade
from flumine.order.order import LimitOrder
import betfairlightweight
# ── Config ────────────────────────────────────────────────────
USERNAME = "your_betfair_username"
PASSWORD = "your_betfair_password"
APP_KEY = "your_live_api_key"
CERTS_PATH = "/path/to/certs/"
MIN_PRICE = 2.5 # Only back at 2.5 or higher
STAKE = 10.0 # £10 per bet
MAX_BETS = 5 # Maximum bets per market
# ── Logging ───────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
logger = logging.getLogger(__name__)
# ── Strategy ──────────────────────────────────────────────────
class FootballValueStrategy(flumine.BaseStrategy):
def check_market_book(self, market, market_book):
"""Return True only for pre-match, open markets."""
if market_book.status != "OPEN":
return False
if market_book.inplay:
return False # Pre-match only
return True
def process_market_book(self, market, market_book):
"""Process each market update from the Streaming API."""
bets_placed = market.context.get("bets_placed", 0)
if bets_placed >= MAX_BETS:
return
for runner in market_book.runners:
if runner.status != "ACTIVE":
continue
if not runner.ex.available_to_back:
continue
best_back_price = runner.ex.available_to_back[0].price
best_back_size = runner.ex.available_to_back[0].size
# Skip if price below threshold or insufficient liquidity
if best_back_price < MIN_PRICE:
continue
if best_back_size < STAKE:
continue
# Skip if already bet on this runner
runner_key = f"bet_{runner.selection_id}"
if market.context.get(runner_key):
continue
# Place the bet
trade = Trade(
market_id=market.market_id,
selection_id=runner.selection_id,
handicap=runner.handicap,
strategy=self,
)
order = LimitOrder(
price=best_back_price,
size=STAKE,
persistence_type="LAPSE",
)
trade.place_order(order)
market.place_order(order)
market.context[runner_key] = True
market.context["bets_placed"] = bets_placed + 1
logger.info(
f"Placed back bet: {market.market_id} "
f"selection={runner.selection_id} "
f"price={best_back_price} size={STAKE}"
)
# ── Main ──────────────────────────────────────────────────────
if __name__ == "__main__":
trading = betfairlightweight.APIClient(
username=USERNAME,
password=PASSWORD,
app_key=APP_KEY,
certs=CERTS_PATH,
)
trading.login()
logger.info("Authenticated with Betfair API")
framework = Flumine(client=clients.BetfairClient(trading))
strategy = FootballValueStrategy(
market_filter={
"event_type_ids": ["1"], # Football
"competition_ids": ["10932509"], # Premier League
"market_types": ["MATCH_ODDS"],
},
max_selection_exposure=STAKE * MAX_BETS,
max_order_exposure=STAKE,
)
framework.add_strategy(strategy)
logger.info("Starting Flumine framework...")
framework.run() This is a simplified example for educational purposes. Production strategies require position sizing, CLV tracking, and risk management logic.