A position on a map answers where. Almost every real tracking problem also needs what — engine rpm, tank level, cargo temperature, battery voltage, a door sensor.
Most platforms make that a schema exercise: register the field, pick a type, redeploy, then send data. Here, the field exists because you sent it.
Send anything
curl "https://gpstrack.dev/api/ingest?token=$TOKEN&lat=6.9271&lng=79.8612\
&rpm=900&fuel=42.5&coolant=88&door=true"
rpm, fuel, coolant and door are not fields the relay knows. They are kept
anyway, as custom channels, and appear in the widget picker on the next poll.
The same over MQTT:
{ "lat": 6.9271, "lng": 79.8612, "rpm": 900, "fuel": 42.5, "door": true }
Both transports run through one normaliser. That is a deliberate design choice rather than a coincidence: HTTP and MQTT used to be two copies of the same logic, and two copies drift on exactly this — which custom fields each one accepts. An identical payload now produces an identical point either way.
Types are inferred, and query strings are the interesting case
A query string delivers everything as text. rpm=900 arrives as the string
"900", and a gauge cannot be drawn from a string, so values are coerced:
| Sent | Stored as |
|---|---|
900 |
number 900 |
42.5 |
number 42.5 |
true / false |
boolean |
left-rear |
string "left-rear" |
| (empty) | dropped |
The rule is: if it parses as a finite number it becomes one, true/false
become booleans, and everything else stays a string. This is why you do not have
to declare types — but also why a field that is sometimes numeric and sometimes
not will change display kind between points. Keep a channel one type.
Nested payloads flatten, one level
Device firmware usually has structure already:
{
"lat": 6.9271, "lng": 79.8612,
"engine": { "rpm": 900, "coolant": 88 },
"tank": { "level": 42.5 }
}
becomes the channels engine.rpm, engine.coolant and tank.level. You do not
have to flatten it yourself.
One level, and no further. Recursing into arbitrarily deep payloads is a denial-of-service vector rather than a feature, and real device payloads are shallow.
Binding a channel to a widget
Channels are discovered from the points that actually arrived, so the picker only ever offers fields your device really sent. Pick a channel, pick how to draw it:
- value — the number, large and legible. The default for anything numeric
- gauge — needs a
minandmax; good for fuel, temperature, load - sparkline — recent history rather than the instant; good for rpm
- compass — a bearing in degrees
- text — strings and booleans
Widgets are stored per session in the browser, and a new session seeds its layout from the last one you edited — so a fleet of identical devices does not mean rebuilding the same dashboard every time.
What the relay will not accept
The bounds are a memory budget, not hygiene. The relay holds a thousand points per session in RAM, so an unbounded bag of strings per point is a way to run the server out of memory:
| Limit | Value |
|---|---|
| Custom keys per point | 16 |
| Key length | 32 characters |
| Key characters | A-Z a-z 0-9 _ . - |
| String value length | 64 characters |
| Whole custom bag | 512 bytes |
Over the limit, the extra keys are dropped and the point is still accepted. Losing the position because the seventeenth channel did not fit would be the wrong trade — the position is the part that matters.
token and key can never become channels. They are how a request
authenticates, and without that exclusion your session's write secret would be
captured as telemetry: rendered in a widget, written to browser storage, and
exported into the CSV and GPX files people share. The known field aliases —
lat, latitude, speedKph and the rest — are reserved for the same reason.
A worked example: a generator that reports itself
#!/usr/bin/env bash
TOKEN=tk_9d41b0e6a7c2f5
LAT=6.9271; LNG=79.8612 # a fixed installation still wants a position
while true; do
LOAD=$(awk '{print $1}' /proc/loadavg)
TEMP=$(awk '{printf "%.1f", $1/1000}' /sys/class/thermal/thermal_zone0/temp)
DISK=$(df -P / | awk 'NR==2{print $5}' | tr -d '%')
curl -s "https://gpstrack.dev/api/ingest?token=$TOKEN&lat=$LAT&lng=$LNG\
&load=$LOAD&cpu_temp=$TEMP&disk_pct=$DISK&label=generator-01" > /dev/null
sleep 10
done
Three channels, no configuration, and a gauge on cpu_temp with min: 20 and
max: 90 in about fifteen seconds of clicking.
Nothing is invented
If a device sends only lat and lng, the dashboard shows only a position. No
signal-strength bar, no HDOP, no battery voltage.
That sounds obvious and it was not always true here: the dashboard used to fill
those panels with fixed constants — -70 dBm, HDOP 1.0, 0 Volts — for fields
no device had ever sent. A number on screen that came from nowhere is worse than
an empty panel, because you cannot tell them apart. Absent data now renders
nothing at all.
Where to go next
- Track anything with one HTTP request — the basics, if you skipped ahead
- Stream GPS from an ESP32 over MQTT — where nested payloads come from in practice