FillTrax API Reference
How tire-fill machines (or the gateways beside them) send every fill cycle to FillTrax over HTTPS.
Overview
FillTrax records one reading per fill cycle: when it finished, who ran it, how much was filled, how long the pump ran, and optionally the target weight and fill mix. Everything in FillTrax (live status, Insights, reports, alerts, the assistant) is built from these readings.
The API is a small, write-only ingestion API: a machine authenticates with its own API key and posts readings as JSON. There are two ways to send them: one reading per request as each cycle finishes, or several at once in a batch (for example after a connection outage).
https://dashboard.filltrax.com. The full endpoint is also shown on each machine's page under Data sources → HTTP API → Connection details.What the API can do
- Record fill cycles for one machine per API key, live as they happen.
- Send buffered cycles in batches of up to 500, e.g. after the machine was offline. Each reading in a batch stands on its own.
- Validate every reading on arrival, and say exactly what's wrong with any it rejects.
- Ignore repeats safely: a reading with a
sequenceIdFillTrax already has is skipped, so retries never double-count. - Check in without data: an empty batch tells FillTrax the sender is alive (it updates the machine's "last seen").
- Accept timestamps from any timezone, as long as the offset is included.
- Keep extra fields you send for debugging, without letting them affect any numbers.
Getting started
- In FillTrax, add the machine (Machines → Add machine) and allocate a license to it (an owner can do this from the machine's page or Licenses).
- Copy the API key shown when the license is allocated. It starts with
ftk_live_and is shown once: FillTrax stores only a hash and can't show it again. - Configure the machine or gateway to
POSTeach cycle to/api/ingest/telemetrywith the key (below). - Send a test reading. Within seconds it appears on the machine's page under Recent telemetry, and the HTTP API source shows "Receiving data".
Authentication
Every request carries the machine's API key as a bearer token:
Authorization: Bearer ftk_live_xxxxxxxxxxxxxxxxxxxxxxxx
The key identifies the machine, so readings never name a machine themselves. A request is accepted only if:
- the key is valid and its license is still allocated to a machine,
- the machine's status is active, and
- its HTTP API data source isn't paused, and the customer's account isn't cancelled.
Removing a machine's license, deactivating the machine or pausing its data source stops data immediately, without any change on the machine. Always use HTTPS.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /api/ingest/telemetry | Send one reading. |
| POST | /api/ingest/telemetry/batch | Send up to 500 readings at once (or an empty array to check in). |
| GET | /api/health | Check FillTrax is reachable (no key needed). |
Request bodies are JSON with Content-Type: application/json; responses are JSON.
The reading object
One fill cycle. Unknown fields are ignored for all metrics (but kept with the raw reading, for debugging).
| Field | Type | Rules |
|---|---|---|
recordedAt required | string, ISO 8601 | When the cycle ended, with a timezone: "2026-09-27T14:25:59Z" or "2026-09-27T10:25:59-04:00". Seconds and fractions optional. No more than 5 minutes in the future. |
operatorId required | string or number | Who ran the cycle, e.g. "12" or "JD". 1–64 characters. Names are set in FillTrax (Team → Operators), so machines only send the ID. |
fillWeight required | number (lb) | Weight added to the tire. Greater than 0, at most 2000. |
cycleTimeMs required | number (ms) | How long the pump ran. Greater than 0, at most 3,600,000 (1 hour). Rounded to whole milliseconds. |
targetWeight optional | number (lb) | What the machine was aiming for on this tire. Greater than 0, at most 2000. 0 or null means "no target". Enables on-target and giveaway metrics. |
liquidRatio optional | number 0–1 | Share of the fill that is liquid (typically about 0.98). |
grindRatio optional | number 0–1 | Share of the fill that is grind (typically about 0.02). |
sequenceId optional | string | A unique ID for this cycle, up to 200 characters. A repeat is ignored, which makes retries safe. Strongly recommended; see Duplicates & retries. |
{
"recordedAt": "2026-09-27T14:25:59Z",
"operatorId": "12",
"fillWeight": 51,
"cycleTimeMs": 128000,
"targetWeight": 50,
"liquidRatio": 0.98,
"grindRatio": 0.02,
"sequenceId": "SN1234-000418"
}
Send one reading
The body is a single reading object. Send it when the cycle finishes.
| Status | Body | Meaning |
|---|---|---|
| 201 | {"status":"recorded"} | Stored. |
| 200 | {"status":"already recorded"} | Its sequenceId was already received; nothing changed. Treat as success. |
| 400 | {"error":"Invalid reading","details":["fillWeight is required", …]} | The reading broke the rules. details lists every problem, not just the first. Fix it before resending. |
| 400 | {"error":"Invalid JSON body"} | The body wasn't valid JSON. |
| 401 / 403 | {"error":"…"} | See Status codes & errors. |
Send a batch
The body is a JSON array of up to 500 readings. Use it to flush cycles buffered while offline, or to upload a run of cycles at once. Each reading is checked and stored on its own: good ones are recorded even when others in the same batch are rejected.
[
{ "recordedAt": "2026-09-27T14:25:59Z", "operatorId": "12", "fillWeight": 51, "cycleTimeMs": 128000, "sequenceId": "SN1234-000418" },
{ "recordedAt": "2026-09-27T14:32:21Z", "operatorId": "12", "fillWeight": 49, "cycleTimeMs": 124500, "sequenceId": "SN1234-000419" }
]
| Status | Body | Meaning |
|---|---|---|
| 201 | {"status":"recorded","count":2,"recorded":2,"duplicates":0,"rejected":0} | Every reading was recorded or was already there. |
| 400 | {"error":"Some readings were invalid and were not recorded","count":2,"recorded":1,"duplicates":0,"rejected":1,"details":[{"index":1,"errors":["cycleTimeMs is required"]}]} | The valid ones were recorded; details names each rejected reading by its position (0-based) in the array. |
| 400 | {"error":"Invalid JSON body: expected an array of readings"} | The body wasn't a JSON array. |
| 413 | {"error":"Too many readings: at most 500 per batch, got 750"} | Split it into batches of 500 or fewer. |
Checking in: an empty array [] returns 201 with "count":0 and records nothing, but updates the machine's "last seen". Useful for a sender that only has data now and then.
Resending a whole batch after fixing a rejected reading is safe as long as the readings carry sequenceIds: the ones already recorded come back as duplicates.
Health check
Returns 200 {"status":"ok"} when FillTrax is up. No authentication. Handy for a gateway to test connectivity before sending.
Status codes & errors
| Status | Error message | What to do |
|---|---|---|
| 200 / 201 | (success) | Move on to the next reading. |
| 400 | Invalid reading + details | Fix the reading (the details say how). Don't retry it unchanged. |
| 401 | Missing or malformed Authorization header | Send Authorization: Bearer <key>. |
| 401 | Invalid or unallocated API key | The key is wrong, was replaced, or the machine's license was removed. Get the current key from FillTrax. Don't keep retrying. |
| 403 | Machine is not active | The machine is set inactive in FillTrax. |
| 403 | Data source is paused | Someone paused the HTTP API source on the machine's page; resume it there. |
| 413 | Too many readings… | Batches only: send at most 500 per request. |
| 5xx / no response | (server or network problem) | Keep the reading and retry later with backoff. With a sequenceId, retrying can't double-count. |
Recommended sender behavior: on 2xx, discard the reading; on 400, log it and discard (it will never be accepted as is); on 401/403, stop and alert someone; on 5xx or a timeout, keep it and retry.
Duplicates & retries
Give every reading a sequenceId that's unique to that cycle and stays the same if you send it again. FillTrax skips any reading whose sequenceId it already has, so a retry after a timeout, a resent batch, or a replay of a buffered log never counts a tire twice.
"SN1234-000418", or the serial plus the cycle's timestamp.- Readings without a
sequenceIdare always stored, so a retried one would be counted twice. - A rejected reading's
sequenceIdis not remembered, so you can fix the reading and resend it under the same ID.
Times & timezones
recordedAtmust include its offset (Zfor UTC, or e.g.-07:00). A bare local time is rejected, because FillTrax would otherwise have to guess the machine's timezone.- Readings up to 5 minutes in the future are accepted (clocks drift); later ones are rejected with
recordedAt is in the future; check the machine's clock. Keep the machine's clock synced (NTP). - Old readings are fine: a batch of last week's cycles is recorded with their real times and shows up in that week's reports.
- FillTrax shows each time in the viewing user's own timezone, and works out days and shifts in each location's timezone.
Code examples
curl -X POST https://dashboard.filltrax.com/api/ingest/telemetry \
-H "Authorization: Bearer $FILLTRAX_KEY" \
-H "Content-Type: application/json" \
-d '{
"recordedAt": "2026-09-27T14:25:59Z",
"operatorId": "12",
"fillWeight": 51,
"cycleTimeMs": 128000,
"targetWeight": 50,
"sequenceId": "SN1234-000418"
}'import os, requests
from datetime import datetime, timezone
reading = {
"recordedAt": datetime.now(timezone.utc).isoformat(),
"operatorId": "12",
"fillWeight": 51,
"cycleTimeMs": 128000,
"targetWeight": 50,
"sequenceId": "SN1234-000418",
}
r = requests.post(
"https://dashboard.filltrax.com/api/ingest/telemetry",
json=reading,
headers={"Authorization": f"Bearer {os.environ['FILLTRAX_KEY']}"},
timeout=30,
)
if r.status_code in (200, 201):
pass # recorded (or already recorded)
elif r.status_code == 400:
print("rejected:", r.json().get("details")) # fix, don't retry as is
elif r.status_code in (401, 403):
raise SystemExit(r.json()["error"]) # key or machine problem
else:
... # 5xx: keep the reading and retry laterconst res = await fetch("https://dashboard.filltrax.com/api/ingest/telemetry", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FILLTRAX_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
recordedAt: new Date().toISOString(),
operatorId: "12",
fillWeight: 51,
cycleTimeMs: 128000,
targetWeight: 50,
sequenceId: "SN1234-000418"
})
});
const body = await res.json();
if (!res.ok) console.error(res.status, body.error, body.details ?? "");$headers = @{ Authorization = "Bearer $env:FILLTRAX_KEY" }
$reading = @{
recordedAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
operatorId = "12"
fillWeight = 51
cycleTimeMs = 128000
targetWeight = 50
sequenceId = "SN1234-000418"
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://dashboard.filltrax.com/api/ingest/telemetry" `
-Headers $headers -ContentType "application/json" -Body $readingcurl -X POST https://dashboard.filltrax.com/api/ingest/telemetry/batch \
-H "Authorization: Bearer $FILLTRAX_KEY" \
-H "Content-Type: application/json" \
-d '[
{"recordedAt":"2026-09-27T14:25:59Z","operatorId":"12","fillWeight":51,"cycleTimeMs":128000,"sequenceId":"SN1234-000418"},
{"recordedAt":"2026-09-27T14:32:21Z","operatorId":"12","fillWeight":49,"cycleTimeMs":124500,"sequenceId":"SN1234-000419"}
]'What happens to the data
- Live: open FillTrax pages update within seconds of a reading arriving: the Overview's machine status (running, idle, stopped), today's tires, pump rate and cycles per hour.
- Metrics: pump rate (
fillWeight÷cycleTimeMs) against the machine's rated maximum, utilization, stops, operator pace, and, whentargetWeightis sent, on-target fills and over-fill giveaway. - Reports & alerts: daily reports (PDF and CSV), Insights over 24 hours to 90 days, and alerts for a machine that stops mid-shift, goes quiet, runs slow or sends rejected readings.
- Rejected readings are kept with the reason and shown on the machine's page under Data sources ("Failed to parse"), so whoever maintains the integration can see what's wrong without server access.
Without code: CSV options
For machines that write CSV logs instead of calling an API, FillTrax has three no-code routes. They all read the same columns and use the same duplicate protection, so a row is never counted twice whichever way it arrives.
| Option | How it works | Best for |
|---|---|---|
| Windows client | A small program on a Windows PC (7 or later) watches the folder the machine logs to and sends new rows every 30 seconds. Downloaded from the machine's page, with its key built in. | Machines with a PC beside them. |
| Each machine gets its own private FillTrax address; its system emails the CSV log there (attached, or pasted in the message). Sending the whole log every time is fine. | Systems that can send email but not call an API. | |
| CSV import | Upload a CSV once from the machine's page, with a preview, column mapping and one-click undo. | History and catch-up. |
Columns are recognized from the header row; the controller's usual log layout works as is:
Date,Time,Mach ID,Tire ID,Oper ID,Tire Fill,Target Wt,Liq %,Fill Time
9/27/2026,14:25:59,1,418,12,51,50,98,2.13
| Needed | Recognized headers (examples) |
|---|---|
| Date (and time) | Date, Timestamp, DateTime + Time |
| Operator | Oper ID, Operator ID, Operator |
| Fill weight (lb) | Tire Fill, Fill Weight, Fill Wt |
| Fill / pump time | Fill Time, Pump Time, Cycle Time (minutes, seconds or ms; detected from the header or the values) |
| Optional | Target Wt, Liq %, Mach ID, Tire ID |
CSV times have no timezone, so they're read in the machine location's timezone set in FillTrax.
Limits at a glance
| Limit | Value |
|---|---|
| Readings per batch | 500 |
| Fill weight / target weight | greater than 0, at most 2000 lb |
| Pump time | greater than 0, at most 1 hour |
| Future timestamps | up to 5 minutes ahead |
operatorId / sequenceId length | 64 / 200 characters |
| Machines per API key | exactly one |
What it doesn't do
- No reading data back. The API only receives. Reports, CSV exports and PDFs are downloaded from the FillTrax dashboard by signed-in users.
- No editing or deleting readings through the API. (CSV imports can be undone from the dashboard.)
- No managing machines, users or licenses through the API; that's done in the dashboard.
- No webhooks or push notifications to outside systems; alerts go to FillTrax users by email and in the app.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Every request is 401 | Wrong key, or a new key was issued (Rotate key, or a Windows client download), or the license was removed. |
| Readings rejected as "in the future" | The machine's clock is ahead, or a local time was sent with the wrong offset. |
| "recordedAt must be an ISO 8601 time with a timezone" | The time has no Z or ±hh:mm offset. |
| Counts look doubled | Readings are being retried without a sequenceId. |
| Some readings from another machine are "already recorded" | sequenceIds aren't unique across machines; add the serial number. |
| No on-target or giveaway figures | targetWeight isn't being sent (or is 0). |
| Operators show as "Operator 12" | Normal: name them once in FillTrax (Team → Operators). |