Skip to main content

Sending telemetry

This guide describes the telemetry contract in full: what you send, how IFMS processes it and what comes back. For your first call, see Send your first batch. The telemetry API reference shows the published contract.

The request

POST https://gw.kairosinnovations.dev/api/v1/ingest/telemetry
Authorization: Bearer <access_token>
Content-Type: application/json
  • The body is UTF-8 JSON. Field names use snake_case.
  • Send your token as described in Access tokens.
  • Never send your provider identity: no provider_code or provider_name in the body, and no identity headers. The gateway adds your identity from your approved application.

The envelope

Every batch is wrapped in a data object:

{
"data": {
"positions": [
{ "vehicle_plate_number": "AA-12345", "recorded_at": "2026-08-05T20:04:00Z", "latitude": 9.01, "longitude": 38.77 }
]
}
}
FieldTypeRequiredRule
dataobjectYesMust not be null.
data.positionsarrayYesAt least one position. Version 1 sets no maximum number of positions.

A batch has no other fields. Each position is one GPS fix for one vehicle at one moment. You can put several vehicles, or several fixes for one vehicle, in the same batch.

Position fields

FieldTypeRequiredRule
vehicle_plate_numberstringYesNot blank. At most 120 characters. IFMS changes it to uppercase and removes all whitespace.
recorded_atISO 8601 date and timeYesThe time your device recorded the fix, with an explicit offset. UTC (Z) is recommended. Must be inside the observation window below.
latitudenumberYesWGS 84 decimal degrees, from −90 to 90 inclusive.
longitudenumberYesWGS 84 decimal degrees, from −180 to 180 inclusive.
ignition_onbooleanNoEngine or ignition state, if your device reports it. It helps IFMS tell idling from parked.
device_imeistringNoAt most 32 characters. Used for audit and correlation only, never as the vehicle's identity.

A position with both optional fields:

{"data":{"positions":[
{"vehicle_plate_number":"AA-12345","recorded_at":"2026-08-05T20:04:00Z",
"latitude":9.005401,"longitude":38.763611,"ignition_on":true,
"device_imei":"356938035643809"}]}}

The timestamps in these examples are illustrations. Send the real time your device recorded each position.

What not to send

Do not send any of these as fields:

  • provider identity;
  • vehicle or driver master data;
  • speed, heading or movement state;
  • geofence or reverse-geocoding values;
  • message identifiers or batch sequence numbers;
  • odometer, altitude or accuracy values;
  • other vendor-specific values.

IFMS works out speed, heading and movement state itself, from your successive positions. IFMS ignores fields it does not recognise, so never rely on one being processed.

The observation window

IFMS accepts a position only if recorded_at is between:

  • IFMS server time minus 30 days, and
  • IFMS server time plus 5 minutes.

A position outside the window does not fail the batch. It comes back in failures with the reason RECORDED_AT_OUT_OF_RANGE. Keep your device and server clocks accurate.

How IFMS processes a batch

RuleWhat happens
Each position on its ownIFMS checks every position separately. An invalid position is counted and listed in failures. The valid positions in the same batch are still stored.
The counts always add upaccepted + duplicates + rejected equals the number of positions you sent. failures has one entry for every duplicate or rejected position.
DuplicatesA position is a duplicate when IFMS already stored one with the same provider identity, normalised plate and recorded_at, compared to the microsecond.
The same plate from another providerIt counts as a separate vehicle. It is not a duplicate.
OrderAn older position that is still inside the window is stored as history. It does not move the vehicle's live state backwards.
A plate IFMS has not seen beforeIFMS registers the vehicle as unverified and checks it against the vehicle registry later, in the background. The check never blocks acceptance: a position is not rejected because its plate is new or not yet verified.

Retrying safely

To retry, resend the original position with its original recorded_at. recorded_at is part of the duplicate key, so never refresh it on a retry. If IFMS already stored the position, the retry comes back as a DUPLICATE, and nothing is stored twice.

The 202 response

A valid batch returns HTTP 202 Accepted. 202 does not mean every position was stored. Always check the counts and every entry in data.failures.

This batch had two positions. The first was stored and the second was rejected:

{
"header": {
"operation": "POST",
"request_uri": "/api/v1/ingest/telemetry",
"response_code": 202,
"response_message": "SUCCESS",
"additional_details": ""
},
"data": {
"provider_code": "<your provider code>",
"provider_name": "<your organisation name>",
"accepted": 1,
"duplicates": 0,
"rejected": 1,
"failures": [
{
"index": 1,
"vehicle_plate_number": "AA-67890",
"recorded_at": "2026-08-05T20:04:30Z",
"outcome": "REJECTED",
"reason": "INVALID_FIELDS",
"errors": [
{
"field": "latitude",
"message": "must be less than or equal to 90.0",
"rejected_value": "91"
}
]
}
]
}
}

Response fields

FieldMeaning
provider_codeYour stable provider identity, taken from your approved application. You never send it.
provider_nameYour organisation's name. It can be absent.
acceptedNew, valid positions that IFMS stored.
duplicatesPositions IFMS had already stored.
rejectedPositions that were not stored because they failed validation, the time window, parsing or storage.
failuresOne entry for every duplicate or rejected position, in the order you sent them. Empty when every position was accepted.

Failure entry fields

Fields with no value are left out.

FieldMeaning
indexThe position's place in the positions array you sent, counting from 0.
vehicle_plate_numberThe plate as you sent it. Absent when it could not be read.
recorded_atThe timestamp as you sent it. Absent when it could not be parsed.
outcomeREJECTED or DUPLICATE.
reasonA code you can act on. See the table below.
messageOptional text for a person to read, for parse, time-window or internal errors. Do not branch on it.
errorsFor INVALID_FIELDS only: one entry per wrong field, with field, message and rejected_value.

Failure reasons

ReasonMeaningWhat to do
UNPARSEABLEA value or type could not be read.Correct the position and resend only that position.
INVALID_FIELDSOne or more fields broke a rule.Use errors to correct the fields. Resend only that position.
RECORDED_AT_OUT_OF_RANGEThe timestamp is more than 30 days old or more than 5 minutes ahead.Fix your device clock or backlog handling. Resend with the true observation time.
DUPLICATEIFMS had already stored this position.Nothing. The retry was safe.
INTERNAL_ERRORIFMS could not store this one position.Retry the same position with controlled backoff and the same recorded_at.

The set of failure reasons is additive: new reasons can appear. Treat reason and outcome values as plain strings, and handle a value you do not recognise: log it as it is instead of failing. See Changes to the API.

Reading the counts

CountsWhat happenedWhat to do
accepted 2, duplicates 0, rejected 0Two new positions stored. failures is empty.Nothing.
accepted 0, duplicates 1, rejected 0A safe replay. One DUPLICATE entry.Nothing. Mark the original as delivered.
accepted 1, duplicates 0, rejected 1One stored, one rejected and listed.Follow the reason. Resend only the corrected position.
accepted 0, duplicates 0, rejected 1Nothing stored. One rejected and listed.Correct or retry, as the reason says.

Other responses

Troubleshooting and FAQ lists every other response from the telemetry gateway, such as 400, 401, 403 and 5xx, and what to do about each. For a 401, also see Access tokens.

Handling failures in your client

Your client is ready when it:

  • checks that accepted + duplicates + rejected equals the number of positions it sent;
  • handles every entry in failures by its index and reason;
  • accepts reasons and response fields it does not recognise;
  • retries only the affected positions, with their original recorded_at and controlled backoff.

The sample clients on Send your first batch print one line for every entry in failures: see print_failures in the cURL sample and report in the Python, JavaScript and Java samples.