GPSTrack

Send custom telemetry alongside GPS

4 min read4 of 5 in Telematics

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 min and max; 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

Open the dashboard → Push points from any device over HTTP or MQTT and watch them move on a live map.
More on Telematics dashboard All 5 →
  1. Telematics

    Track anything with one HTTP request

    Put a moving thing on a live map with nothing but curl — no account, no SDK, no app. A complete GPS tracking walkthrough in two requests.

    15 June 2026 · 4 min read

  2. Telematics

    Stream GPS from an ESP32 over MQTT

    Wire a NEO-6M to an ESP32, publish fixes over MQTT with TLS on port 8883, and watch them arrive on a live map. Full sketch, no backend to write.

    18 June 2026 · 5 min read