Telemetry Ingestion
Ingest batches of GPS and sensor data from tracking devices.
/apidev/v1/telemetryOverview
Accepts arrays of GPS and sensor data points from tracking devices. Uses a fire-and-forget pattern: the server validates the payload, returns 200 OK with an acceptance count, and processes the data asynchronously in the background.
A successful response confirms the points were accepted for processing — not that they have been persisted yet.
Request
Body
The request body is a JSON array of telemetry point objects:
| Field | Type | Required | Max Length | Description |
|---|---|---|---|---|
imei | string | Yes | 50 | Device IMEI identifier |
eventcode | string | Yes | 20 | Event code — see Event codes table below |
fix | boolean | Yes | — | Whether the GPS has a valid fix |
datetime | string | Yes | 40 | Timestamp of the reading (ISO 8601, no timezone) |
latitude | string | Yes | 25 | Latitude in decimal degrees |
longitude | string | Yes | 25 | Longitude in decimal degrees |
altitude | number | Yes | — | Altitude in meters |
speed | number | Yes | — | Speed in km/h |
heading | number | Yes | — | Heading in degrees (0–360) |
satellites | integer | No | — | Number of visible GPS satellites |
ignition | boolean | No | — | Ignition state |
accuracy | string | No | 30 | GPS accuracy in meters |
odometer | string | No | 50 | Odometer reading in km |
horometer | number | No | — | Hour meter reading |
address | string | No | 500 | Reverse-geocoded address |
Event Codes
The eventcode field must be one of the following values:
| Code | Event | Description |
|---|---|---|
101 | POSICION | Periodic position report |
113 | POSICION_STANDBY | Position report in standby mode |
116 | BATTERY_LOW | Low battery alert |
117 | EXCESO_VELOCIDAD | Speed limit exceeded |
118 | IGNICION_ON | Ignition turned on |
119 | IGNICION_OFF | Ignition turned off |
120 | POWER_ON | External power connected |
121 | POWER_OFF | External power disconnected |
122 | INPUT01_ON | Input 1 activated |
123 | INPUT01_OFF | Input 1 deactivated |
124 | INPUT02_ON | Input 2 activated |
125 | INPUT02_OFF | Input 2 deactivated |
126 | INPUT03_ON | Input 3 activated |
127 | INPUT03_OFF | Input 3 deactivated |
128 | OUTPUT01_ON | Output 1 activated |
129 | OUTPUT01_OFF | Output 1 deactivated |
130 | OUTPUT02_ON | Output 2 activated |
131 | OUTPUT02_OFF | Output 2 deactivated |
132 | OUTPUT03_ON | Output 3 activated |
133 | OUTPUT03_OFF | Output 3 deactivated |
149 | REMOLQUE_INICIO | Trailer attached |
150 | REMOLQUE_FIN | Trailer detached |
151 | BATERIA_EXT_LOW | External battery low |
152 | BATERIA_INT_LOW | Internal battery low |
153 | POWER_UP | Device power up |
162 | COLISION | Collision detected |
163 | ACELERACIONBRUSCA | Harsh acceleration |
164 | FRENADOBRUSCO | Harsh braking |
165 | GIROBRUSCO | Harsh cornering |
Unknown fields in the body are rejected. Only the fields listed above are accepted — any extra property triggers a VALIDATION_ERROR.
Code Examples
- cURL
- JavaScript
- Python
curl -s -X POST "https://$TENANT/apidev/v1/telemetry" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '[
{
"imei": "352093081234567",
"eventcode": "101",
"fix": true,
"datetime": "2026-04-04T14:30:00",
"latitude": "-34.6037",
"longitude": "-58.3816",
"altitude": 25,
"speed": 45,
"heading": 180,
"satellites": 12,
"ignition": true,
"odometer": "125430",
"accuracy": "3.5"
},
{
"imei": "352093081234567",
"eventcode": "101",
"fix": true,
"datetime": "2026-04-04T14:30:30",
"latitude": "-34.6040",
"longitude": "-58.3820",
"altitude": 25,
"speed": 48,
"heading": 185
}
]'
const points = [
{
imei: "352093081234567",
eventcode: "101",
fix: true,
datetime: "2026-04-04T14:30:00",
latitude: "-34.6037",
longitude: "-58.3816",
altitude: 25,
speed: 45,
heading: 180,
satellites: 12,
ignition: true,
odometer: "125430",
accuracy: "3.5",
},
{
imei: "352093081234567",
eventcode: "101",
fix: true,
datetime: "2026-04-04T14:30:30",
latitude: "-34.6040",
longitude: "-58.3820",
altitude: 25,
speed: 48,
heading: 185,
},
];
const response = await fetch(`https://${TENANT}/apidev/v1/telemetry`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(points),
});
const { data } = await response.json();
console.log(`Accepted: ${data.accepted}, Rejected: ${data.rejected}`);
points = [
{
"imei": "352093081234567",
"eventcode": "101",
"fix": True,
"datetime": "2026-04-04T14:30:00",
"latitude": "-34.6037",
"longitude": "-58.3816",
"altitude": 25,
"speed": 45,
"heading": 180,
"satellites": 12,
"ignition": True,
"odometer": "125430",
"accuracy": "3.5",
},
{
"imei": "352093081234567",
"eventcode": "101",
"fix": True,
"datetime": "2026-04-04T14:30:30",
"latitude": "-34.6040",
"longitude": "-58.3820",
"altitude": 25,
"speed": 48,
"heading": 185,
},
]
response = requests.post(
f"https://{TENANT}/apidev/v1/telemetry",
headers={**headers, "Content-Type": "application/json"},
json=points,
)
result = response.json()
print(f"Accepted: {result['data']['accepted']}")
Response
Success — 200 OK
| Field | Type | Description |
|---|---|---|
success | boolean | true — points accepted for async processing |
data.accepted | integer | Number of telemetry points accepted |
data.rejected | integer | Number of points rejected (currently always 0 on success) |
data.status | string | Processing status ("OK") |
meta | object | Empty object |
{
"success": true,
"data": {
"accepted": 2,
"rejected": 0,
"status": "OK"
},
"meta": {}
}
A 200 OK confirms the points passed validation and were enqueued. Processing happens asynchronously — data may not be queryable via the Device Position endpoint immediately.
Rate Limiting
Telemetry uses a token bucket algorithm at two levels. See Rate Limits for the full explanation.
| Parameter | Global (per user) | Per IMEI |
|---|---|---|
| Bucket capacity | 180 tokens | 90 tokens |
| Refill rate | 30 tokens/sec | 15 tokens/sec |
| Cost per request | ceil(points / 20) tokens | ceil(points / 20) tokens |
Examples:
- 1 point = 1 token → you can send ~180 single-point requests before the bucket empties
- 100 points = 5 tokens → you can send ~36 batches of 100 before the bucket empties
- Sustained throughput: ~600 points/sec globally, ~300 points/sec per device
Errors
| Code | HTTP | Description |
|---|---|---|
BAD_REQUEST | 400 | Missing required headers |
VALIDATION_ERROR | 400 | Invalid or missing fields, unknown properties, or body is not an array |
UNAUTHORIZED | 401 | Invalid or expired JWT / API Key |
RATE_LIMITED | 429 | Token bucket exhausted (global or per-IMEI) |
INTERNAL_ERROR | 500 | Unexpected server error during ingestion |
Best Practices
- Batch your points — send up to 100 points per request for optimal throughput-to-token-cost ratio
- One IMEI per batch when possible — avoids per-IMEI bucket contention across devices
- Monitor rate limit headers — check
X-RateLimit-Remainingto throttle before hitting 429 - Timestamps without timezone — send as
"2026-04-04T14:30:00", not"2026-04-04T14:30:00Z" - GPS fix = false — the server accepts the point but coordinates may be unreliable
Related
- Devices API — Query device positions after ingestion
- Rate Limits — Token bucket algorithm details
- Authentication — JWT + API Key dual-auth model