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).

The API is served by the FillTrax dashboard at 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

Getting started

  1. 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).
  2. 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.
  3. Configure the machine or gateway to POST each cycle to /api/ingest/telemetry with the key (below).
  4. Send a test reading. Within seconds it appears on the machine's page under Recent telemetry, and the HTTP API source shows "Receiving data".
Lost or exposed key? An owner can issue a new one from the machine's page (Rotate key); the old key stops working immediately. Note that downloading the Windows client for a machine also issues it a new key, which replaces the one the HTTP API was using.

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:

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

MethodPathPurpose
POST/api/ingest/telemetrySend one reading.
POST/api/ingest/telemetry/batchSend up to 500 readings at once (or an empty array to check in).
GET/api/healthCheck 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).

FieldTypeRules
recordedAt requiredstring, ISO 8601When 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 requiredstring or numberWho 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 requirednumber (lb)Weight added to the tire. Greater than 0, at most 2000.
cycleTimeMs requirednumber (ms)How long the pump ran. Greater than 0, at most 3,600,000 (1 hour). Rounded to whole milliseconds.
targetWeight optionalnumber (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 optionalnumber 0–1Share of the fill that is liquid (typically about 0.98).
grindRatio optionalnumber 0–1Share of the fill that is grind (typically about 0.02).
sequenceId optionalstringA 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

POST /api/ingest/telemetry

The body is a single reading object. Send it when the cycle finishes.

StatusBodyMeaning
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

POST /api/ingest/telemetry/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" }
]
StatusBodyMeaning
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

GET /api/health

Returns 200 {"status":"ok"} when FillTrax is up. No authentication. Handy for a gateway to test connectivity before sending.

Status codes & errors

StatusError messageWhat to do
200 / 201(success)Move on to the next reading.
400Invalid reading + detailsFix the reading (the details say how). Don't retry it unchanged.
401Missing or malformed Authorization headerSend Authorization: Bearer <key>.
401Invalid or unallocated API keyThe key is wrong, was replaced, or the machine's license was removed. Get the current key from FillTrax. Don't keep retrying.
403Machine is not activeThe machine is set inactive in FillTrax.
403Data source is pausedSomeone paused the HTTP API source on the machine's page; resume it there.
413Too 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.

Make it unique across the customer's machines, not just on one machine: duplicates are detected across the whole account. Include something machine-specific, e.g. the serial number plus a cycle counter: "SN1234-000418", or the serial plus the cycle's timestamp.

Times & timezones

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 later
const 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 $reading
curl -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

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.

OptionHow it worksBest for
Windows clientA 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.
EmailEach 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 importUpload 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
NeededRecognized headers (examples)
Date (and time)Date, Timestamp, DateTime + Time
OperatorOper ID, Operator ID, Operator
Fill weight (lb)Tire Fill, Fill Weight, Fill Wt
Fill / pump timeFill Time, Pump Time, Cycle Time (minutes, seconds or ms; detected from the header or the values)
OptionalTarget 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

LimitValue
Readings per batch500
Fill weight / target weightgreater than 0, at most 2000 lb
Pump timegreater than 0, at most 1 hour
Future timestampsup to 5 minutes ahead
operatorId / sequenceId length64 / 200 characters
Machines per API keyexactly one

What it doesn't do

Troubleshooting

SymptomLikely cause
Every request is 401Wrong 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 doubledReadings 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 figurestargetWeight isn't being sent (or is 0).
Operators show as "Operator 12"Normal: name them once in FillTrax (Team → Operators).