Forts Tools

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.

Maximum file size 300 MB.

What should happen to it?

Give a start and an end time and everything outside that stretch is dropped. The result is a normal replay of just that stretch.

minutes and seconds, like 5:00, or a number of seconds

Names go away; the match itself plays exactly as before.

Reads the recorded action stream out and hands it back as downloadable files, zipped. This runs last, so it exports whatever the steps above left.

How much detail Some replay formats were produced by more than one build. Leave this on automatic and every tier simply labels the export with all the possible builds; pick one only if you want it narrowed to that build.

Compares the recording against signatures that have already been characterised and reported. Most recordings cannot be checked, and that is said plainly rather than reported as a clean result.

Across all of our disclosures and findings to date, we have identified 8 distinct cheat and match-integrity signatures. This tool screens uploaded replays for 0 of them.

Whatever you tick, it happens in this order: keep part of the match, then remove identities, then change release. Each step works on what the one before it produced, so it is one upload however many you choose.

Tick nothing and you just get told what the replay is.

One thing at a time instead

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.

EndpointPOST https://forts-tools.com/api/replay
Request bodymultipart/form-data
Response bodyapplication/json
AuthenticationNone. No key, no origin allow-list.
Browser accessNo CORS headers are sent. fetch() from a page is blocked by the browser before the body is readable. Call this endpoint server-side.

Parameters

NameInTypeRequiredNotes
replaymultipart bodyfilerequiredOne Forts .fwr recording.
seasonmultipart bodyintegeroptionalThe 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.

FieldTypeDescription
okbooleantrue on success, false on failure.
schemastringContract version. Currently forts-replay/1.
releaseobjectThe release the recording reports. Fields below.
fileobjectSize and digest of the file you sent. Fields below.
map_namestring | nullMap name as the recording carries it.
mapsstring[]Every map the recording names, in file order. The first is also given above.
maps_totalintegerHow many maps the recording carries, counted before any ceiling below is applied.
durationstring | nullMatch length, mm:ss.
duration_secnumber | nullMatch length in seconds, two decimals.
playersobject[]One object per player. Fields below.
players_totalintegerHow many players the recording carries, counted before any ceiling below is applied.
commandersobject[]One object per commander. Fields below.
modsstring[]Add-on names the recording carries. Empty when it carries none.
mods_totalintegerHow many mods the recording carries, counted before any ceiling below is applied.
winnerstring | null"left" or "right".
winner_conflictbooleanTrue when the recording carries competing claims about who won. winner is null when it does.
game_endedboolean | nullWhether the recording reaches the end of the match. null when that could not be read.
rankedboolean | nullWhether 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_basisstringWhich signal decided it, so an unknown is never read as a no.
ranked_off_liststring[]Mods present that ranked would have refused. A non-empty list is the proof the match was not ranked.
ranked_conflictbooleanTrue when two independent signals disagree: the name claims a ranked season while the mods could not have passed ranked. Reported, not resolved.
seasoninteger | nullThe season the request sent, after validation; null when it sent none. Echoed so you can see what the conflict check ran against.
known_issuesobjectThe known-issue scan. Always present; a caller may ignore it.
release
build_idstringIdentifier for this release in the build table.
versionstringRelease number, e.g. 1.38.2.
revisionstring | nullRevision, e.g. r22447.
releasedstring | nullRelease date, YYYY-MM-DD.
labelstringDisplay label. English on every locale.
exactbooleanfalse 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_currentbooleanWhether this is the release the game currently ships.
file
bytesintegerSize of the uploaded file in bytes.
sha256stringSHA-256 of the uploaded file, lower-case hex, so a batch caller can key results without re-reading the file.
players[]
namestring | nullPlayer name as the recording carries it. Not redacted.
sidestring | null"left" or "right". Fixed tokens, not display text.
steam_idstring | nullSteam account id as the recording carries it.
commanders[]
namestring | nullCommander name as the recording carries it. Not translated.
sidestring | null"left" or "right". Fixed tokens, not display text.
known_issues
checkedbooleantrue 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.
signaturesstring[]The signatures this answer was produced against.
reasonstringWhy 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.
findingsobject[]One entry per seat that matched. Empty on most recordings.
known_issues.findings[]
steam_idstringThe account the recording attributes the matching commands to.
signaturestringWhich signature matched.
responsestringThe standing status of that signature. A match is not proof on its own.

Limits

LimitValueNotes
File size4096 B - 64 MBLower 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 parts1Sent as replay. If more than one is sent, the last is read.
Requests per minute, per caller6Shared with the upload form on this page. Over it: 429.
List ceilings (players / add-ons / maps)64 / 64 / 32Set far above anything the game produces, so a real recording comes back whole. Compare each list with its _total count to be certain.
Text length128Characters kept from any one name read out of the file.
RetentionnoneThe 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.

StatusMeaning
200Success.
400Missing file part, file below the minimum size, unreadable file, or an invalid parameter value.
405Method other than POST.
413Body above the maximum size.
422File read but not usable: unsupported game version, or nothing decodable.
429Rate limit exceeded. Back off; do not retry immediately.
500Internal error.
503Busy, timed out, or the endpoint is disabled.