API reference
Replay Parser
Upload a Fortnite .replay file and receive structured match data — metadata, the reporter's stats, per-player aggregates, the ordered elimination feed, participants, and real per-team placements decoded from the replay's network stream.
Endpoint
/replay/parseBase URL https://api.zyron.pro/api/v1 — so the full URL is https://api.zyron.pro/api/v1/replay/parse.
Request
Send the replay as multipart/form-data with a single field named replay containing the .replay file.
Headers
| Header | Required | Value |
|---|---|---|
Authorization | Required | Bearer zyr_your_key |
Content-Type | Set by client | multipart/form-data (with boundary — let your HTTP client set it) |
Body field
| Field | Type | Description |
|---|---|---|
replayrequired | file | The Fortnite .replay file. Max 35 MB. Must be a valid Unreal replay — validated by the magic bytes 0x1CA2E27F at the start of the file. |
Constraints
35 MB are rejected with 413 file_too_large, and files that fail the magic-byte check are rejected with 415 unsupported_file.Examples
curl -X POST https://api.zyron.pro/api/v1/replay/parse \
-H "Authorization: Bearer zyr_your_key" \
-F "replay=@match.replay"Response
A 200 OK returns a JSON object with these top-level keys: source, meta, reporter, reporterStats, players, elims, participants, chunkSummary, and placements.
| Field | Type | Description |
|---|---|---|
source | "native" | "js" | "native" when the replay's network stream was decoded (real per-team placements, reporter.epicId, per-player teamIndex/placement). "js" means the network decoder was unavailable (e.g. a brand-new game build it doesn't yet support) and the response fell back to the event-chunk parser, in which case placements is the estimated heuristic described below. |
meta — object
| Field | Type | Description |
|---|---|---|
startedAt | string | Match start time, ISO 8601. |
lengthMs | number | Match length in milliseconds. |
build | string | null | Game build string, e.g. ++Fortnite+Release-41.00. May be null if absent from the replay. |
mapPath | string | null | Game asset path of the map, when present in the replay. |
playlist | string | null | Playlist id, e.g. Playlist_Habanero_PunchBerry_Duos (native source only). |
teams | number | null | Number of teams in the lobby (native source only). |
maxPlayers | number | null | Lobby capacity (native source only). |
friendlyName | string | The replay's internal name. |
fileVersion | number | Replay container format version. |
networkVersion | number | Engine network version the replay was recorded with. |
changelist | number | Engine changelist the replay was recorded with. |
encrypted | boolean | Whether the replay's event chunks are AES-encrypted (nearly always true). |
elimParseFailures | number | Elimination events that could not be decoded — usually 0. |
reporter — object
The uploader / recording player. Ground truth, not an estimate.
| Field | Type | Description |
|---|---|---|
epicId | string | null | The uploader's Epic account ID (native source; null if the replay didn't flag the recording player). |
placement | number | null | Final placement in the match. |
totalPlayers | number | null | Lobby size for the match. |
teamIndex | number | null | The uploader's team index (native source only). |
eliminations | number | The uploader's kill count. |
won | boolean | placement === 1. |
reporterStats — object | null
The uploader's full stat block for the match, straight from the replay's AthenaMatchStats event. null when that event wasn't present.
| Field | Type | Description |
|---|---|---|
accuracy | number | 0–1. |
assists | number | Assist count. |
eliminations | number | Same value as reporter.eliminations. |
weaponDamage | number | Damage dealt with weapons. |
otherDamage | number | Damage dealt by other means (falls, storm, etc.). |
revives | number | Teammates revived. |
damageTaken | number | Damage received. |
damageToStructures | number | Damage dealt to builds. |
materialsGathered | number | Materials harvested. |
materialsUsed | number | Materials spent building. |
totalTraveled | number | Distance travelled, in centimetres. |
players — array of objects
Per-player aggregates. When source is "native" this is the full lobby roster (every real player, even those never seen in the elimination feed), each carrying its real teamIndex and placement; the kill/knock/death/weapon/position detail is still derived from the elimination feed. Every bot elimination is merged into a single synthetic entry with isBot: true and id: "".
| Field | Type | Description |
|---|---|---|
id | string | Epic account ID, or "" for the bots entry. |
isBot | boolean | |
name | string | null | Display name, when known (native source). |
teamIndex | number | null | Real team index (native source only). |
placement | number | null | Real final placement of this player's team (native source only). |
kills | number | Full eliminations credited to this player. |
knocks | number | Knocks (down-but-not-out) credited to this player. |
deaths | number | Times this player was eliminated (knocked or killed). |
weapons | object | Kills/knocks by this player, keyed by weapon label. |
positions | array | World-space points (x/y/z in UE centimetres) tagged with t and event (kill/knock/death) — only the moments this player appears in the elimination feed, not a full movement trail. |
killedBy | array | Each time this player died: t, by (Epic ID), weapon, knocked. |
firstSeen | number | null | Timestamp (ms) of this player's first elim-feed appearance. |
lastSeen | number | null | Timestamp (ms) of this player's last elim-feed appearance. |
elims — array of objects
Every elimination and knock, sorted by time.
| Field | Type | Description |
|---|---|---|
t | number | Milliseconds since match start. |
eliminator | string | Epic account ID of the eliminator, or "" if a bot. |
eliminated | string | Epic account ID of the eliminated player, or "" if a bot. |
eliminatorIsBot | boolean | Whether the eliminator was a bot. |
eliminatedIsBot | boolean | Whether the eliminated player was a bot. |
eliminatorName | string | null | Display name, only ever populated for a named bot — never for a real player. |
eliminatedName | string | null | Display name, only ever populated for a named bot — never for a real player. |
gunType | number | Raw weapon/death-cause byte from the replay. |
gunLabel | string | Best-effort human label for gunType (e.g. AR, Sniper, Storm), or unknown (0x..) for an unmapped byte. |
knocked | boolean | True for a knock (down-but-not-out), false for a full elimination. |
eliminatedPos | object | null | World-space location of the eliminated player (UE centimetres), when present. |
eliminatorPos | object | null | World-space location of the eliminator, when present. |
participants — array of strings
| Field | Type | Description |
|---|---|---|
participants[] | string | Distinct Epic account IDs seen in the elimination feed. |
chunkSummary — object
Counts and byte totals for each chunk type in the file — a transparency indicator for how much of the replay this parser actually decodes.
| Field | Type | Description |
|---|---|---|
header | { count, bytes } | The single header chunk. |
data | { count, bytes } | Oodle-compressed netcode chunks — not decoded by this parser. |
checkpoint | { count, bytes } | Oodle-compressed checkpoint chunks — not decoded by this parser. |
event | { count, bytes } | Event chunks — this is what elims/reporterStats/players are built from. |
placements
Real per-team placements decoded from the replay's network stream (source: "native"): an object keyed by the team's real finishing position (as a string, per JSON), each with its actual roster of Epic IDs — one for solos, up to the squad size.
What a single replay can and can't show
Fallback: source === "js"
source: "js" and placements becomes a best-effort estimate from the elimination feed: players are grouped into inferred teams when they die close together in time and either close in world position or to the same eliminator (capped at 4). Treat estimated placements as approximate — check source to know which you got. placements is null only when the replay yields no usable data at all.| Field | Type | Description |
|---|---|---|
placements[n].winner | boolean | True for the match winner(s) — placement 1 (present only if the recorder's team won or the winner was still visible). |
placements[n].epicIds | string[] | Epic account IDs on this placement's team (1 for solos, up to the squad size). |
Example response
{
"source": "native",
"meta": {
"startedAt": "2026-06-22T10:33:48.000Z",
"lengthMs": 592354,
"build": "++Fortnite+Release-41.00",
"mapPath": "/Game/Athena/Maps/Athena_Terrain",
"playlist": "Playlist_Habanero_PunchBerry_Duos",
"teams": 20,
"maxPlayers": 40,
"friendlyName": "UnsavedReplay-2026.06.22-10.33.48",
"fileVersion": 8,
"networkVersion": 33,
"changelist": 12345678,
"encrypted": true,
"elimParseFailures": 0
},
"reporter": {
"epicId": "df05386e908940c4a56c748adf7771a9",
"placement": 7,
"totalPlayers": 40,
"teamIndex": 21,
"eliminations": 5,
"won": false
},
"reporterStats": {
"version": 3,
"accuracy": 0.34,
"assists": 1,
"eliminations": 5,
"weaponDamage": 612,
"otherDamage": 40,
"revives": 0,
"damageTaken": 260,
"damageToStructures": 890,
"materialsGathered": 540,
"materialsUsed": 310,
"totalTraveled": 412000
},
"players": [
{
"id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
"isBot": false,
"name": "SomePlayer",
"teamIndex": 12,
"placement": 8,
"kills": 2,
"knocks": 1,
"deaths": 1,
"weapons": { "AR": 2, "Shotgun": 1 },
"positions": [ { "t": 604100, "x": 12345.6, "y": -9876.5, "z": 320.1, "event": "death" } ],
"killedBy": [ { "t": 604100, "by": "0f1e2d3c4b5a69788796a5b4c3d2e1f0", "weapon": "Sniper", "knocked": false } ],
"firstSeen": 132400,
"lastSeen": 604100
}
],
"elims": [
{
"t": 132400,
"eliminator": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
"eliminated": "",
"eliminatorIsBot": false,
"eliminatedIsBot": true,
"eliminatorName": null,
"eliminatedName": null,
"gunType": 4,
"gunLabel": "AR",
"knocked": false,
"eliminatedPos": { "x": 11200.3, "y": -8100.2, "z": 300.5 },
"eliminatorPos": { "x": 11150.1, "y": -8050.7, "z": 302.0 }
},
{
"t": 271800,
"eliminator": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
"eliminated": "0f1e2d3c4b5a69788796a5b4c3d2e1f0",
"eliminatorIsBot": false,
"eliminatedIsBot": false,
"eliminatorName": null,
"eliminatedName": null,
"gunType": 3,
"gunLabel": "Shotgun",
"knocked": true,
"eliminatedPos": { "x": 13000.0, "y": -7000.0, "z": 280.0 },
"eliminatorPos": { "x": 12980.5, "y": -6990.2, "z": 281.0 }
},
{
"t": 604100,
"eliminator": "0f1e2d3c4b5a69788796a5b4c3d2e1f0",
"eliminated": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
"eliminatorIsBot": false,
"eliminatedIsBot": false,
"eliminatorName": null,
"eliminatedName": null,
"gunType": 6,
"gunLabel": "Sniper",
"knocked": false,
"eliminatedPos": { "x": 12345.6, "y": -9876.5, "z": 320.1 },
"eliminatorPos": { "x": 14500.0, "y": -9200.0, "z": 410.3 }
}
],
"participants": [
"a1b2c3d4e5f60718293a4b5c6d7e8f90",
"0f1e2d3c4b5a69788796a5b4c3d2e1f0"
],
"chunkSummary": {
"header": { "count": 1, "bytes": 2048 },
"data": { "count": 812, "bytes": 5242880 },
"checkpoint": { "count": 6, "bytes": 1048576 },
"event": { "count": 47, "bytes": 6100 }
},
"placements": {
"7": { "winner": false, "epicIds": ["df05386e908940c4a56c748adf7771a9", "c71b783ab45f4c32a9ef1677c5fe0f49"] },
"8": { "winner": false, "epicIds": ["a1b2c3d4e5f60718293a4b5c6d7e8f90", "2ac1fb3ab5f04b799eb0b835cee0908b"] },
"9": { "winner": false, "epicIds": ["455c11b4110440b796fbf57a674ee297", "b8e60c36a3044ac38e6970a3760d7762"] }
}
}Errors for this endpoint
| Status | error | When |
|---|---|---|
400 | invalid_replay | The file is a replay but is corrupt or unsupported. |
413 | file_too_large | Upload exceeds 35 MB. |
415 | unsupported_file | Not a .replay (fails the magic-byte check). |
422 | parse_timeout | Parsing exceeded the time budget. |
See the full list, including auth and rate-limit errors, on the Errors page.
How it works & safety
Uploads are never stored or executed
GET /key
Returns your tier and today's usage. Useful for showing quota in a dashboard without spending a parse.
/key{
"tier": "pro",
"limits": { "perMinute": 60, "perDay": 2000 },
"usage": { "day": "2026-07-06", "dayCount": 137 }
}See Rate limits for what these numbers mean.