Esports API: The Complete Developer Guide (2026)

Updated · Published

Quick answer: An esports API should return every match, player, team and odds line under one permanent ID, say exactly what each number measures, and tell you plainly when it has nothing to return. KashRock does this with a permanent kr_match_id, explicit market scope fields, and an available: false answer with a reason instead of an empty 200.

ProblemWhat breaksKashRock fieldResult
Three names, one matchJoins return zero rowskr_match_id, kr_tm_*, aliasesOne key for every join
Reversed team orderDuplicate matchesPermanent kr_match_idEither order resolves to one match
21.5 of what?Bets settled wrongmarket_type, scope, included_mapsScope is stated, not guessed
Empty 200 responsesRetries burn quotaavailable: false, reasonYou branch once, no retry loop
Stuck on upcomingFinished matches look livestatus, end_date, map scoresA reliable final state

What an esports API should give you

Most esports data problems are not missing data. They are data that does not line up: a fixture feed, an odds feed and a stats feed each describe the same match differently. A useful esports API removes that work. It should give you:

  • Fixtures and results for every game you build on, with final status you can trust. See the CS2 API.
  • Odds and lines from many books, compared on the same terms. See the Esports Odds API.
  • Player props from daily fantasy apps, tied to the same players as your stats. See the DFS Esports API.
  • History with open and close lines, for models. See the Historical Esports Data API.
  • One ID per match, team and player across all of it. See the Esports Data API.

The rest of this guide takes the five problems that break esports apps, one at a time. Each one has a real response from the KashRock API, captured on the day this guide was updated.

Problem 1: one match, three names

Team names are not stable. One source says Team Liquid, another says Liquid. A third uppercases it. If you join fixtures to odds on the team name, the join fails quietly and your app shows a match with no lines.

KashRock gives each team a permanent kr_tm_* ID and each match a permanent kr_match_id. The name a book or feed uses is kept as an alias next to the canonical name, so you can still show what a user expects to see:

GET /v6/esports/cs2/matches/id/kr_cs2_liquid-vs-wildcard-gaming-27-09-2026
{
  "kr_match_id": "kr_cs2_liquid-vs-wildcard-gaming-27-09-2026",
  "team1": "Team Liquid",
  "team1_alias": "Liquid",
  "team2": "Wildcard Gaming",
  "team2_alias": "Wildcard"
}

The canonical name is Team Liquid. The alias is Liquid. Both point at the same team, and your code joins on the ID, not the spelling. The full before-and-after is in Why Esports Data Breaks Your App.

Problem 2: reversed team order creates duplicate matches

One source lists a vs b. Another lists b vs a for the same fixture. Systems that key on the pair treat them as two matches, and you end up with duplicated rows, split odds and two sets of results.

A KashRock kr_match_id is permanent and does not change with which side a source calls home. This match was requested with its teams in both orders. Both requests returned the same ID:

GET /v6/esports/cs2/matches/reveal-vs-huskies-esport-30-09-2026/boxscore
{
  "kr_match_id": "kr_cs2_huskies-esport-vs-reveal-30-09-2026",
  "match_slug": "huskies-esport-vs-reveal-30-09-2026",
  "status": "finished",
  "start_date": "2026-09-30T16:20:00.000+00:00",
  "end_date": "2026-09-30T17:54:16.000+00:00",
  "team1": {
    "name": "Reveal",
    "id": "kr_tm_81428365c10b",
    "slug": "reveal",
    "score": 2
  },
  "team2": {
    "name": "Huskies eSport",
    "id": "kr_tm_c6d077861786",
    "slug": "huskies-esport",
    "score": 0
  },
  "maps": [
    {
      "map_number": 1,
      "map_name": "Inferno",
      "winner": "Reveal",
      "winner_score": 13,
      "loser_score": 9
    },
    {
      "map_number": 2,
      "map_name": "Dust2",
      "winner": "Reveal",
      "winner_score": 13,
      "loser_score": 4
    }
  ]
}

The path asked for reveal-vs-huskies-esport-30-09-2026. The response is the canonical huskies-esport-vs-reveal-30-09-2026. The reversed slug is resolved, not rejected, and the ID you store never changes. Here is the check in Python and JavaScript:

Python

import os
import requests

BASE = "https://kashrock.up.railway.app/v6/esports/cs2"
HEADERS = {"X-API-Key": os.environ["KASHROCK_API_KEY"]}


def match(ref: str) -> dict:
    r = requests.get(f"{BASE}/matches/{ref}/boxscore", headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()


a = match("reveal-vs-huskies-esport-30-09-2026")
b = match("huskies-esport-vs-reveal-30-09-2026")
assert a["kr_match_id"] == b["kr_match_id"]
print(a["kr_match_id"], a["status"], a["team1"]["score"], a["team2"]["score"])

JavaScript (Node 18+)

const BASE = "https://kashrock.up.railway.app/v6/esports/cs2"
const headers = { "X-API-Key": process.env.KASHROCK_API_KEY }

async function match(ref) {
  const r = await fetch(`${BASE}/matches/${ref}/boxscore`, { headers })
  if (!r.ok) throw new Error(`HTTP ${r.status}`)
  return r.json()
}

const a = await match("reveal-vs-huskies-esport-30-09-2026")
const b = await match("huskies-esport-vs-reveal-30-09-2026")
console.log(a.kr_match_id === b.kr_match_id, a.status, a.team1.score, a.team2.score)

Captured live on 2026-09-30 and trimmed for length. Fields are unchanged; unrelated fields are removed.

Problem 3: 21.5 could be maps, rounds or one map

A line of 21.5 tells you nothing on its own. It could be total rounds across a series, total kills on map 1, or something else. If your app guesses, it will settle some bets wrong, and you will not see it until a user complains.

KashRock labels what each line measures. In the history for one real match, the market total_maps carries two very different lines. A 2.5 is scoped to maps. A 21.5 under the same market name is scoped to rounds, and the response says why:

GET /v6/esports/cs2/matches/liquid-vs-wildcard-gaming-27-09-2026/lines/history
{
  "markets": [
    {
      "market": "total_maps",
      "market_type": "maps",
      "scope": "maps",
      "scope_basis": "line_within_series_map_range",
      "line": 2.5
    },
    {
      "market": "total_maps",
      "market_type": "maps",
      "scope": "rounds",
      "scope_basis": "line_outside_series_map_range",
      "line": 21.5
    }
  ]
}

scope_basis is the reason KashRock used to decide. A line of 2.5 sits inside the possible map range of a best-of-three, so it is maps. A line of 21.5 sits outside it, so it is rounds. Other markets carry market_scope (for example map_1 or series) and included_maps, and each book reports ot_included for overtime: true, false, or unknown when it cannot be proven. The Esports Odds API page covers how lines are compared on these fields, and Esports Odds API: Compare Lines Without Guessing Market Scope walks through them with real responses.

Problem 4: empty 200 responses burn retries

An endpoint that returns 200 with an empty list looks like success. Clients retry it, back off, retry again, and spend quota on an answer that will not change. A request that cannot be served should say so.

When KashRock has nothing to return, it says available: false and gives a reason. This is the real response for a match with no consensus line:

GET /v6/esports/cs2/lines?event_id=kr_cs2_meia-noite-vs-metanoia-wolves-30-09-2026
{
  "source": "kashrock",
  "available": false,
  "reason": "no_group_met_the_consensus_rules: a consensus needs two books on the same line, a proven scope and the same overtime class",
  "sport": "cs2",
  "total_events": 0,
  "events": [],
  "top_edges": []
}

The reason is specific: a consensus needs two books on the same line, a proven scope and the same overtime class. History does the same. A match with no stored quotes returns this instead of an empty array:

GET /v6/esports/cs2/matches/leo-vs-phantom-academy-30-09-2026/lines/history
{
  "source": "kashrock",
  "available": false,
  "status": "empty",
  "reason": "no_quotes_stored: no match_winner quotes are stored for this match",
  "bo_label": "BO3",
  "kr_match_id": "kr_cs2_leo-vs-phantom-academy-30-09-2026",
  "markets": []
}

Branch on it once and stop retrying. This helper returns the reason instead of an empty list:

Python (reuses BASE and HEADERS from above)

def lines(event_id: str):
    r = requests.get(f"{BASE}/lines", params={"event_id": event_id}, headers=HEADERS, timeout=10)
    r.raise_for_status()
    body = r.json()
    if body.get("available") is False:
        return None, body["reason"]  # an honest "no", not an empty list
    return body["events"], None

Problem 5: scores stuck on upcoming

Apps that poll a schedule often show a finished match as upcoming for hours, because the feed they read never flipped its status. The match response in Problem 2 shows what a settled match looks like in KashRock: a finished status, an end_date, the series score for each team and the result of every map.

Use the match boxscore route for final scores. That is the route the examples above call, and it is the one the Python and JavaScript snippets in this guide read.

Closing lines, not just snapshots

Models need to know where a line closed, not only where it is now. Every market in history carries an open and a close per book, and each one has an observed_at timestamp so you can see when the quote was taken. This is a real series winner line for one match. The home side moved from -260 to -350 between open and close:

GET /v6/esports/cs2/matches/liquid-vs-wildcard-gaming-27-09-2026/lines/history
{
  "market": "match_winner",
  "market_type": "winner",
  "market_scope": "series",
  "books": [
    {
      "ot_included": "unknown",
      "open": {
        "observed_at": "2026-09-27T06:25:22Z",
        "sides": [
          {
            "side": "away",
            "american": 190
          },
          {
            "side": "home",
            "american": -260
          }
        ]
      },
      "close": {
        "observed_at": "2026-09-27T22:45:00Z",
        "sides": [
          {
            "side": "away",
            "american": 250
          },
          {
            "side": "home",
            "american": -350
          }
        ]
      }
    }
  ]
}

A model trained on the open would have priced this match very differently from one trained on the close. The Line Gaps API uses the same history to show where books disagree.

How to evaluate any esports API

Before you commit to an esports API, test it on these five questions with a real match:

  • Request the same match with the teams in both orders. Do you get one ID back?
  • Look up a team by two spellings. Does it resolve to one team?
  • Pull a line. Does the response say what it measures and whether overtime counts?
  • Ask for something it does not have. Do you get an explicit answer, or an empty 200?
  • Fetch a match that finished an hour ago. Is the status final?

To run these yourself, start with the Quickstart. The Pricing page shows which plan unlocks lines and history.

FAQ

What is an esports API?

An esports API is a developer interface that returns esports data such as fixtures, results, player stats, odds and props. A good one returns it under stable IDs so data from different sources joins correctly.

What is a kr_match_id?

A kr_match_id is KashRock's permanent ID for a match. It follows the format kr_{sport}_{team-a}-vs-{team-b}-DD-MM-YYYY and stays the same when a source lists the teams in the opposite order.

How does KashRock handle reversed team order?

It resolves the reversed spelling to the same kr_match_id. In the example above, requesting reveal-vs-huskies returned the canonical huskies-esport-vs-reveal match.

How do I know whether a line is maps or rounds?

Read scope, scope_basis, market_scope and included_maps on the market. The response states what the number measures. Each book also reports whether overtime is included, or unknown.

What does available: false mean?

It means the request was valid but KashRock has nothing to return, and reason says why. Treat it as a final answer rather than retrying.

Written by

Javieon, Founder of KashRock

Javieon founded KashRock, the esports data API that gives every match, player, team and prop one permanent ID.