Forts replay workbench
Upload a replay once. You get told what it is, and everything you tick is applied to it in turn — one file out at the end, no second upload.
One thing at a time instead
- Just identify a replay
- Break a long replay into several playable pieces
- Only remove the players' identities
- Put your own names on the players
- Export a replay's full action stream as downloadable files
- Get the command that installs an older build
Your uploads are not kept. A replay is deleted as soon as it has been processed, and any result you download expires shortly afterwards. Nothing about the contents of your file, or its name, is logged. The Activity tab shows only that an operation ran and when.
API reference
Everything a Forts replay says about itself, uncut - the form above with no operation ticked, as JSON. Server-to-server; the browser-facing surface is that form.
| Endpoint | POST https://forts-tools.com/api/replay |
|---|---|
| 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 |
|---|---|---|---|---|
replay | multipart body | file | required | One Forts .fwr recording. |
season | multipart body | integer | optional | The integer season prefix of a ranked recording's filename (43_…fwr), sent on its own. The filename itself is never read. One to three digits; anything else is ignored. With it, a prefix beside a mod ranked would refuse is reported as ranked_conflict. |
Example request
curl -X POST -F "replay=@your-replay.fwr" \
https://forts-tools.com/api/replay
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-replay/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-replay/1. |
release | object | The release the recording reports. Fields below. |
file | object | Size and digest of the file you sent. Fields below. |
map_name | string | null | Map name as the recording carries it. |
maps | string[] | Every map the recording names, in file order. The first is also given above. |
maps_total | integer | How many maps the recording carries, counted before any ceiling below is applied. |
duration | string | null | Match length, mm:ss. |
duration_sec | number | null | Match length in seconds, two decimals. |
players | object[] | One object per player. Fields below. |
players_total | integer | How many players the recording carries, counted before any ceiling below is applied. |
commanders | object[] | One object per commander. Fields below. |
mods | string[] | Add-on names the recording carries. Empty when it carries none. |
mods_total | integer | How many mods the recording carries, counted before any ceiling below is applied. |
winner | string | null | "left" or "right". |
winner_conflict | boolean | True when the recording carries competing claims about who won. winner is null when it does. |
game_ended | boolean | null | Whether the recording reaches the end of the match. null when that could not be read. |
ranked | boolean | null | Whether the match was ranked. null when the recording cannot say: a mod list ranked would have refused proves it was NOT ranked, but an allowed list is merely consistent with ranked. |
ranked_basis | string | Which signal decided it, so an unknown is never read as a no. |
ranked_off_list | string[] | Mods present that ranked would have refused. A non-empty list is the proof the match was not ranked. |
ranked_conflict | boolean | True when two independent signals disagree: the name claims a ranked season while the mods could not have passed ranked. Reported, not resolved. |
season | integer | null | The season the request sent, after validation; null when it sent none. Echoed so you can see what the conflict check ran against. |
known_issues | object | The known-issue scan. Always present; a caller may ignore it. |
release | ||
build_id | string | Identifier for this release in the build table. |
version | string | Release number, e.g. 1.38.2. |
revision | string | null | Revision, e.g. r22447. |
released | string | null | Release date, YYYY-MM-DD. |
label | string | Display label. English on every locale. |
exact | boolean | false means the era number in the recording covers several releases; the one named above is the newest it could be, not certainly the one it was recorded on. |
is_current | boolean | Whether this is the release the game currently ships. |
file | ||
bytes | integer | Size of the uploaded file in bytes. |
sha256 | string | SHA-256 of the uploaded file, lower-case hex, so a batch caller can key results without re-reading the file. |
players[] | ||
name | string | null | Player name as the recording carries it. Not redacted. |
side | string | null | "left" or "right". Fixed tokens, not display text. |
steam_id | string | null | Steam account id as the recording carries it. |
commanders[] | ||
name | string | null | Commander name as the recording carries it. Not translated. |
side | string | null | "left" or "right". Fixed tokens, not display text. |
known_issues | ||
checked | boolean | true only when a seat was actually graded. false means the recording could NOT be evaluated — it does not mean the recording is clean. Only true with an empty findings list means that. |
signatures | string[] | The signatures this answer was produced against. |
reason | string | Why this answer came out the way it did. One of the stable keys match, no_match, insufficient_sample, unattributed or decode_failed; empty when the scan is disabled. Not display text — a caller renders its own sentence for the value. |
findings | object[] | One entry per seat that matched. Empty on most recordings. |
known_issues.findings[] | ||
steam_id | string | The account the recording attributes the matching commands to. |
signature | string | Which signature matched. |
response | string | The standing status of that signature. A match is not proof on its own. |
Limits
| Limit | Value | Notes |
|---|---|---|
| File size | 4096 B - 64 MB | Lower than the form's own ceiling above. One branch of the read costs far more on a recording that stops before the match does, and this ceiling is where even that branch finishes inside the job limit. |
| File parts | 1 | Sent as replay. If more than one is sent, the last is read. |
| Requests per minute, per caller | 6 | Shared with the upload form on this page. Over it: 429. |
| List ceilings (players / add-ons / maps) | 64 / 64 / 32 | Set far above anything the game produces, so a real recording comes back whole. Compare each list with its _total count to be certain. |
| Text length | 128 | Characters kept from any one name read out of the file. |
| Retention | none | The upload is deleted as soon as identification finishes. No file is produced and nothing expires. |
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: unsupported game version, or nothing decodable. |
429 | Rate limit exceeded. Back off; do not retry immediately. |
500 | Internal error. |
503 | Busy, timed out, or the endpoint is disabled. |