Overview

The GPS Tracking API gives your own systems read access to your fleet: vehicles, live positions, history, trips, alerts, sensors, geofences, sub-accounts and mileage. Everything is read-only, and a key never sees more than the person who created it.

Base URL   https://stage-app.sensoitgps.com/api/v1
Auth       Authorization: Bearer YOUR_KEY
Format     JSON, snake_case, UTC timestamps

Machine-readable specification: https://stage-app.sensoitgps.com/api/openapi.json

Quickstart

Three steps, about five minutes.

StepWhere
1Create a key with only the permissions you needMonitor → API Keys
2Call /devices to confirm the key worksyour server
3Poll /positions/latest every 10 secondsyour server
curl -H "Authorization: Bearer YOUR_KEY" \
  "https://stage-app.sensoitgps.com/api/v1/devices?limit=10"
The key is shown once. It is stored as a one-way hash, so it cannot be recovered — if you lose it, revoke it and create another.

Authentication

Send the key on every request. Either header works:

Authorization: Bearer YOUR_KEY
X-API-Key: YOUR_KEY

X-API-Key exists so code written against an older tracking API keeps working unchanged. There is no login call and no token to refresh — the key is the credential.

Never put a key in browser JavaScript or a mobile app. Anything shipped to a device is readable by whoever holds the device. Call this API from your server — that is also why no Access-Control-Allow-Origin header is sent.

A key can never exceed its creator. If your account cannot see a vehicle in the app, no key you create will return it. A key made by a sub-account is limited to that account’s own vehicles.

Polling live positions

GET /positions/latest is built for polling. It reads a pre-computed table, so it costs the same whether you have ten vehicles or ten thousand. Poll it every 10 seconds — not faster; nothing changes in between.

Every response carries an ETag. Send it back as If-None-Match and you get 304 Not Modified with no body when nothing has moved.

# first call — keep the ETag from the response headers
curl -D headers.txt -H "Authorization: Bearer YOUR_KEY" \
  "https://stage-app.sensoitgps.com/api/v1/positions/latest?limit=500"

# subsequent calls
curl -H "Authorization: Bearer YOUR_KEY" \
  -H 'If-None-Match: "a1b2c3..."' \
  "https://stage-app.sensoitgps.com/api/v1/positions/latest?limit=500"
A 304 does not count against your daily quota. A parked fleet costs you almost nothing, so using the ETag is strictly better than not.

Do not poll /positions/history on a timer. It reads stored frames and is meant for backfill and reports, not for a live view.

Pagination

Every list returns { "data": [...], "next_cursor": "..." }. Pass that value back as cursor for the next page. When next_cursor is null you have reached the end.

curl -H "Authorization: Bearer YOUR_KEY" \
  "https://stage-app.sensoitgps.com/api/v1/trips?from=2026-09-01T00:00:00Z&to=2026-09-08T00:00:00Z&limit=100"

# then, with next_cursor from that response
curl -H "Authorization: Bearer YOUR_KEY" \
  "https://stage-app.sensoitgps.com/api/v1/trips?from=2026-09-01T00:00:00Z&to=2026-09-08T00:00:00Z&limit=100&cursor=eyJrIjoi..."

Treat the cursor as opaque — do not build or parse one. limit defaults to 100 and caps at 500. Loop until next_cursor is null rather than stopping on a short page.

Time ranges

Time-ranged endpoints take from and to as ISO 8601. Omit both and you get the last 24 hours.

Endpoint groupMaximum windowWhy
positions, trips, alerts, sensors, geofence events31 daysreads stored frames
reports/mileage366 daysreads one pre-computed row per device per day

Ask for more and the start is moved forward, never the end back — so the most recent data you asked for is always the data you get.

Addressing a device

Every endpoint that takes device_id also accepts imei. Use whichever you already have; you never need to build a mapping table.

https://stage-app.sensoitgps.com/api/v1/trips?device_id=6f1c...&from=...
https://stage-app.sensoitgps.com/api/v1/trips?imei=860123456789012&from=...

An IMEI your key cannot see returns 404, the same as one that does not exist.

Rate limits

Each key has a per-second burst limit and a daily request limit. Every response tells you where you stand:

X-RateLimit-Limit: 50000          # requests per day
X-RateLimit-Remaining: 49994      # left today
X-RateLimit-Reset: 41412          # seconds until the daily counter resets
X-RateLimit-Limit-Second: 5
X-RateLimit-Remaining-Second: 4

Over a limit you get 429 with a Retry-After header in seconds. Wait that long — retrying immediately spends another burst slot and gets another 429.

Errors

Every failure has the same shape, whatever went wrong:

{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "Your API key is missing the required scope: alerts:read.",
    "required_scopes": ["alerts:read"],
    "granted_scopes": ["devices:read", "positions:read"]
  },
  "request_id": "req-8f2c1a"
}

Branch on code, never on message — the code is stable, the wording is not. Quote request_id in a support request and we can find the exact call.

StatuscodeWhat to do
400INVALID_REQUESTFix the parameters — the message says which.
401INVALID_API_KEYWrong, expired or revoked key. Create a new one.
403INSUFFICIENT_SCOPEThe key lacks a permission. Create one with the scope you need.
404NOT_FOUNDNo such record, or it is outside what this key can see.
429RATE_LIMITEDWait for Retry-After seconds.
500INTERNALOur fault. Retry with backoff, then send us the request_id.

Devices

GET/devicesdevices:read
  • imei string — Exact IMEI. Unlike `search`, this matches a single device.
  • status string — Filter by device status.
  • search string — Case-insensitive substring over name, IMEI and plate.
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/devices

GET/devices/{device_id}devices:read
  • device_id stringrequired

https://stage-app.sensoitgps.com/api/v1/devices/{device_id}

GET/devices/{device_id}/alert-settingsdevices:read
  • device_id stringrequired

https://stage-app.sensoitgps.com/api/v1/devices/{device_id}/alert-settings

GET/devices/{device_id}/geofencesgeofences:read
  • device_id stringrequired

https://stage-app.sensoitgps.com/api/v1/devices/{device_id}/geofences

Positions

GET/positions/historypositions:read
  • valid_only boolean — Drop frames the quality filter rejected (no fix, implausible jumps). Leave on unless you are debugging.
  • device_id stringrequired — The device to read. Either this or `imei`.
  • imei string — The device to read, by IMEI. Either this or `device_id`.
  • from string — Start of the window, ISO 8601. Defaults to 24 hours before `to`. The span is capped at 31 days by moving this forward, never by moving `to` back.
  • to string — End of the window, ISO 8601. Defaults to now.
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/positions/history

GET/positions/latestpositions:read
  • if-none-match stringrequired
  • If-None-Match string — The ETag from a previous response. Returns 304 with no body when nothing has moved, and a 304 does not count against the daily quota.
  • device_id string — Narrow to one device. Omit for every device the key can see.
  • imei string — Narrow to one device by IMEI, instead of `device_id`.
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/positions/latest

Trips

GET/tripstrips:read
  • device_id string — Narrow to one device. Omit for every device the key can see.
  • imei string — Narrow to one device by IMEI, instead of `device_id`.
  • from string — Start of the window, ISO 8601. Defaults to 24 hours before `to`. The span is capped at 31 days by moving this forward, never by moving `to` back.
  • to string — End of the window, ISO 8601. Defaults to now.
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/trips

Alerts

GET/alertsalerts:read
  • acknowledged string — Only acknowledged, or only un-acknowledged. Omit for both.
  • severity string
  • alert_type string — Exact alert type, e.g. `speeding`. Not validated against a list: an unknown type matches nothing and returns an empty page.
  • device_id string — Narrow to one device. Omit for every device the key can see.
  • imei string — Narrow to one device by IMEI, instead of `device_id`.
  • from string — Start of the window, ISO 8601. Defaults to 24 hours before `to`. The span is capped at 31 days by moving this forward, never by moving `to` back.
  • to string — End of the window, ISO 8601. Defaults to now.
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/alerts

Sensors

GET/sensors/{kind}sensors:read
  • kind stringrequired
  • device_id string — Narrow to one device. Omit for every device the key can see.
  • imei string — Narrow to one device by IMEI, instead of `device_id`.
  • from string — Start of the window, ISO 8601. Defaults to 24 hours before `to`. The span is capped at 31 days by moving this forward, never by moving `to` back.
  • to string — End of the window, ISO 8601. Defaults to now.
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/sensors/{kind}

Geofences

GET/geofence-eventsgeofences:read
  • device_id string — Narrow to one device. Omit for every device the key can see.
  • imei string — Narrow to one device by IMEI, instead of `device_id`.
  • from string — Start of the window, ISO 8601. Defaults to 24 hours before `to`. The span is capped at 31 days by moving this forward, never by moving `to` back.
  • to string — End of the window, ISO 8601. Defaults to now.
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/geofence-events

GET/geofencesgeofences:read
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/geofences

Sub-accounts

GET/usersusers:read
  • limit integer — Rows per page. Max 500.
  • cursor string — Opaque cursor from a previous response’s `next_cursor`. Do not construct or parse it.

https://stage-app.sensoitgps.com/api/v1/users

GET/users/{user_id}users:read
  • user_id stringrequired

https://stage-app.sensoitgps.com/api/v1/users/{user_id}

Reports

GET/reports/mileagereports:read
  • device_id string — Narrow to one device. Omit for every device the key can see.
  • imei string — Narrow to one device by IMEI, instead of `device_id`.
  • from string — Start of the window, ISO 8601. Defaults to 24 hours before `to`. The span is capped at 31 days by moving this forward, never by moving `to` back.
  • to string — End of the window, ISO 8601. Defaults to now.

https://stage-app.sensoitgps.com/api/v1/reports/mileage

Migrating from ProTrack

If your integration already speaks ProTrack’s Open API, these are the equivalents. The biggest difference is authentication: there is no /authorization call and no two-hour token to refresh — the API key is sent directly on every request.

ProTrackHere
/api/authorizationNot needed — send the key itself
/api/trackGET /positions/latest
/api/playbackGET /positions/history
/api/device/listGET /devices
/api/device/detailGET /devices/{device_id}
/api/device/mileageGET /reports/mileage
/api/alarm/list2GET /alerts
/api/geofence/listGET /geofences
/api/device/geofenceGET /devices/{device_id}/geofences
/api/device/alertsettings/getGET /devices/{device_id}/alert-settings
/api/user/listGET /users
/api/user/infoGET /users/{user_id}

Their write and command endpoints — creating geofences, changing alarm settings, blocking accounts, sending commands — have no equivalent here yet. See Not in v1.

Devices can be addressed by imei exactly as they are there, so an existing IMEI-keyed integration does not need a mapping step.

Scopes reference

A key carries a set of scopes chosen when it was created. A request to an endpoint whose scope the key lacks returns 403 INSUFFICIENT_SCOPE and names the missing one.

ScopeGrants
devices:readVehicle list and detail, alarm settings
positions:readLive positions and position history
trips:readTrips
alerts:readAlarms and events
sensors:readFuel, temperature and tyre readings
geofences:readGeofences, their assignments and crossings
users:readSub-account list and detail
reports:readMileage and usage summaries

Grant only what the integration needs. A key limited to positions:read cannot read your user list even if it is stolen.

Not in v1

This version is read-only. Sending commands to a device, video and camera media, webhooks and push streams are not part of it.

Nothing documented here will be removed or changed in a breaking way. New versions get a new path prefix and /v1 keeps working.