Messaging & Nearby
Send a free-text message to the mobile app running on a vehicle, and find the mobiles closest to a given point — useful for assigning the nearest unit to a job.
All endpoints require a valid JWT token, API key, and tenant header. See Authentication.
Send Message
Queue a free-text message to the mobile app of a single vehicle. This is fire-and-forget: a success response means the message was queued, not that it was delivered or read. The mobile app picks it up the next time it polls for messages.
/apidev/v1/devices/{vehicle_id}/messagesPath Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
vehicle_id | string | Yes | Vehicle (mobile) unique identifier. Must belong to your tenant. |
Request Body
| Field | Type | Required | Max Length | Description |
|---|---|---|---|---|
text | string | Yes | 500 | Message to send to the mobile. Cannot be empty. |
push | boolean | No | — | Trigger a push notification (default true). |
Today every queued message also fires a push notification, so push: false is accepted but not yet honored. Send true (or omit it) to be future-proof.
Response Fields
| Field | Type | Description |
|---|---|---|
message_id | string | Identifier of the queued message (BigInt as string). May be omitted if the queue did not return one. |
vehicle_id | string | Echo of the target mobile identifier. |
delivery | string | Always "queued" — the message was accepted into the delivery queue. Queued is not the same as delivered or read. |
Code Example
- cURL
- JavaScript
- Python
curl -s -X POST "https://$TENANT/apidev/v1/devices/1013/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{
"text": "Head back to base, end of shift."
}'
const response = await fetch(
`https://${TENANT}/apidev/v1/devices/1013/messages`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"X-API-Key": API_KEY,
"tenant": TENANT,
"Content-Type": "application/json",
},
body: JSON.stringify({ text: "Head back to base, end of shift." }),
}
);
const { data } = await response.json();
console.log(`Message ${data.message_id} → ${data.delivery}`);
response = requests.post(
f"https://{TENANT}/apidev/v1/devices/1013/messages",
headers={**headers, "Content-Type": "application/json"},
json={"text": "Head back to base, end of shift."},
)
data = response.json()["data"]
print(f"Message {data['message_id']} → {data['delivery']}")
Example Response
{
"success": true,
"data": {
"message_id": "8472910",
"vehicle_id": "1013",
"delivery": "queued"
},
"meta": {}
}
If the vehicle has no active device that can receive messages (no active IMEI), the request fails with 409 CONFLICT. This is different from 404, which means the vehicle does not exist in your tenant. Associate an active device with the mobile before sending it messages.
Nearby Mobiles
Find the mobiles closest to a point, sorted by distance. The search is bounded by a radius (up to 50 km) and can be narrowed by GPS freshness, GPS validity, and vehicle type.
/apidev/v1/devices/nearbyThis is a read in spirit, but it carries a structured body (a point plus filters) that does not fit cleanly into query parameters, and it performs a distance computation. It follows the same convention as the report endpoints, which also accept a POST body.
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
point | object | Yes | — | Center of the search: { "latitude": number, "longitude": number }. latitude in [-90, 90], longitude in [-180, 180]. |
radius_m | number | Yes | — | Search radius in meters. Min 1, max 50000 (50 km). |
limit | number | No | 20 | Maximum results. Min 1, max 100. |
only_valid_gps | boolean | No | true | Only mobiles whose last GPS fix is valid (coordinates present, position valid). |
max_age_min | number | No | — | Drop mobiles whose last GPS fix is older than N minutes. Min 1, max 1440. When omitted, no age limit is applied. |
vehicle_types | string[] | No | — | Filter by vehicle type identifiers (BigInt as strings). Up to 100 values. |
Response Fields
The data field is an array of nearby mobiles, ordered by distance ascending.
| Field | Type | Description |
|---|---|---|
vehicle_id | string | Vehicle (mobile) identifier (BigInt as string). |
name | string | Mobile display name. |
plate | string | License plate. |
vehicle_type_id | string | null | Vehicle type identifier (BigInt as string). |
point | object | Last GPS point: { "latitude": number, "longitude": number }. |
distance_m | number | Distance from the requested point, in meters (rounded). |
heading | number | null | Heading in degrees (0–359), or null when not available. |
speed | number | Speed in km/h from the last GPS point. |
last_signal | string | null | Timestamp of the last valid GPS fix (without timezone). |
valid_gps | boolean | Whether the last GPS fix is valid. |
heading is null when no heading is available — never 0, which would mean "due north". last_signal is returned without timezone (e.g., "2026-06-25T14:58:00") and represents the company's configured timezone; display it as-is, without appending Z or converting to UTC.
Code Example
- cURL
- JavaScript
- Python
curl -s -X POST "https://$TENANT/apidev/v1/devices/nearby" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{
"point": { "latitude": -34.9050, "longitude": -56.1910 },
"radius_m": 5000,
"limit": 10,
"only_valid_gps": true,
"max_age_min": 30,
"vehicle_types": ["5"]
}'
const response = await fetch(
`https://${TENANT}/apidev/v1/devices/nearby`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"X-API-Key": API_KEY,
"tenant": TENANT,
"Content-Type": "application/json",
},
body: JSON.stringify({
point: { latitude: -34.9050, longitude: -56.1910 },
radius_m: 5000,
limit: 10,
only_valid_gps: true,
max_age_min: 30,
vehicle_types: ["5"],
}),
}
);
const { data, meta } = await response.json();
console.log(`${meta.total} mobiles within range`);
for (const m of data) {
console.log(`${m.name} — ${m.distance_m} m`);
}
response = requests.post(
f"https://{TENANT}/apidev/v1/devices/nearby",
headers={**headers, "Content-Type": "application/json"},
json={
"point": {"latitude": -34.9050, "longitude": -56.1910},
"radius_m": 5000,
"limit": 10,
"only_valid_gps": True,
"max_age_min": 30,
"vehicle_types": ["5"],
},
)
result = response.json()
for m in result["data"]:
print(f"{m['name']} — {m['distance_m']} m")
Example Response
{
"success": true,
"data": [
{
"vehicle_id": "1013",
"name": "Grua 07",
"plate": "ABC 1234",
"vehicle_type_id": "5",
"point": { "latitude": -34.9012, "longitude": -56.1888 },
"distance_m": 342,
"heading": 270,
"speed": 0,
"last_signal": "2026-06-25T14:58:00",
"valid_gps": true
},
{
"vehicle_id": "1021",
"name": "Grua 12",
"plate": "DEF 5678",
"vehicle_type_id": "5",
"point": { "latitude": -34.9100, "longitude": -56.1950 },
"distance_m": 820,
"heading": 135,
"speed": 34,
"last_signal": "2026-06-25T14:59:30",
"valid_gps": true
}
],
"meta": {
"total": 2,
"limit": 10
}
}
If no mobile falls within the radius — or all of them are filtered out by only_valid_gps, max_age_min, or visibility rules — the response is 200 with an empty data array. Each mobile is also subject to the visibility rules of your technical user, so you only see the mobiles that user is allowed to see.
The maximum radius is 50 km. A larger radius_m is rejected with 400 rather than silently clamped, to prevent an accidental scan of the whole fleet.
Errors
See Error Handling for the full reference.
| Code | HTTP | Applies to | Description |
|---|---|---|---|
VALIDATION_ERROR | 400 | Send Message, Nearby | Empty text; or point out of range, radius_m missing or above 50000, limit above 100. |
UNAUTHORIZED | 401 | All | Missing, invalid, or expired tenant / Authorization / X-API-Key. |
FORBIDDEN | 403 | All | User lacks APICLI_DEVICE_MESSAGE (Send Message) or APICLI_FLEET_DEVICES_READ (Nearby). |
NOT_FOUND | 404 | Send Message | The vehicle_id does not exist or does not belong to your tenant. |
CONFLICT | 409 | Send Message | The mobile has no active device that can receive messages (no active IMEI). |
RATE_LIMITED | 429 | All | Exceeded 20 req/min. |
INTERNAL_ERROR | 500 | All | Unexpected server error. |
Related
- Devices API — List, inspect, locate, and update mobiles in your fleet
- Drivers API — Manage drivers assigned to devices
- Error Handling — Standard error envelope and codes
- Rate Limits — How request limits are enforced