Bet Grading API: Settle Bets as Won, Lost or Void in Python

James Whitfield
To settle a sports bet in code you need three things: the final result, the rule for which score counts, and the arithmetic for the line. A bet grading API does all three. You send the event, the market, the line and the side, and it answers won, lost, void, half_won or half_lost.
This guide uses the two settlement endpoints added to the Odds-API.io v3 API on 12 October 2026. Every response below was captured from the live API that day, using real games from the weekend before.
What a bet grading API does
There are two endpoints. GET /v3/settlements tells you what happened in up to 50 events: the status, the final score by period, and whether the result is final. POST /v3/settlements/grade takes up to 50 bets and returns a verdict for each one.
Grading covers 64 markets in 20 sports: moneyline, spreads and Asian handicaps with quarter lines, totals, both teams to score, double chance, draw no bet, half-time markets, tennis games and sets, esports maps, football corners and bookings, and player props in basketball, baseball, ice hockey and American football. Both endpoints are on every paid plan, and each call counts as one request.
You do not say which bookmaker the bet was placed with. The grade comes from the result of the event, not from a bookmaker's own settlement.
Why comparing a bet to the final score goes wrong
Grading looks like one comparison: read the score, check it against the line. Four games from last week show where that breaks.
Extra time. A football match on 11 October finished 3-2 after 90 minutes and 3-3 after extra time. The match score says 3-3. Football bets settle on 90 minutes, so the home moneyline won.
Shootouts. Vegas Golden Knights v Toronto Maple Leafs on 9 October was 3-3 after overtime and Vegas won the shootout. In ice hockey the shootout winner gets one goal added, so over 6.5 goals won on a 4-3 result.
Quarter lines. Arsenal beat Leeds United 2-1. A bet on Arsenal at -0.75 is two bets, half at -0.5 and half at -1. The first half won and the second was returned, so the answer is half_won.
Ties. Washington Commanders v New York Giants ended 27-27 after overtime. A moneyline bet on either team is returned.
A grading API holds these rules in one place, and for most score markets the answer shows the score it was based on.
Check the result first
/v3/settlements answers for every id you send. This request asks for the hockey game above, a cancelled football match, a match that has not kicked off, and an id that does not exist.
curl --compressed "https://api.odds-api.io/v3/settlements?apiKey=YOUR_KEY&eventIds=72908962,73215748,72221318,999"{
"settlements": [
{
"eventId": 72908962,
"sport": "Ice Hockey",
"status": "settled",
"final": true,
"settledAt": "2026-10-09T05:03:27.240094Z",
"scores": {
"home": 3,
"away": 3,
"periods": {
"p1": {"home": 1, "away": 3},
"p2": {"home": 2, "away": 0},
"p3": {"home": 0, "away": 0},
"ft": {"home": 3, "away": 3},
"ot": {"home": 3, "away": 3},
"ap": {"home": 4, "away": 3}
}
}
},
{"eventId": 73215748, "sport": "Football", "status": "cancelled", "final": false},
{"eventId": 72221318, "sport": "Football", "status": "pending", "final": false}
],
"notFound": ["999"]
}status is pending, live, awaiting_result, settled or cancelled. In periods, ft is the score at the end of regulation, ot is the score after overtime, and ap is the score after the shootout. The periods are reordered here for reading.
The cancelled match is not final yet. A cancelled event becomes final 48 hours after its scheduled kickoff, because a postponed match can be rescheduled under the same event id.
Describe the bet with the fields you stored
A selection uses the same fields as an odds row from /v3/odds. This is one Spread row for Everton v Chelsea on 17 October, as served:
{"hdp": -0.75, "home": "3.700", "away": "1.260"}Store the event id, the market name, the line and the side when the bet is placed. To grade it, send the same four values back:
{"ref": "bet-1", "eventId": 72221318, "market": "Spread", "hdp": -0.75, "side": "away"}The handicap is always the home team's. For a bet on the away side you still send hdp as served, here -0.75, and the API works out that your side is +0.75. ref is your own id for the bet and is echoed in the answer. Player props add a label, sent as served, such as Dak Prescott (TD Passes).
Market names are matched exactly, apart from upper and lower case. Corners Totals is not Totals, and Spread HT is never graded on the full-time score. The Asian handicap guide on this blog covers hdp and quarter lines in more detail.
Grade a batch of bets in Python
Here are twelve stored bets on last week's games, saved as bets.json. They cover the cases above plus corners, a player prop, a cancelled match, a match still to be played and a market the API does not grade.
[
{"ref": "arsenal-ah", "eventId": 72221292, "market": "Spread", "hdp": -0.75, "side": "home"},
{"ref": "villa-over4", "eventId": 72221294, "market": "Totals", "hdp": 4, "side": "over"},
{"ref": "united-dnb", "eventId": 72221308, "market": "Draw No Bet", "side": "home"},
{"ref": "liverpool-btts", "eventId": 72221306, "market": "Both Teams To Score", "side": "yes"},
{"ref": "chelsea-corners", "eventId": 72221296, "market": "Corners Totals", "hdp": 9.5, "side": "over"},
{"ref": "extra-time-ml", "eventId": 74919888, "market": "ML", "side": "home"},
{"ref": "vegas-over", "eventId": 72908962, "market": "Totals", "hdp": 6.5, "side": "over"},
{"ref": "commanders-ml", "eventId": 71515886, "market": "ML", "side": "home"},
{"ref": "dak-td-passes", "eventId": 71515872, "market": "Player Props", "label": "Dak Prescott (TD Passes)", "hdp": 2.5, "side": "under"},
{"ref": "abha-ml", "eventId": 73215748, "market": "ML", "side": "home"},
{"ref": "chelsea-next", "eventId": 72221318, "market": "ML", "side": "away"},
{"ref": "cowboys-score", "eventId": 71515872, "market": "Correct Score", "label": "16-24", "side": "away"}
]The script sends them 50 at a time, prints one line per bet and keeps the ones that are not final.
import json
import os
import requests
BASE = "https://api.odds-api.io/v3"
KEY = os.environ["ODDS_API_KEY"]
with open("bets.json") as f:
bets = json.load(f)
open_bets = []
for start in range(0, len(bets), 50):
chunk = bets[start:start + 50]
res = requests.post(f"{BASE}/settlements/grade",
params={"apiKey": KEY}, json={"selections": chunk})
res.raise_for_status()
for bet, grade in zip(chunk, res.json()["grades"]):
basis = grade.get("basis") or {}
score = f'{basis["home"]}-{basis["away"]} {basis["scope"]}' if "home" in basis else ""
print(f'{grade["ref"]:<16} {grade["result"]:<12} {grade["reason"] or "":<20} '
f'final={str(grade["final"]).lower():<6} {score}')
if not grade["final"]:
open_bets.append(bet)
print(f"{len(bets) - len(open_bets)} settled, {len(open_bets)} to check again later")Run against the live API on 12 October, it printed:
arsenal-ah half_won final=true 2-1 regulation
villa-over4 void push final=true 2-2 regulation
united-dnb void push final=true 1-1 regulation
liverpool-btts lost final=true 0-3 regulation
chelsea-corners lost final=true
extra-time-ml won final=true 3-2 regulation
vegas-over won final=true 4-3 full_game
commanders-ml void push final=true 27-27 full_game
dak-td-passes won final=true
abha-ml void event_cancelled final=false
chelsea-next pending event_not_started final=false
cowboys-score ungradeable unsupported_market final=true
10 settled, 2 to check again laterGrades come back in the order you sent the selections. reason is empty for won, lost, half_won and half_lost, and always filled in for void, pending and ungradeable.
Reading the results
The basis column is the score the grade used. regulation means the 90-minute score in football. full_game includes overtime, and in ice hockey the shootout goal, which is why the Vegas line reads 4-3.
Aston Villa v Brentford finished 2-2, so over 4 goals landed exactly on the line and is void with reason push. Manchester United v Tottenham finished 1-1, so the draw no bet stake is returned as well.
The corners bet and the player prop have no basis. Markets graded from match statistics or the box score return the verdict without the underlying count. Football corners and bookings are ready about 30 minutes after full time. Until then the answer is pending with reason awaiting_stats.
Correct score is not graded yet, so it comes back ungradeable with reason unsupported_market. A name the API does not recognise gets the same answer. It never picks the closest match.
When to settle: the final flag
Every grade carries final. Read it as: you can stop checking. Scores are sometimes corrected in the first hours after a game. final turns true once the result has been checked against the official one, or 48 hours after the game ended.
The loop in the script is the whole pattern. Grade your open bets, settle the ones that are final, and send the others again later. Once a minute is the most often you need to ask. Check final on each grade and not on the event, because an event can be final while a player prop on it is still waiting for the box score.
What the API will not guess
A bet is only graded when the stored result proves the answer. A football result with no 90-minute score, a basketball score lower than the sum of its quarters, a tennis match that ended in a retirement, or a prop where two players in the game share a name all come back ungradeable with a reason. You then apply your own rule or check the bet by hand.
To measure how often that happens, we ran 22,903 bets settled by our own bet tracker in the 30 days to 12 October 2026 through the API. It graded 98.0% of football score-market bets, 99.1% in basketball, 99.2% in ice hockey, 99.6% in baseball and 94.4% in tennis. Football corners came in at 82.4% and bookings at 88.3%. Treat these as a guide: they come from one month of one product's bets, mostly football main markets.
How do I settle bets automatically with an API?
Store the event id, market, line and side when the bet is placed. After the game, send them to POST /v3/settlements/grade. The response gives won, lost, void, half_won or half_lost for each bet, and final tells you when the result is confirmed.
Does a football bet include extra time?
No. Football, futsal, handball and rugby bets settle on regulation time, which in football is 90 minutes plus stoppage time. Extra time and penalty shootouts never count. In basketball, American football, baseball and ice hockey, the moneyline, spread and totals include overtime.
What happens to bets on a postponed or cancelled match?
The event status becomes cancelled, and every bet on it grades as void with reason event_cancelled. If the match is rescheduled, the event goes back to pending under the same id.
Getting started
Settlement is included in every paid plan, listed on the pricing page. The Bet Settlement guide in the docs has the full market list, every reason code and the settlement rules for each sport. To see where the market, line and side for a selection come from, read Odds Comparison API: 30 Bookmakers, One Request, and for prop labels the player props guide.
For a one-page overview of the endpoints, result values and supported markets, see the Bet Settlement API page.
