FIX Log Analysis API
POST a FIX log from your engine, a script or a CI job and get the same statistics as the FIX log analyzer: message counts and rates, orders and outcomes, fill and reject rates, traded quantity and value, latency percentiles, reject reasons and session health. Free, no key, no sign-up. Or skip the server entirely and open a log from your application straight in the browser.
Privacy: an API request is processed in memory, not stored, not logged and not cached. The browser handoff below never reaches our server at all.
Endpoint
POST https://fixparser.zagtrader.com/api/v1/analyze- Raw text: the log as the body,
Content-Type: text/plainorapplication/octet-stream. - Gzip:
Content-Encoding: gzip,Content-Type: application/gzip, or any body that starts with the gzip bytes 1f 8b. - Multipart:
multipart/form-datawith a field namedfile: a .log, .txt, .gz or .zip file.
| Query parameter | Meaning |
|---|---|
| format | json (default), html (a self-contained report file that works offline) or text (the plain-text summary) |
| clock | Clock for latency: log (default, the log timestamp), 52 (SendingTime) or 60 (TransactTime) |
| tz | IANA time zone for the times shown, for example Europe/London; default: as written in the log |
| anonymise | 1 replaces CompIDs and free text with placeholders (COMP1, TEXT1), as the parser's Anonymise switch does. The statistics never contain accounts, order IDs or party IDs. |
GET on the same address returns a short usage document as JSON.
Examples
curl --data-binary @fix.log -H "Content-Type: text/plain" "https://fixparser.zagtrader.com/api/v1/analyze"gzip -c fix.log | curl --data-binary @- -H "Content-Encoding: gzip" "https://fixparser.zagtrader.com/api/v1/analyze"curl -F "file=@fix-logs.zip" "https://fixparser.zagtrader.com/api/v1/analyze"curl --data-binary @fix.log -H "Content-Type: text/plain" "https://fixparser.zagtrader.com/api/v1/analyze?format=html" -o fix-report.htmlcurl --data-binary @fix.log -H "Content-Type: text/plain" "https://fixparser.zagtrader.com/api/v1/analyze?anonymise=1&format=text&tz=Europe/London"$r = Invoke-RestMethod -Method Post -Uri "https://fixparser.zagtrader.com/api/v1/analyze" -InFile .\fix.log -ContentType "text/plain"
$r.stats.orders.fillRate
$r.report_urlimport requests
with open("fix.log", "rb") as f:
r = requests.post("https://fixparser.zagtrader.com/api/v1/analyze", data=f, headers={"Content-Type": "text/plain"}, params={"clock": "log"})
r.raise_for_status()
stats = r.json()["stats"]
print(stats["orders"]["fillRate"], r.json()["report_url"])Response
JSON with the input summary, the full statistics and a report_url that opens the charts in the browser. The report link carries the statistics only (totals and top lists, no messages) in the part after #, so opening it sends nothing to a server.
{
"api": "zagtrader-fix-analytics",
"version": "1",
"generated_at": "2026-10-20T12:00:00.000Z",
"input": {
"bytes": 2453190, "compressed": false, "messages": 11022, "sessions": 3,
"fix_versions": ["FIX.4.4", "FIX.4.2", "FIXT.1.1"],
"first_time": "2026-10-19 09:25:00.000", "last_time": "2026-10-19 16:10:10.021",
"time_zone": "as written in the log", "clock": "log", "anonymised": false
},
"stats": {
"overview": { "messages": 11022, "sessions": 3, "peakPerSecond": { "count": 62, "ms": 1792402201000 }, "...": "..." },
"orders": { "orders": 2041, "fills": 2843, "fillRate": 0.754, "rejectRate": 0.0088, "orderToTradeRatio": 0.91, "outcomes": ["..."] },
"trading": { "symbols": ["..."], "currencies": ["..."], "sides": ["..."], "lastMarkets": ["..."] },
"rejects": { "total": 24, "session": ["..."], "order": ["..."], "cancel": ["..."], "texts": ["..."] },
"sessions": [{ "label": "ZAGOMS ↔ BROKERX", "gaps": 1, "resendRequests": 1, "heartBtInt": 30, "...": "..." }],
"latency": { "clock": "log", "metrics": [{ "metric": "ack", "count": 2037, "p50": 1.77, "p90": 5.27, "p99": 10.2, "max": 2390 }] },
"timeline": { "bucketMs": 300000, "buckets": ["..."] },
"skipped": []
},
"report_url": "https://fixparser.zagtrader.com/report#r=...",
"notes": ["Times are shown as written in the log; ..."]
}Limits and errors
- Request body: up to 4.5 MB. The hosting platform rejects larger bodies before they reach the API, so gzip larger logs (FIX logs compress about five times).
- Decompressed log: up to 200 MB; decompression stops beyond that.
- Processing time: up to 60 seconds per request; 100,000 messages take about ten seconds.
- Fair use: rate limited to about 60 requests a minute per address. For continuous or high-volume use, talk to ZagTrader.
Errors are JSON: { "error": { "code": "no_fix_messages", "message": "..." } }
| Status | Code | When |
|---|---|---|
| 400 | empty_body | The body or the file is empty. |
| 400 | no_fix_messages | No message starting with 8=FIX was found. |
| 400 | missing_file | A multipart form without a field named file. |
| 400 | invalid_parameter | format, clock or tz has a value the API does not accept. |
| 413 | body_too_large | The body is over 4.5 MB. Gzip the log first. |
| 413 | decompressed_too_large | The log is over 200 MB once decompressed. |
| 415 | unsupported_media_type | Send text, gzip, zip or multipart/form-data. |
| 422 | decode_failed | The gzip or zip data is damaged, or the file is not a text log. |
| 429 | rate_limited | More than 60 requests a minute from one address. |
| 500 | internal_error | Something failed on our side. The message never includes log content. |
Open a log from your application
Your engine or tool can show a log in the full parser and analyzer without any upload: it opens the parser in the browser and hands the log over with window.postMessage. The log goes from your application to the browser on the same computer and is never uploaded to ZagTrader or anyone else. The parser page shows “Log received from … Nothing was uploaded.”
From a web page: a button
Open /?handoff=1 on a click (so pop-up blockers allow it), wait for zagtrader-fix-ready, then post the log. A complete page to copy: example-button.html.
const PARSER = "https://fixparser.zagtrader.com";
button.onclick = () => {
const win = window.open(PARSER + "/?handoff=1", "zagtrader-fix-parser"); // on the click: no pop-up block
addEventListener("message", function onMessage(e) {
if (e.source !== win || e.origin !== PARSER) return;
if (e.data.type === "zagtrader-fix-ready") {
win.postMessage({ type: "zagtrader-fix-log", version: 1, name: "today.log", text: logText }, PARSER);
removeEventListener("message", onMessage);
}
});
};From a server or desktop engine: a local viewer file
Copy local-viewer-template.html, put the log inside <script type="text/plain" id="fixlog"> (write every </script in the log as <\/script), save it as a temporary .html file and open it with the default browser: start "" fix-log.html on Windows, xdg-open on Linux, open on macOS. The file shows the parser full-window (/?handoff=1&embed=1 in an iframe) and passes the log in. For large logs, gzip the log, base64-encode it and add data-encoding="base64-gzip"to the script block. The file also has an “Open in a new tab instead” link.
using System.Diagnostics;
using System.IO;
using System.Net.Http;
// Fill the local viewer template with the log and open it in the default browser.
static async Task OpenInFixParser(string logText, string name = "engine.log")
{
var template = await new HttpClient().GetStringAsync(
"https://fixparser.zagtrader.com/handoff/local-viewer-template.html"); // or ship a copy with your app
var safe = logText.Replace("</script", "<\\/script", StringComparison.OrdinalIgnoreCase);
var html = template
.Replace("data-name=\"engine.log\"", $"data-name=\"{System.Net.WebUtility.HtmlEncode(name)}\"")
.Replace("PUT THE FIX LOG HERE", safe);
var path = Path.Combine(Path.GetTempPath(), $"fix-log-{Guid.NewGuid():N}.html");
await File.WriteAllTextAsync(path, html);
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
}import html, pathlib, tempfile, urllib.request, uuid, webbrowser
def open_in_fix_parser(log_text: str, name: str = "engine.log") -> None:
"""Fill the local viewer template with the log and open it in the default browser."""
template = urllib.request.urlopen(
"https://fixparser.zagtrader.com/handoff/local-viewer-template.html").read().decode("utf-8")
safe = log_text.replace("</script", "<\\/script")
page = (template
.replace('data-name="engine.log"', f'data-name="{html.escape(name)}"')
.replace("PUT THE FIX LOG HERE", safe))
path = pathlib.Path(tempfile.gettempdir()) / f"fix-log-{uuid.uuid4().hex}.html"
path.write_text(page, encoding="utf-8")
webbrowser.open(path.as_uri())Message format
| Direction | Message | Notes |
|---|---|---|
| Parser to application | { type: "zagtrader-fix-ready", version: 1 } | Sent when the parser page has loaded, and again every second until a log arrives. |
| Application to parser | { type: "zagtrader-fix-log", version: 1, name?: string, text?: string, data?: ArrayBuffer | Uint8Array | Blob, view?: "analytics" | "timeline" } | Exactly one of text or data. data may be plain text, gzip or zip bytes. view defaults to analytics. Any other key, a different version or a second log is ignored. |
| Parser to application | { type: "zagtrader-fix-loaded", version: 1, messages: number } | The log is open; messages is the number of FIX messages found. |
| Parser to application | { type: "zagtrader-fix-error", version: 1, message: string } | The log could not be read, or it has no FIX messages. |
- The parser accepts the log from any origin, including
file://pages (origin “null”), because the log only flows into a display tool. It takes the first valid log and stops listening. - Add
embed=1to hide the site header and footer when the parser fills a window or an iframe. - If no log arrives within 15 seconds the page says it is waiting; the parser works as usual meanwhile.
- Size handling is the same as opening a file: large logs are parsed in a background thread with a progress bar.
Running FIX at scale? ZagTrader monitors sessions and order flow across all your FIX connections from one OMS/EMS.