> ## Documentation Index
> Fetch the complete documentation index at: https://docs.odds-api.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Bet Settlement API

> Settle sports bets with one API call. Send the bets you hold and get back won, lost, void or half won for 64 markets in 20 sports, graded from final scores.

## What the settlement API does

You hold a list of bets. You want to know which ones won. The settlement API answers that in two calls:

| Endpoint | Question it answers | Plans |
| - | - | - |
| `GET /v3/settlements` | What happened in these events? Is the result final? | Paid |
| `POST /v3/settlements/grade` | Did these bets win? | Paid |

You send the event, the market, the side and the line, the same four things you already get from `/v3/odds`. We send back `won`, `lost`, `void`, `half_won` or `half_lost`.

It covers 64 markets in 20 sports: moneyline, spreads and Asian handicaps, 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.

One rule runs through all of it. We only grade a bet when the stored result proves the answer. If something is missing or does not add up, you get `ungradeable` with a reason, never a guess.

## Quick start

### 1. Check what happened

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.odds-api.io/v3/settlements?apiKey=YOUR_API_KEY&eventIds=74919888,73101436"
  ```

  ```javascript JavaScript theme={null}
  const apiKey = process.env.ODDS_API_KEY;
  const ids = [74919888, 73101436].join(',');

  const res = await fetch(
    `https://api.odds-api.io/v3/settlements?apiKey=${apiKey}&eventIds=${ids}`
  );
  const { settlements, notFound } = await res.json();
  ```

  ```python Python theme={null}
  import os
  import requests

  res = requests.get(
      "https://api.odds-api.io/v3/settlements",
      params={"apiKey": os.environ["ODDS_API_KEY"], "eventIds": "74919888,73101436"},
  )
  data = res.json()
  ```
</CodeGroup>

```json theme={null}
{
  "settlements": [
    {
      "eventId": 74919888,
      "sport": "Football",
      "status": "settled",
      "final": true,
      "settledAt": "2026-10-11T15:39:12Z",
      "scores": {
        "home": 3,
        "away": 3,
        "periods": {
          "p1": { "home": 1, "away": 2 },
          "p2": { "home": 2, "away": 0 },
          "ft": { "home": 3, "away": 2 },
          "ot": { "home": 3, "away": 3 }
        }
      }
    }
  ],
  "notFound": []
}
```

Every id you send gets an answer. A cancelled match comes back with `"status": "cancelled"`. An id we have never seen is listed in `notFound`.

### 2. Grade your bets

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.odds-api.io/v3/settlements/grade?apiKey=YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "selections": [
        { "ref": "bet-1", "eventId": 74919888, "market": "ML", "side": "home" },
        { "ref": "bet-2", "eventId": 74919888, "market": "Spread", "hdp": -0.75, "side": "home" },
        { "ref": "bet-3", "eventId": 74919888, "market": "Totals", "hdp": 5.5, "side": "over" }
      ]
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    `https://api.odds-api.io/v3/settlements/grade?apiKey=${apiKey}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        selections: [
          { ref: 'bet-1', eventId: 74919888, market: 'ML', side: 'home' },
          { ref: 'bet-2', eventId: 74919888, market: 'Spread', hdp: -0.75, side: 'home' },
          { ref: 'bet-3', eventId: 74919888, market: 'Totals', hdp: 5.5, side: 'over' },
        ],
      }),
    }
  );
  const { grades } = await res.json();
  ```

  ```python Python theme={null}
  res = requests.post(
      "https://api.odds-api.io/v3/settlements/grade",
      params={"apiKey": os.environ["ODDS_API_KEY"]},
      json={
          "selections": [
              {"ref": "bet-1", "eventId": 74919888, "market": "ML", "side": "home"},
              {"ref": "bet-2", "eventId": 74919888, "market": "Spread", "hdp": -0.75, "side": "home"},
              {"ref": "bet-3", "eventId": 74919888, "market": "Totals", "hdp": 5.5, "side": "over"},
          ]
      },
  )
  grades = res.json()["grades"]
  ```
</CodeGroup>

```json theme={null}
{
  "grades": [
    { "ref": "bet-1", "eventId": 74919888, "result": "won", "reason": null, "final": true,
      "basis": { "scope": "regulation", "home": 3, "away": 2 } },
    { "ref": "bet-2", "eventId": 74919888, "result": "half_won", "reason": null, "final": true,
      "basis": { "scope": "regulation", "home": 3, "away": 2 } },
    { "ref": "bet-3", "eventId": 74919888, "result": "lost", "reason": null, "final": true,
      "basis": { "scope": "regulation", "home": 3, "away": 2 } }
  ]
}
```

This match finished 3-2 after 90 minutes and 3-3 after extra time. Football bets settle on 90 minutes, so the home moneyline won. `basis` shows you the score we used.

Grades come back in the order you sent the selections, and `ref` is echoed so you can match them to your own records. You can send up to 50 selections in one request, on as many events as you like.

## How to describe a bet

A selection uses the same fields as an odds row from `/v3/odds`. If you stored the market, line, label and side when the bet was placed, send them back unchanged.

| Field | Required | What to send |
| - | - | - |
| `eventId` | Yes | The event id, as a number or a string |
| `market` | Yes | The market name as we serve it: `ML`, `Spread`, `Totals HT`, `Corners Totals`, `Player Props` |
| `side` | Yes | `home`, `away`, `draw`, `over`, `under`, `yes`, `no`, `odd`, `even`, `1x`, `12`, `x2`, `none`, or a pair such as `draw/home` for half time / full time |
| `hdp` | For markets with a line | The line as served. Handicaps are stated for the home side. For totals it is the total |
| `label` | For player props | The row label as served, for example `Arike Ogunbowale (Assists)` |
| `tieRule` | No | `lose` or `void`. See [ties on the moneyline](#ties-on-the-moneyline) |
| `ref` | No | Your own id for the bet. We send it back |

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. A name we do not recognise comes back as `unsupported_market`. We never pick the closest match.

<Note>
  The handicap is always the home team's. If the row says `Spread` with `hdp: -1.5` and you backed the away team, send `"side": "away"` with `"hdp": -1.5`. We work out that your side is +1.5.
</Note>

## Reading the answer

| `result` | Meaning |
| - | - |
| `won` | The bet won |
| `lost` | The bet lost |
| `half_won` | Quarter-line bet: half the stake won, half is returned |
| `half_lost` | Quarter-line bet: half the stake lost, half is returned |
| `void` | Stake returned. `reason` says why: `push` or `event_cancelled` |
| `pending` | Not decided yet. `reason` says what we are waiting for |
| `ungradeable` | We cannot settle this bet for you. `reason` says why |

`reason` is `null` for `won`, `lost`, `half_won` and `half_lost`. It is always filled in for `void`, `pending` and `ungradeable`.

| `reason` | Goes with | What it means |
| - | - | - |
| `push` | `void` | Landed exactly on the line, or a tie where the stake is returned |
| `event_cancelled` | `void` | The match was cancelled or postponed |
| `event_not_started` | `pending` | The match has not kicked off |
| `event_live` | `pending` | The match is being played |
| `awaiting_result` | `pending` | The match is over and we are confirming the result |
| `awaiting_stats` | `pending` | We are waiting for final corners, cards or box scores |
| `event_not_found` | `ungradeable` | We do not know this event id |
| `unsupported_market` | `ungradeable` | We do not grade this market, or not for this sport |
| `invalid_side` | `ungradeable` | This side does not exist on this market |
| `invalid_line` | `ungradeable` | The line is missing, or the market has no line |
| `invalid_label` | `ungradeable` | The label is missing or does not belong on this market |
| `incomplete_score` | `ungradeable` | The score this market needs is missing or does not add up |
| `ambiguous_tie` | `ungradeable` | A level score on a moneyline where we need your tie rule |
| `player_not_matched` | `ungradeable` | We could not identify the player with certainty |
| `stats_unavailable` | `ungradeable` | The statistic this market needs was not recorded |
| `result_unavailable` | `ungradeable` | More than 72 hours after kickoff and no result exists |

## When is a result final?

Every answer carries `final`. Read it as "you can stop checking".

Scores are sometimes corrected in the first hours after a match. We check each result against the official one, and `final` turns `true` once that check is done, or 48 hours after the match ended.

A simple loop that works:

1. Grade your open bets.
2. Pay out or settle on any answer you are comfortable with. Most products wait for `final: true`.
3. Keep the selections where `final` was `false`, and send them again later. Once a minute is the most often you need to.

Check `final` on each grade, not on the event. An event can be final while a player prop on it is still `awaiting_stats`.

<Note>
  A cancelled event becomes final 48 hours after its scheduled kickoff. If the match is rescheduled later, the event goes back to `pending` under the same event id. Whether a bet you already voided stays void is your rule to set; many bookmakers void a match that is not played within 48 hours.
</Note>

## How bets are settled

These are the rules this API applies. They follow common bookmaker practice, but books differ on details, so check them against your own terms.

### Which score counts

* **Football, futsal, handball, rugby.** Every market settles on regulation time. In football that is 90 minutes plus stoppage time. Extra time and penalty shootouts never count.
* **Basketball, American football, baseball, ice hockey.** `ML`, `Spread`, `Totals`, team totals, `Odd/Even` and `Both Teams To Score` include overtime and extra innings. `3-Way Result`, `Double Chance`, `European Handicap` and `Draw No Bet` settle on regulation time.
* **Ice hockey shootouts.** The shootout winner gets one goal added. A game that is 2-2 after overtime and won in a shootout is a 3-2 win for totals and handicaps too.
* **Tennis, volleyball, table tennis, badminton, padel.** The match score is sets. Esports is maps. Darts and snooker use the match score. MMA and boxing use the winner.

### Half lines and quarter lines

A whole-number line can land exactly, and then the bet is `void` with reason `push`.

A quarter line (`-0.25`, `2.75`) is two bets at the lines either side. Home `-0.75` when the home team wins by one is half on `-0.5` (won) and half on `-1` (pushed), so the answer is `half_won`.

### Ties on the moneyline

`3-Way Result` always has a draw, so a level score means `draw` wins and both teams lose.

MMA and boxing are the exception to both rules. We store the winner of a fight, and a result with no winner could be a draw or a no contest. Those bets come back `incomplete_score`, whatever `tieRule` says.

`ML` depends on the sport:

| Sport | Level score on `ML` |
| - | - |
| Football, futsal, handball | The draw is its own outcome. `home` and `away` lose, `draw` wins |
| American football, baseball | `void` with reason `push` |
| Everything else | Depends on the bookmaker. Send `tieRule` |

In sports such as rugby and Aussie rules, some bookmakers offer the moneyline with a draw price and some without. We cannot know which kind your bet was. Send `"tieRule": "lose"` if the draw was a separate outcome at your book, or `"tieRule": "void"` if stakes are returned. Without it you get `ambiguous_tie`, and only when the score really was level.

### Cancelled and postponed matches

Every bet on a cancelled event comes back `void` with reason `event_cancelled`. Postponed and abandoned matches are usually reported as cancelled too. A match that never gets a result and is never marked cancelled stays `pending` with reason `awaiting_result`, and turns `ungradeable` with reason `result_unavailable` 72 hours after kickoff. That includes a market that was already decided when a match was abandoned, such as over 0.5 goals after a goal had been scored.

### Tennis

We grade a tennis match when the stored set scores show that it was played to the end: the winner has the sets needed, and every set is a finished set.

If a player retires or gives a walkover, the bet comes back `incomplete_score` and you apply your own retirement rule. A few tennis results arrive without game scores for each set. Those also come back `incomplete_score`.

Some bookmakers list the games handicap and the games total under plain `Spread` and `Totals`. A set score can never reach those lines, so we read them as games: a `Spread` handicap larger than 3 in either direction (for example `-4.5` or `3.5`), or a `Totals` line above 5, is graded on total games, and `basis.scope` says `games`. Lines inside the sets range are graded on sets. In volleyball, table tennis, badminton and esports, a plain `Spread` or `Totals` line outside the sets or maps range is a points or rounds line, and it comes back `incomplete_score`. `Map Handicap` and `Total Maps` are always graded on maps.

`Spread (Games)` and `Totals (Games)` add up the games in every set. A match decided by a match tie-break is not graded on games markets.

### Corners, bookings and first team to score

These are football only and are graded from post-match statistics. They are ready about 30 minutes after full time. Until then the answer is `pending` with reason `awaiting_stats`.

* **Corners** count each team's corners.
* **Bookings** use booking points: a yellow card is 1, a red card is 2, and a second yellow that becomes a red adds 2 more.
* **First Team To Score** follows the scoreboard, so an own goal counts for the team whose score went up. A match that is 0-0 after 90 minutes settles as `none`.

Full-match corners and bookings are only graded for matches with no extra time, because the stored totals cover the whole match. For these markets you get the verdict without the underlying count.

### Player props

Send `"market": "Player Props"` with the label as served, such as `LeBron James (Points)`, the line in `hdp`, and `over` or `under`.

Props are graded for basketball, baseball, ice hockey and American football, from the official box score. We match the player by name and refuse when the name could be two people. A player who does not appear in the box score comes back `player_not_matched`. We do not assume the player sat out.

## Supported markets

| Market | Sports |
| - | - |
| `ML`, `Moneyline` | All 20 sports |
| `3-Way Result` | Football, futsal, handball, rugby, Aussie rules, basketball, American football, baseball, ice hockey, MMA, boxing |
| `Spread`, `Totals` | Football, futsal, handball, rugby, Aussie rules, basketball, American football, baseball, ice hockey, tennis, table tennis, volleyball, beach volleyball, badminton, padel, esports |
| `Goals Over/Under` | Football |
| `Team Total Home`, `Team Total Away` | Football, futsal, handball, rugby, Aussie rules, basketball, American football, baseball, ice hockey |
| `European Handicap`, `Double Chance`, `Draw No Bet`, `Both Teams To Score`, `Odd/Even` | The same team sports |
| `Half Time / Full Time` | Football, futsal, handball, rugby, basketball, American football |
| `ML HT`, `Spread HT`, `Totals HT`, `Team Total Home HT`, `Team Total Away HT`, `European Handicap HT`, `Double Chance HT`, `Draw No Bet HT`, `Both Teams To Score HT` | Football, futsal, handball, rugby, Aussie rules, basketball, American football |
| `ML 2H`, `Spread 2H`, `Totals 2H`, `European Handicap 2H`, `Both Teams To Score 2H` | Football, futsal, handball, rugby, basketball, American football |
| `Spread 1Q`, `Totals 1Q` | Basketball, American football, Aussie rules, ice hockey (first period) |
| `First 5 Innings ML`, `First 5 Innings Spread`, `First 5 Innings Totals`, `1st Inning ML`, `1st Inning Totals` | Baseball |
| `Spread (Games)`, `Totals (Games)`, `ML 2nd Set`, `Totals 2nd Set`, `ML 3rd Set` | Tennis |
| `ML 1st Set`, `Totals 1st Set` | Tennis, volleyball |
| `Map Handicap`, `Total Maps`, `1st Map ML` | Esports |
| `Corners Totals`, `Corners Spread`, `Corners Totals Home`, `Corners Totals Away`, `Alternative Corners` | Football |
| `Corners Totals HT`, `Corners Spread HT`, `Corners Totals Home HT`, `Corners Totals Away HT` | Football |
| `Bookings Totals`, `Bookings Spread`, `Bookings Totals Home`, `Bookings Totals Away` | Football |
| `Bookings Totals HT`, `Bookings Spread HT`, `Bookings Totals Home HT`, `Bookings Totals Away HT` | Football |
| `First Team To Score` | Football |
| `Player Props` | Basketball, baseball, ice hockey, American football |

In basketball and American football the second half includes overtime.

Not graded yet: correct score, goalscorer markets, winning margin, football player props, American football anytime touchdown props, cricket, and outrights. They come back `unsupported_market`.

## Why some bets come back ungradeable

A wrong grade costs you money and a customer's trust. An ungradeable one costs a manual check. So we refuse whenever the stored data cannot prove the answer. For example:

* A football result with no 90-minute score recorded, where we cannot rule out extra time.
* A basketball score that is lower than the sum of its quarters.
* A tennis match that stopped before the last set was finished.
* A player prop where two players in the game share the name on the bet.

To give you a feel for how often that happens, we ran 22,903 bets that our own bet tracker settled in the 30 days to 12 October 2026 through this API.

| Bets | Settled automatically |
| - | - |
| Football, score markets | 98.0% |
| Basketball, score markets | 99.1% |
| Ice hockey, score markets | 99.2% |
| American football, score markets | 98.5% |
| Baseball, score markets | 99.6% |
| Handball, score markets | 97.3% |
| Esports | 99.8% |
| Tennis | 94.4% |
| Football corners | 82.4% |
| Football bookings | 88.3% |
| Player props, basketball, baseball and ice hockey | 99% |

The tennis bets we do not grade are mostly retirements, walkovers and a few results without game scores. Treat these numbers as a guide. They come from one month of one product's bets, mostly football main markets, on events that had already been settled once.

## Limits and plans

* Both endpoints are on every paid plan and each call counts as one request.
* `GET /v3/settlements` takes up to 50 event ids.
* `POST /v3/settlements/grade` takes up to 50 selections and a request body of up to 64 KB.
* Free plans get a `403`. You can still read final scores from [`/v3/historical/events`](/guides/historical).

## Frequently asked questions

### How do I settle a bet with an API?

Store the event id, market, line and side when the bet is placed. After the match, send them to `POST /v3/settlements/grade`. The response says `won`, `lost`, `void`, `half_won` or `half_lost` for each bet, and `final` tells you when the result is confirmed.

### Does the score include extra time?

Not for football. Football, futsal, handball and rugby bets settle on regulation time, and we grade them on the 90-minute score even when the match went to extra time or penalties. In basketball, American football, baseball and ice hockey, the moneyline, spread and totals include overtime.

### What happens when a match is postponed or abandoned?

In most cases the event status becomes `cancelled`, and every bet on it grades as `void` with reason `event_cancelled`. If the match is rescheduled later, the event goes back to `pending` under the same id. If a match is never marked cancelled and never gets a result, its bets stay `pending` with reason `awaiting_result`, then turn `ungradeable` with reason `result_unavailable` 72 hours after kickoff.

### Can I settle Asian handicap bets, including quarter lines?

Yes. Send them as `Spread` with the line in `hdp`. Quarter lines return `half_won` or `half_lost` when half the stake is returned.

### Can I settle player props?

Yes, for basketball, baseball, ice hockey and American football. Send `Player Props` with the label, such as `Nikola Jokic (Rebounds)`, and the line.

### Can I settle corners and cards?

Yes, for football. Use `Corners Totals`, `Corners Spread`, `Bookings Totals`, `Bookings Spread` and their team and half-time versions. They are graded about 30 minutes after full time.

### How soon after a match can I settle?

Score markets are usually ready a few minutes after the final whistle. Statistics markets take about 30 minutes. Until then the answer is `pending`.

### What does `final: false` mean?

The answer is our current result, but the score has not been confirmed against the official one yet. Corrections are rare. If you pay out on results that are not final, check them again later.

### Why did my bet come back `ungradeable`?

Read `reason`. It is either something in the request, such as a market name we do not grade or a missing line, or it is the data: the score or statistic the market needs was not recorded in a way we can trust.

### Do I need to tell you which bookmaker the bet was placed with?

No. Settlement uses the result of the event, not a bookmaker's own grading. The one place bookmakers differ is a tie on the moneyline in a few sports, which is what `tieRule` is for.

## Related

* [Fetching odds](/guides/fetching-odds): where the market, line and side for each selection come from.
* [Historical odds](/guides/historical): closing lines and results for past events.
* [API reference](/api-reference/introduction): full request and response schemas.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.