BlogOdds APIs

Soccer Odds API Guide to Three-Way Markets and Draws

A soccer odds API returns bookmaker prices for soccer matches in a form your code can read: the three-way result (home, draw, away), the spread and total, and sometimes half-time lines and player props. The part that trips up developers who have worked with American leagues is the draw. A soccer result market has three outcomes, so anything you wrote for two-way moneylines, from margin removal to best-price logic, needs a third side.

The other two problems are scale and time. One sport covers hundreds of competitions, from top divisions to reserve leagues, and kickoffs land at every hour of the day across time zones. This guide covers how ZippyOddsAPI handles all three and what to watch for with any provider.

The draw changes the market shape

In basketball or the NFL, a moneyline has two sides. In soccer, the standard result market has three, and it usually settles on the score after regular time plus stoppage time, not extra time or penalties. Check each book's own rules for a cup tie, because that's where the difference matters.

In ZippyOddsAPI the three-way result has its own market key, three_way, and its outcomes carry side values of home, draw and away. The main shortcut means moneyline, spread and total, and it doesn't include three_way. So a request for soccer results has to ask for it by name:

curl -s "https://zippyoddsapi.com/v1/odds?league=soccer&markets=three_way,total&format=decimal" \
  -H "Authorization: Bearer $ZIPPYODDS_KEY"

markets=all also includes it, along with team totals and alternate lines. If you'd rather start from the American leagues, where every result is two-way, the NFL API and NBA API guides cover what's different there.

Removing the margin from a three-way price

Say a book prices a match at home +150, draw +240 and away +190. Convert each to an implied chance:

OutcomePriceImplied chance
Home+15040.00%
Draw+24029.41%
Away+19034.48%

The three add up to 103.89%, so the book's margin is 3.89%. The simplest way to remove it is to divide each by 1.0389, which gives fair chances of 38.50%, 28.31% and 33.19%, or fair prices of about +160, +253 and +201.

Dividing evenly assumes the margin sits evenly across all three outcomes. On three-way markets it often doesn't. ZippyOddsAPI removes each reference book's margin with Shin's method, which on a three-way market allows for some bettors knowing more than the book, so its fair numbers will differ a little from the even split. The method is fixed, and the docs explain the weights and gates behind it.

Asking for format=probability makes the margin a one-line sum, because each price comes back as an implied chance:

for event in body["data"]:
    for book in event["bookmakers"]:
        for market in book["markets"]:
            if market["key"] != "three_way" or market["suspended"]:
                continue
            prices = [o["price"] for o in market["outcomes"] if o["price"] is not None]
            if len(prices) == 3:
                print(event["competition"], event["home"]["name"], book["name"], f"margin {sum(prices) - 1:.2%}")

One league id, many competitions

ZippyOddsAPI files every match under one league id, soccer. Each event carries a competition field with the name the sources gave, so you filter or group by competition in your own code. /v1/leagues shows what is on the board for soccer right now, and the coverage page shows which books are feeding. A league id being accepted doesn't mean it has matches at that moment.

Matching is where soccer is hardest. Clubs field B sides, reserve teams, youth teams and women's teams under nearly the same name, and a false merge between a first team and its B side would invent a price gap of several points. ZippyOddsAPI joins two soccer listings with very different kickoff times only when both name their competition and the very same team names, so Castilla never joins Real Madrid's game. eSoccer and virtual matches, where the same pairing replays all day, are never joined that way.

When you build your own matching across providers, err the same way. A missed merge costs you one comparison. A false one costs you a fake edge that your code will happily act on.

Kickoff times you can't take at face value

A source sometimes lists a match at exactly midnight UTC when it only knows the date. That's a placeholder, except that midnight UTC is also a real evening kickoff in the Americas. ZippyOddsAPI treats midnight as a date without a time only when more feeds agree on a later time of day, and otherwise lets it stand.

Each event has a start_time taken from what most sources say, start_time_spread_minutes showing how far apart they were, and start_time_tbc when no source gave a real time. Use these before you mark a price as pre-match or in-play. A price read before a kickoff that has since passed is not a current price, whatever it pays.

In-play prices

A soccer live odds API has to cope with prices that move on every attack, card and substitution. Once a match has started, ZippyOddsAPI judges every price by a 90-second age limit, and anything read before kickoff counts as stale. Live events need a plan that includes live data, and the pricing page lists which do.

For in-play work, the stream at /v1/stream sends a snapshot, then a delta when a market changes and an event message when a status or score changes. Polling /v1/odds every few seconds works too, but it spends 2 credits a call, and deltas arrive as each venue is read, not on a fixed tick.

A short checklist

  • Ask for three_way explicitly, and treat draw as a first-class side in every calculation.
  • Group by competition, and keep B sides, youth and women's matches apart.
  • Check start_time_tbc and the age of every price before calling anything live or pre-match.
  • Remove the margin across three outcomes, not two.

The wider picture, including how prices are dated and what each request costs, is in our sports odds API guide.