Read your ranked match log
Forts keeps a running record of your ranked matches on your own machine. Upload it and this turns it into a table: every match, the map, who won, what your score did, and how much damage each side dealt.
Look up a player
Enter a Steam ID or a player name to open that player's ranked matches.
What it shows you
- Every ranked match in the file, with its date, map, release and which side you played.
- Your score after each match and what it changed by, your win/loss record, and your opponent's score going in.
- Damage dealt and taken per match, forfeits and technical problems, and the ping the match was accepted at.
- How much of the file was read, as two separate figures: how much of it turned into readable text, and how much of that text is log entries.
The game writes it as log2.dat for ranked matches or log3.dat for other online matches, in its own folder under the Steam install — steamapps/common/Forts/users/<your account>/ — beside your replays folder.
API reference
Decode a Forts telemetry log to JSON. Server-to-server; the browser-facing surface is the form above.
| Endpoint | POST https://forts-tools.com/api/telemetry |
|---|---|
| Request body | multipart/form-data |
| Response body | application/json |
| Authentication | None. No key, no origin allow-list. |
| Browser access | No CORS headers are sent. fetch() from a page is blocked by the browser before the body is readable. Call this endpoint server-side. |
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
logfile | multipart body | file | required | The game's telemetry log — the same file the form above takes. Ranked and network logs are both accepted; kind says which one was read. |
samples | query string | string | optional | Include the per-match time series. On: 1, true, yes, on. Off (default): 0, false, no, off. Any other value: 400. The series is one point every few seconds of play; over a full season it is larger than the rest of the response. |
Example request
curl -X POST -F "logfile=@your-log.dat" \
https://forts-tools.com/api/telemetry
Response
Field names, type names and token values are fixed. They are not translated.
Every field has one type or null. null means the file did not record the value: never words standing in for a value, never 0 for a figure that was never written. An empty array means the file records none, not that the value is missing.
schema is forts-telemetry/1. It changes only on a breaking change; a new field never changes it. Any other value is outside this contract.
| Field | Type | Description |
|---|---|---|
ok | boolean | true on success, false on failure. |
schema | string | Contract version. Currently forts-telemetry/1. |
releases | string[] | Game releases the file recorded, sorted and deduplicated. Empty when it recorded none. |
summary | object | Whole-file figures. Fields below. |
matches | object[] | One object per resolved match, oldest first. Fields below. |
matches_total | integer | Matches resolved in the file. On this endpoint it equals the length of matches. |
rejections | object[] | Lines the reader could not resolve, grouped by message. Fields below. |
samples_requested | boolean | Whether ?samples=1 asked for the series. |
samples | object | The per-match time series. Fields below. Present but empty unless requested. |
downloads | object[] | Files generated from the upload. The full decoded text is here, not in the body. Fields below. |
expires_in_sec | integer | Seconds the downloads URLs stay fetchable. |
summary | ||
kind | string | "ranked" or "network". |
lines_total | integer | Lines in the decoded file. |
lines_understood | integer | Lines the reader resolved. |
understood_pct | integer | lines_understood as a percentage of lines_total, rounded. |
readable_pct | number | Share of the file that decoded to text, as a percentage. |
samples_total | integer | Sample points across all matches. |
matches_with_series | integer | Matches that carry a sample series. |
unresolved | integer | Match blocks the reader could not close. |
counters | object | Event tallies. Keys: launches, forfeits, technical, lobbies, timeouts, uploads. An empty object means nothing was counted. |
low_confidence | boolean | The reading is incomplete: readable_pct below threshold, or part of the file did not decode to text. |
first_seen | string | null | Earliest date in the file, YYYY-MM-DD. |
last_seen | string | null | Latest date in the file, YYYY-MM-DD. |
season_first | integer | null | Lowest season number in the file. |
season_last | integer | null | Highest season number in the file. |
wins | integer | null | Wins in the season-to-date record, as of the most recent match in the file. The counter resets each season, so this is not a career total and does not add up to matches_total. |
losses | integer | null | Losses in the same season-to-date record, on the same terms as wins. |
score_first | integer | null | First leaderboard score in the file. |
score_last | integer | null | Last leaderboard score in the file. |
score_best | integer | null | Highest leaderboard score in the file. |
matches[] | ||
date | string | Match date, YYYY-MM-DD. |
time | string | Match start time, HH:MM:SS. |
season | integer | Ranked season number. |
samples | integer | Sample points recorded for this match. 0 means none. |
notes | string[] | Flags for this match. Values: won_by_forfeit, lost_by_forfeit, you_forfeited, technical_issue, winner_disputed, pre_game_winner, score_disagrees. [] means none. |
map_name | string | null | Map the match was played on. |
release | string | null | Game release the file recorded for this match. |
side | string | null | The uploader's team as the file writes it: "Team 1" or "Team 2". Not the "left", "right" tokens used by the replay endpoint. |
result | string | null | "won" or "lost", from the uploader's side. |
score_after | integer | null | Leaderboard score after the match. |
change | string | null | Score change, signed, e.g. "+14". |
opponent_score | integer | null | Opponent's leaderboard score. |
record | string | null | Season-to-date win/loss record after this match, e.g. "44W 9L". Resets each season. |
damage_dealt | string | null | Damage dealt, two decimals. |
damage_taken | string | null | Damage taken, two decimals. |
duration | string | null | Length of play, mm:ss. Taken from the samples, so it excludes lobby and loading. |
ping_ms | integer | null | Matchmaking ping, in milliseconds. |
stake_win | string | null | Points that a win would have gained, signed. |
stake_lose | string | null | Points that a loss would have cost, signed. |
rejections[] | ||
message | string | The line as the file wrote it, cleaned. This is the uploader's own text and is not rewritten. |
count | integer | How many lines carried it. |
samples | ||
columns | string[] | Column names for every row, in order: seconds, damage_dealt, damage_taken. |
matches | object[] | One object per match that carries a series. Fields below. |
samples.matches[] | ||
date | string | Match date. Pairs with the matches row of the same date and time. |
time | string | Match start time. |
map_name | string | null | Map the match was played on. |
rows | array[] | One array per sample point. |
downloads[] | ||
id | string | Artifact id, as it appears in url. |
name | string | File name. Always English, whatever this page's language is. |
bytes | integer | File size in bytes. |
url | string | Path to fetch, relative to the site root. |
Figures typed string keep the sign and the decimal places the file's own line carried. They are text, not numbers: parse before doing arithmetic.
Each row holds the values named in samples.columns, in that order: seconds as an integer, then two damage figures as numbers rounded to two decimals, or null. A figure the file never wrote is null, never 0.
Limits
| Limit | Value | Notes |
|---|---|---|
| File size | 256 B – 32 MB | |
| File parts | 1 | Sent as logfile. If more than one is sent, the last is read. |
| Matches per response | 300 | Over the cap the request is refused with 422 and nothing is published. The form on this page writes the full table as a download and has no cap. |
| Sample points per response | 6,000 | Over the cap the request is refused. A response is never silently shortened. |
| Requests per minute, per caller | 6 | Shared with the upload form on this page. Over it: 429. |
| Download link lifetime | 10 min |
Errors
Every failure returns {"ok": false, "error": "…"} with one of the status codes below. error is a human-readable message in the language negotiated from Accept-Language. Branch on the status code, not on the message text.
| Status | Meaning |
|---|---|
200 | Success. |
400 | Missing file part, file below the minimum size, unreadable file, or an invalid parameter value. |
405 | Method other than POST. |
413 | Body above the maximum size. |
422 | File read but not usable: nothing decodable, or over a cap listed above. |
429 | Rate limit exceeded. Back off; do not retry immediately. |
500 | Internal error. |
503 | Busy, timed out, or the endpoint is disabled. |