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

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 characterised signatures of known exploits. A match means the outcome came from a known exploit, not a game bug. 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 2 of them.

Whatever you tick, it happens in this order: keep part of the match, then export the action stream. 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.

Keep or publish (optional, off by default)

The file and its result are stored with the game version and the upload time, never the file's name, so forts-tools can study them. You get a private link to download or delete them. Up to 96 MB per file, 50 a day.

Replays only, and it also keeps a copy. The analysis gets a public page and is listed with the recent uploads until you delete it with your private link.

Analyse sends this file to the replay analysis page instead: scoreboard, economy, damage, build order and actions, drawn as charts with their data tables. The ticked operations do not run.

One thing at a time instead

Your uploads are not kept unless you choose to keep them. Without Keep or Publish ticked, a replay is deleted as soon as it has been processed, and any result you download expires shortly afterwards. With Keep, the file and its result are stored until you delete them with your private link; with Publish, its analysis also gets a public page. Your file's name is never stored or logged. The Activity tab shows which operation ran, when, and a short summary such as the game build — never a filename or a player.

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.
analysismultipart bodyintegeroptionalOptional. 1 adds the replay analysis (forts-analysis/1) under analysis; analysis_error names a refusal and is otherwise null. Any other value is ignored and the answer is unchanged.

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.
ranked_flagboolean | nullThe ranked flag the game wrote into the replay header. It decides the verdict whenever it is readable. null when the file's header layout is not verified or the flag's check fails.
ranked_block_validboolean | nullWhether the ranked block that follows a set flag passes its check word. null when the flag is not set.
recorded_localstring | nullWhen the match was recorded, as YYYYMMDD_HHMMSS: the recording PC's local clock at match load, with no time zone. null for replays from before 1.39.
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.39.0.
revisionstring | nullRevision, e.g. r22745.
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, decode_failed, no_signatures or unsupported_build; 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, unless you send keep=1 or publish=1. 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.