Rocket League does not hand you an official stats API. Epic Games and developer Psyonix have never shipped a public endpoint that returns your current rank or MMR on demand, so every “rank tracker” site you have used is built on a workaround. Some scrape web pages. Some rely on data that quietly went dark years ago. This tutorial shows you how to build your own tracker the honest way: on top of ballchasing.com’s real, documented replay API, combined with a simple manual rank log you update after each session. By the end you will have a working Flask dashboard on your own machine that charts your performance trend and rank progress over a season, with full control over your data.
This project targets Season 23, which started on June 10, 2026 alongside the v2.70 update. The ranked ladder itself has not changed shape in 2025 or 2026: it is still seven rank families from Bronze to Grand Champion, each split into three tiers and four divisions, topped by the open-ended Supersonic Legend rank. We will use that structure to build a rank-logging schema, then wire it up to real match data pulled from your own replays.
Most existing rank-tracker sites solve the missing-API problem by scraping a web page and hoping the markup does not change next patch, or by pointing at an old endpoint that was never meant to stay public forever. Both approaches break silently, usually right when you care most, mid-season, with a promotion on the line. Building your own tracker on a documented API plus a five-second manual log removes that fragility. You own the database file, you control the sync schedule, and nothing about this setup depends on a scraper surviving a front-end redesign.
This guide assumes basic comfort with Python syntax and a terminal, but not much beyond that. If you have never written a Flask route or an SQL insert statement before, you will still be able to follow along, since every script below is short enough to type from scratch and understand line by line. If you get stuck at any point, jump straight to the troubleshooting table further down. It covers the exact errors most people hit on their first pass through this build.
Prerequisites: Tools, Accounts, and Versions
You do not need to be a professional developer to finish this build, but you should be comfortable running commands in a terminal. Here is everything to have ready before Step 1.
- Python 3.11 or newer, installed and available on your PATH
- pip, which ships with modern Python installs
- A free ballchasing.com account (sign in with Steam, Epic, or Xbox Live)
- A code editor such as VS Code (any recent build works fine)
- Your Steam64 ID, Epic account ID, or PSN/Xbox identifier, depending on platform
- Rocket League installed with in-game replays saved after each match (default setting on PC)
- Roughly 90 minutes for the full build, including testing
Version numbers matter more than they might seem here. Python 3.11 introduced faster exception handling and better error messages that make debugging the requests in Step 5 noticeably easier, and every package in this tutorial supports it without extra configuration. If you already have an older Python 3 install, upgrading first will save you time later, since some of the syntax used below, including f-strings with nested braces, behaves inconsistently on very old 3.x releases.
We will lean on three Python packages: Flask for the dashboard, requests for HTTP calls, and python-dotenv to keep your API key out of source control. All three are actively maintained open-source projects, and you can read the current Flask documentation or the requests documentation if you want details beyond what this guide covers. Storage runs on SQLite, which ships inside Python’s standard library, so there is nothing extra to install there. If you want to inspect how Python’s sqlite3 module works under the hood, the standard library docs cover every method used below.
Rocket League’s 2026 Rank System at a Glance
Before writing a line of code, it helps to lock down exactly what you are tracking. The table below lays out every rank family and how many division steps sit inside it. This is the structure our rank_snapshots table will mirror later in the tutorial.
| Rank Family | Tiers | Divisions per Tier | Total Steps |
|---|---|---|---|
| Unranked | 1 | 0 | 1 |
| Bronze | 3 | 4 | 12 |
| Silver | 3 | 4 | 12 |
| Gold | 3 | 4 | 12 |
| Platinum | 3 | 4 | 12 |
| Diamond | 3 | 4 | 12 |
| Champion | 3 | 4 | 12 |
| Grand Champion | 3 | 4 | 12 |
| Supersonic Legend | 1 | 0 (open-ended) | 1 |
Seven scored families, each with three tiers and four divisions, works out to 84 division steps between Bronze I Division I and Grand Champion III Division IV, with Supersonic Legend sitting above all of them as a single open tier. Epic’s own rank breakdown confirms this layout, and nothing in the 2025-2026 patch notes suggests a change is coming. Season resets still follow the familiar soft-reset pattern: your MMR gets pulled back toward the middle of the distribution and you play 10 placement matches per playlist to re-establish a rank. None of that requires an API to observe, which is exactly why the manual rank log in Step 6 works so well as a stopgap for the data Epic does not expose.
Each playlist tracks its own rank independently, so a player can sit in Champion in 2v2 while still climbing out of Diamond in 1v1. That is why the schema in Step 4 stores a playlist column on every snapshot instead of one global rank value. If you only care about a single queue, say ranked-doubles, you can safely ignore the rest, but building the schema to hold every playlist from day one means you never have to run a painful migration later if you decide to start tracking 3v3 or Hoops as well.
Step 1: Get a ballchasing.com API Key
ballchasing.com is the closest thing Rocket League has to a real public data source. It is a community-run replay repository with a documented, token-based API, and it is where this whole project gets its match data. Sign in at ballchasing.com with the same platform account you use in-game, then open your profile settings and generate an API token. Keep that token private. It authenticates every request you make and ties usage back to your account’s rate limit.
Rate limits scale with your Patreon tier on the site, and they matter for how often you can sync. The free tier is enough for a personal tracker that syncs a few times a day.
| Account Tier | Weekly Limit | Daily Limit |
|---|---|---|
| Free | 70 requests | 20 requests |
| Gold Patron | 350 requests | Not separately capped |
| Diamond Patron | 1,050 requests | Not separately capped |
| Champion Patron | 2,800 requests | Not separately capped |
| Grand Champion Patron | 7,000 requests | Not separately capped |
| Legend Patron | 21,000 requests | Not separately capped |
The full parameter list, including every filterable playlist enum, lives in the official ballchasing.com API documentation. Bookmark that page. You will reference it again once you start filtering replays by playlist or season. The playlist values you will use most for a ranked tracker are:
- ranked-duels, ranked-doubles, ranked-solo-standard, ranked-standard (the four core 1s/2s/3s ranked queues)
- ranked-hoops, ranked-rumble, ranked-dropshot, ranked-snowday (ranked extra modes)
- unranked-duels, unranked-doubles, unranked-standard, unranked-chaos (casual equivalents, useful if you also want to log casual trends)
- tournament, private, season, offline (non-ladder match types worth excluding from your win-rate queries)
Pass any of these to the playlist parameter in list_replays() from Step 5 to pull a single queue instead of your full replay history.
Step 2: Set Up the Python Project
Create a project folder and a virtual environment so this tracker’s dependencies stay isolated from anything else on your machine.
mkdir rl-tracker
cd rl-tracker
python3 -m venv venv
source venv/bin/activate # on Windows: venv\Scripts\activate
mkdir templates
Next, save your ballchasing API key and Steam ID to a local .env file so they never end up hardcoded in a script you might accidentally commit to a public repo.
BALLCHASING_API_KEY=your-token-here
STEAM_ID=76561198000000000
Add a .gitignore entry for .env and venv/ right away, before you forget. That single habit prevents the single most common leak in personal API projects: an access token sitting in plain text inside a public GitHub repo.
Here is the complete file layout you are building toward. Nothing here is optional scaffolding, every file gets created and filled in across the remaining steps.
rl-tracker/
├── venv/
├── templates/
│ └── dashboard.html
├── .env
├── .gitignore
├── requirements.txt
├── schema.sql
├── rl_tracker.db
├── ballchasing_client.py
├── fetch_and_store.py
├── log_rank.py
└── app.py
Eleven files and one folder make up the entire working project. If your directory looks different after Step 9, go back and check which step you skipped before moving on, since later scripts import from ballchasing_client.py and read from rl_tracker.db directly.
Step 3: Install Dependencies
With the virtual environment active, install the three packages this project needs.
pip install flask requests python-dotenv
pip freeze > requirements.txt
Freezing the exact installed versions into requirements.txt now means you (or anyone else) can recreate this environment later with a single pip install -r requirements.txt, even after Flask or requests ship a new release.
Step 4: Design the SQLite Schema
Two tables cover everything this tracker needs to store: one for match-level stats pulled from replays, and one for the rank snapshots you log by hand after checking your in-game rank screen. Save this as schema.sql.
CREATE TABLE IF NOT EXISTS replays (
replay_id TEXT PRIMARY KEY,
playlist TEXT NOT NULL,
match_date TEXT NOT NULL,
duration INTEGER,
team_score INTEGER,
opponent_score INTEGER,
result TEXT,
goals INTEGER,
assists INTEGER,
saves INTEGER,
shots INTEGER,
mvp INTEGER DEFAULT 0,
boost_bpm REAL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS rank_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
playlist TEXT NOT NULL,
rank_family TEXT NOT NULL,
tier INTEGER,
division INTEGER,
logged_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Load it into a fresh database file with the sqlite3 command-line tool, which ships with every standard Python install.
sqlite3 rl_tracker.db < schema.sql
Step 5: Build the ballchasing API Client
Now write a small wrapper around the two ballchasing endpoints this project actually needs: listing replays for a player, and pulling the full detail for a single replay. Save this as ballchasing_client.py.
import os
import requests
BASE_URL = "https://ballchasing.com/api"
class BallchasingClient:
def __init__(self, api_key=None):
self.api_key = api_key or os.environ["BALLCHASING_API_KEY"]
self.session = requests.Session()
self.session.headers.update({"Authorization": self.api_key})
def list_replays(self, player_id, playlist=None, count=25):
params = {"player-id": player_id, "count": count}
if playlist:
params["playlist"] = playlist
resp = self.session.get(f"{BASE_URL}/replays", params=params, timeout=15)
resp.raise_for_status()
return resp.json()
def get_replay(self, replay_id):
resp = self.session.get(f"{BASE_URL}/replays/{replay_id}", timeout=15)
resp.raise_for_status()
return resp.json()
The Authorization header takes your raw token, no “Bearer” prefix required, which trips up a surprising number of first-time users of this API. We will hit that exact mistake again in the troubleshooting section below.
Step 6: Fetch and Store Replay Stats
This is the script that actually populates your database. It lists your recent replays, skips any it has already stored, pulls full detail on the new ones, and writes the result to SQLite. Save it as fetch_and_store.py in the project root.
import os
import sqlite3
import time
from dotenv import load_dotenv
from ballchasing_client import BallchasingClient
load_dotenv()
DB_PATH = "rl_tracker.db"
STEAM_ID = os.environ["STEAM_ID"]
PLAYER_FILTER = f"steam:{STEAM_ID}"
def extract_me(replay):
for team in ("blue", "orange"):
for player in replay.get(team, {}).get("players", []):
if player.get("id", {}).get("id") == STEAM_ID:
return team, player
return None, None
def sync(limit=25):
client = BallchasingClient()
conn = sqlite3.connect(DB_PATH)
listing = client.list_replays(PLAYER_FILTER, count=limit)
for item in listing.get("list", []):
replay_id = item["id"]
already_have = conn.execute(
"SELECT 1 FROM replays WHERE replay_id = ?", (replay_id,)
).fetchone()
if already_have:
continue
replay = client.get_replay(replay_id)
team, me = extract_me(replay)
if not me:
continue
core = me.get("stats", {}).get("core", {})
boost = me.get("stats", {}).get("boost", {})
blue_goals = replay.get("blue", {}).get("stats", {}).get("core", {}).get("goals", 0)
orange_goals = replay.get("orange", {}).get("stats", {}).get("core", {}).get("goals", 0)
won = (team == "blue" and blue_goals > orange_goals) or (team == "orange" and orange_goals > blue_goals)
conn.execute(
"""INSERT INTO replays (replay_id, playlist, match_date, duration,
team_score, opponent_score, result, goals, assists, saves, shots, mvp, boost_bpm)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(replay_id, replay.get("playlist_id"), replay.get("date"), replay.get("duration"),
blue_goals if team == "blue" else orange_goals,
orange_goals if team == "blue" else blue_goals,
"win" if won else "loss",
core.get("goals", 0), core.get("assists", 0), core.get("saves", 0),
core.get("shots", 0), int(core.get("mvp", False)), boost.get("bpm", 0)),
)
conn.commit()
time.sleep(1.2)
conn.close()
if __name__ == "__main__":
sync()
Run it with python fetch_and_store.py. On a fresh database, this pulls your last 25 replays and stores them one by one. The 1.2-second pause between detail requests is a courtesy to a free API run by volunteers, and it also keeps you comfortably under the 20-requests-per-day free tier limit if you cap your sync batch size appropriately.
It helps to know what a healthy response actually looks like before you start debugging a real one. A trimmed list_replays() response for a single match looks roughly like this.
{
"list": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"playlist_id": "ranked-doubles",
"date": "2026-08-14T21:03:12Z",
"duration": 331,
"blue": {"stats": {"core": {"goals": 3}}},
"orange": {"stats": {"core": {"goals": 4}}}
}
],
"count": 1
}
If you print that raw dictionary the first time you run sync() and it looks nothing like this, the bug is almost certainly in Step 1 or Step 5, not in the database code. Confirm the shape of your real response before chasing problems further downstream.
Step 7: Log Manual Rank Snapshots
Here is the part that separates an honest tracker from a broken promise. Since neither Epic nor ballchasing.com exposes your rank or MMR through any public endpoint, you log it yourself, in about five seconds, after checking your in-game rank screen. Save this as log_rank.py.
import sqlite3
import sys
from datetime import datetime
DB_PATH = "rl_tracker.db"
FAMILIES = ["Bronze", "Silver", "Gold", "Platinum", "Diamond",
"Champion", "Grand Champion", "Supersonic Legend"]
def log_rank(playlist, family, tier=None, division=None):
if family not in FAMILIES:
raise ValueError(f"Unknown rank family: {family}")
conn = sqlite3.connect(DB_PATH)
conn.execute(
"INSERT INTO rank_snapshots (playlist, rank_family, tier, division, logged_at) VALUES (?, ?, ?, ?, ?)",
(playlist, family, tier, division, datetime.utcnow().isoformat()),
)
conn.commit()
conn.close()
print(f"Logged {family} {tier or ''} Div {division or ''} for {playlist}")
if __name__ == "__main__":
playlist, family = sys.argv[1], sys.argv[2]
tier = int(sys.argv[3]) if len(sys.argv) > 3 else None
division = int(sys.argv[4]) if len(sys.argv) > 4 else None
log_rank(playlist, family, tier, division)
Run it from the terminal with the playlist, family, tier, and division you see in-game right now.
python log_rank.py ranked-doubles Diamond 2 3
Output example:
Logged Diamond 2 Div 3 for ranked-doubles
Do this once a week, or right after any promotion or demotion, and you will build a timeline that shows exactly when your rank moved relative to your match performance.
Step 8: Correlate Rank Changes With Performance
With both tables populated, a single SQL query can answer the question every ranked grinder actually wants answered: was I actually playing better around the time I ranked up, or did I get carried? This query pulls your win rate and average core stats in the window between two rank snapshots.
SELECT
COUNT(*) AS matches_played,
ROUND(100.0 * SUM(CASE WHEN result = 'win' THEN 1 ELSE 0 END) / COUNT(*), 1) AS win_rate,
ROUND(AVG(goals), 2) AS avg_goals,
ROUND(AVG(saves), 2) AS avg_saves,
ROUND(AVG(boost_bpm), 1) AS avg_boost_bpm
FROM replays
WHERE playlist = 'ranked-doubles'
AND match_date BETWEEN '2026-08-01' AND '2026-08-15';
Run that against sqlite3 rl_tracker.db directly and you get a single row back, something like this.
matches_played win_rate avg_goals avg_saves avg_boost_bpm
14 64.3 1.86 2.71 312.4
Swap the date range for the window between any two of your logged rank snapshots and you get a performance summary for that exact stretch. That is a far more useful signal than any single-number MMR estimate, because it shows you which stat actually moved the needle. Maybe it is more saves, maybe fewer wasted boost pads, maybe a higher shot count. Whatever the data says, you now have it in front of you instead of a guess.
Step 9: Build the Flask Dashboard
Numbers in a SQLite file are useful, but a browser dashboard is what makes you actually check this thing daily. Create app.py in the project root.
from flask import Flask, render_template
import sqlite3
app = Flask(__name__)
DB_PATH = "rl_tracker.db"
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
@app.route("/")
def dashboard():
conn = get_conn()
matches = conn.execute(
"SELECT * FROM replays ORDER BY match_date DESC LIMIT 50"
).fetchall()
ranks = conn.execute(
"SELECT * FROM rank_snapshots ORDER BY logged_at ASC"
).fetchall()
conn.close()
wins = sum(1 for m in matches if m["result"] == "win")
win_rate = round(100 * wins / len(matches), 1) if matches else 0
return render_template("dashboard.html", matches=matches, ranks=ranks, win_rate=win_rate)
if __name__ == "__main__":
app.run(debug=True, port=5000)
Start it with python app.py, then open http://127.0.0.1:5000 in your browser. Flask’s built-in dev server reloads automatically whenever you save a change to app.py, which makes the next step, styling the template, fast to iterate on.
Step 10: Add Chart.js Visualizations
Create templates/dashboard.html and load Chart.js from a CDN so you get a real line chart without installing a JavaScript build pipeline. This template plots win rate as a rolling trend and lists your logged rank history alongside it.
<!DOCTYPE html>
<html>
<head>
<title>Rocket League Tracker</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<h1>Win rate: {{ win_rate }}%</h1>
<canvas id="goalsChart" width="700" height="300"></canvas>
<h2>Rank history</h2>
<ul>
{% for r in ranks %}
<li>{{ r["logged_at"] }} — {{ r["rank_family"] }} {{ r["tier"] }} Div {{ r["division"] }} ({{ r["playlist"] }})</li>
{% endfor %}
</ul>
<script>
const labels = [{% for m in matches %}"{{ m['match_date'] }}",{% endfor %}];
const goals = [{% for m in matches %}{{ m['goals'] }},{% endfor %}];
new Chart(document.getElementById('goalsChart'), {
type: 'line',
data: { labels: labels.reverse(), datasets: [{ label: 'Goals per match', data: goals.reverse() }] }
});
</script>
</body>
</html>
Refresh the page and you should see a live line chart of your recent goal-scoring trend, plus a plain-text log of every rank snapshot you have entered so far. From here you can extend the chart with saves, boost efficiency, or a second line comparing two playlists side by side.
Step 11: Automate Syncs and Back Up Your Data
A tracker you have to remember to run manually stops being useful within a week. On macOS or Linux, add a cron entry that runs the sync script every 30 minutes while you are likely to be playing.
crontab -e
*/30 18-23 * * * cd /home/you/rl-tracker && /home/you/rl-tracker/venv/bin/python fetch_and_store.py >> sync.log 2>&1
On Windows, Task Scheduler does the same job with a basic trigger pointed at your venv’s python.exe and the script path. Either way, back up rl_tracker.db somewhere outside the project folder on a regular basis. It is a single file, so copying it to a cloud drive folder takes one line.
cp rl_tracker.db ~/Dropbox/backups/rl_tracker_$(date +%F).db
That single command, run nightly through the same cron job, protects months of match history from a corrupted disk or an accidental delete.
Testing Your Setup End-to-End
Before trusting this tracker with a full season of data, run through a short manual check. Play one ranked match, save the replay, and confirm it appears in your ballchasing.com profile within a minute or two of the match ending. Then run python fetch_and_store.py and watch the terminal for errors instead of just checking that it exits cleanly. Open rl_tracker.db with the sqlite3 command line and run SELECT COUNT(*) FROM replays to confirm the row count went up by exactly one.
Next, run log_rank.py once with your current rank, then start app.py and load the dashboard in your browser. You should see that one match reflected in the win rate calculation and that one rank entry listed under Rank history. If both show up correctly, the full pipeline works end to end, from replay upload through the browser chart, and you can trust it to run unattended from here.
Common Pitfalls to Avoid
- Forgetting the Authorization header format. ballchasing.com wants the raw token with no “Bearer ” prefix. Adding one returns a 401 every time.
- Assuming private replays are visible. If your in-game replay privacy is set to “private” instead of “public” or “unlisted” in ballchasing, the API will not return them to your listing calls even though you own the replay.
- Skipping the rate-limit math. On the free tier, 20 detail requests a day sounds like plenty until you play a long session and try to backfill 40 matches at once. Batch your syncs.
- Hardcoding your Steam ID as a player-id filter without the platform prefix. The API expects steam:76561198000000000, not the bare numeric ID, and a missing prefix returns an empty list rather than an error.
- Treating your rank_snapshots log as optional. Skip logging for two weeks and your correlation queries in Step 8 lose their reference points, since there is no API to backfill that history for you.
- Running the Flask dev server in production mode. app.run(debug=True) is fine for your own laptop, but never expose that server directly to the internet. Use a proper WSGI server if you ever want remote access.
Troubleshooting Guide
Even a small project like this hits friction. Here are the issues you are most likely to run into, and what actually fixes them.
| Symptom | Likely Cause | Fix |
|---|---|---|
| 401 Unauthorized on every request | Missing or malformed Authorization header | Send the raw token, no “Bearer” prefix, and confirm .env loaded correctly |
| Empty “list” array from /replays | Wrong player-id prefix or replay set to private | Use platform:id format and switch replay visibility to public or unlisted |
| 429 Too Many Requests | Exceeded your daily or weekly ballchasing quota | Reduce sync frequency or batch size, then wait for the quota window to reset |
| sqlite3.OperationalError: no such table | schema.sql was never loaded into the database file | Re-run sqlite3 rl_tracker.db < schema.sql |
| KeyError: ‘STEAM_ID’ | .env file missing or python-dotenv not loaded | Confirm load_dotenv() runs before os.environ is read |
| Dashboard shows 0% win rate with real matches present | extract_me() never matched your player ID inside the replay | Print the raw player IDs from one replay and confirm the format matches STEAM_ID exactly |
| Chart.js canvas stays blank | Jinja loop produced malformed JavaScript array syntax | View page source and check for a trailing comma or missing quote around match_date |
| Cron job never runs | Wrong path to the virtual environment’s Python binary | Use the absolute path from venv/bin/python, not just “python” |
Advanced Tips for Power Users
Once the base tracker works, a few extensions make it genuinely competitive with a hosted service. First, add a Discord webhook call at the end of fetch_and_store.py so a new synced match posts a one-line summary straight into your team’s server. Second, export any query result to CSV with Python’s built-in csv module so you can drop your season stats into a spreadsheet for deeper analysis outside SQLite. Third, if you play on multiple accounts or platforms, add a platform column to the replays table and a second STEAM_ID-style variable per account, then run separate sync jobs that tag each row accordingly.
If you want richer per-shot and positional data than the ballchasing summary stats expose, the open-source carball replay parser can decode a raw .replay file directly, giving you frame-by-frame positioning data for advanced analysis like heatmaps or rotation tracking. That is a significantly heavier lift than anything in this tutorial, but it is the natural next step once the basic tracker feels too simple.
One more upgrade worth making early: add exponential backoff around your ballchasing calls instead of a fixed 1.2-second sleep. A fixed delay works fine for a solo tracker, but a backoff loop protects you the moment you forget you left a second sync job running, or the API has a slow afternoon.
import time
def get_with_backoff(client, replay_id, max_attempts=5):
for attempt in range(max_attempts):
try:
return client.get_replay(replay_id)
except Exception:
wait = 2 ** attempt
time.sleep(wait)
raise RuntimeError(f"Gave up on replay {replay_id} after {max_attempts} attempts")
Swap the plain client.get_replay(replay_id) call inside fetch_and_store.py for get_with_backoff(client, replay_id) and a temporary network hiccup or a 429 no longer kills the whole sync run.
It is also worth building the same pattern for other games once you are comfortable with it. The rank-snapshot-plus-match-history approach used here is close to what we used in our Overwatch 2 Rank Tracker and Fortnite Rank Tracker tutorials, and the same Flask-and-SQLite skeleton carries over almost unchanged to our Marvel Rivals Rank Tracker build. If Rocket League’s ranked ladder was not competitive enough for you, our FACEIT Level Checker walks through a similar API-key-plus-database pattern for CS2, and our Deadlock Tier List Tracker shows how to adapt the same stack for hero-tier data instead of rank data. For more esports tooling projects like these, browse the full esports section.
Player numbers back up why this kind of self-hosted tool is worth the build time. Rocket League reportedly hit a peak of roughly 1,086,329 concurrent players across all platforms in January 2026, up from about 797,000 the previous December, and the game’s Steam-only player base alone has held a 30-day average around 16,444 concurrent players through the middle of 2026. A game with that much active competitive volume produces a steady stream of your own match data every week, which is exactly the fuel this tracker needs to stay useful.
Why There Is No Official Rank API (And Why That Is Unlikely to Change)
It is worth understanding why this workaround exists in the first place. Older documentation floating around GitHub still references a Psyonix contact address, [email protected], for developers requesting API access, which points to a legacy, permission-gated system rather than anything open by default. A separate legacy endpoint at api.tracker.gg’s Rocket League path reportedly stopped functioning after April 2023, which is part of why several older community wrapper libraries no longer work out of the box. Tracker Network’s own consumer site and app remain popular, but they are not a documented public API you can build against reliably, which is exactly why this tutorial leans on ballchasing.com instead. ballchasing.com earns that trust by publishing its full endpoint list, its parameters, and its rate limits in the open, which is the bar any data source needs to clear before you build a personal tool on top of it.
None of this means Psyonix is hostile to third-party tools. The company behind Rocket League, Psyonix, has tolerated a large ecosystem of community trackers and replay sites for years without shutting them down, it has simply never turned that tolerance into a stable, versioned public API contract. Until that changes, a self-hosted tracker built on ballchasing.com plus a manual rank log is the most durable approach available, because it does not depend on a single company’s undocumented internal API surviving the next patch.
That durability matters more the longer a game stays alive. Rocket League is well past its tenth year of live seasons at this point, and a tool built on a scraped endpoint tends to need a rewrite every time the underlying site redesigns its player pages. A tool built on a documented API plus your own database only needs a rewrite when ballchasing.com changes its contract, which the maintainers publish in advance on the same documentation page you bookmarked in Step 1. That is a far smaller surface area to babysit over a multi-year season cycle.
Frequently Asked Questions
Does Epic Games offer an official Rocket League stats API?
No. There is no current public, documented API from Epic or Psyonix that returns live rank, MMR, or match data. The rank tiers themselves are documented in Epic’s help center, but access to your live stats is not.
Is the ballchasing.com API free to use?
Yes, the free tier allows 70 requests per week and 20 per day, which is enough for a personal tracker syncing a handful of times daily. Higher volume requires a paid Patreon tier on the site.
Can I track my Rocket League rank without uploading replays?
You can log rank manually with the log_rank.py script from Step 7 without ever touching replays, but you lose the match-performance correlation that makes the tracker genuinely useful. Replays need to be saved locally and set to public or unlisted visibility on ballchasing for the API to see them.
What is a Steam64 ID and where do I find mine?
It is the long numeric identifier Steam assigns to every account, typically starting with 7656119. You can find yours through your Steam profile URL settings or a Steam ID lookup tool, then use it in the platform:id format the ballchasing API expects.
Why doesn’t ballchasing.com show my exact rank or MMR?
Because Epic does not expose that data publicly, ballchasing.com cannot surface it either. The service reports match stats like goals, saves, and boost usage, which is genuine replay data, but rank and MMR are not part of that dataset.
How often should I sync new replays?
Every 30 minutes during active play sessions works well with the cron setup from Step 11, and it comfortably stays under the free-tier rate limit for a typical multi-hour session.
Can this tracker work for Xbox or PlayStation accounts?
Yes. Swap the platform prefix in your player-id filter from steam: to xbl: or psn: depending on your platform, and use the identifier format ballchasing documents for that platform.
How do I back up my tracker data?
rl_tracker.db is a single SQLite file, so copying it to a cloud storage folder on a schedule, as shown in Step 11, is enough. There is no separate database server to manage or export.
Will this tracker stop working if Epic changes something?
It depends on which layer changes. ballchasing.com’s API is independently maintained and has stayed stable for years, so a Rocket League patch on its own should not break your sync. If Epic ever restricts what replay data players can upload to third-party sites, that would affect ballchasing.com and every tool built on it, including this one, at the same time.
Do I need to keep my computer running for this to work?
Only while a cron job or scheduled task actually needs to run, which is a matter of seconds per sync. You can turn your machine off between sessions and pick up exactly where fetch_and_store.py left off next time you play, since the script skips any replay ID it has already stored.




