System Architecture & Integration Topology
Enterprise Platform Architecture
The Fleet Telematics System is a cloud-native, multi-tenant live GPS tracking platform engineered for high-throughput telematics ingestion, spatial boundary processing, and real-time state synchronization. External ERP systems integrate across three distinct data planes:
1. Ingestion Engine (:9000 TCP)
Asynchronously parses Sinotrack H02 ASCII datagrams. Employs in-memory negative-hit IMEI caching for sub-millisecond lookups, rejecting unauthorized devices before touching the database.
2. Spatial & Geofence Engine
Built on PostgreSQL with PostGIS geography point geometry. Computes geodetic distances on WGS84 ellipsoids (ST_DWithin) to trigger atomic enter and exit transitions.
3. ERP Stream Multiplexer
Stateless, cryptographically bounded WebSocket channel. Enforces tenant boundaries via signed session JWT tokens embedding whitelisted vehicle IDs.
5-Minute ERP Integration Quickstart
Follow this streamlined 5-step implementation lifecycle to register your ERP client, discover vehicles, mint a session token, and stream live telematics updates.
Self-Register or Rotate API Key
The ERP registers itself using the customer admin's dashboard credentials. Re-calling this endpoint with the same erpClientId instantly rotates the key and invalidates the previous one.
curl -X POST https://api.yourtrackinghost.com/api/integration/register \
-H "Content-Type: application/json" \
-d '{
"erpClientId": "odoo-production",
"email": "admin@acme-logistics.com",
"password": "CustomerAdminPassword123!"
}'
{
"customerId": 2,
"erpClientId": "odoo-production",
"apiKey": "fk_9d4e5f6a7b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c"
}
Retrieve Vehicle Catalog & Map Internal IDs
Query the customer's fleet directory to match ERP delivery vehicles against system tracking IDs and device IMEIs.
curl -X GET https://api.yourtrackinghost.com/api/integration/vehicles \
-H "Authorization: Bearer fk_9d4e5f6a7b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c"
[
{ "id": 2, "name": "Van 12 - North Delivery", "plate": "BK-4412", "imei": "867421030123456" },
{ "id": 3, "name": "Van 04 - Express Hub", "plate": "BK-4401", "imei": "867421030123457" },
{ "id": 4, "name": "Heavy Truck 02", "plate": "TL-8821", "imei": "867421030123458" }
]
Mint Short-Lived WebSocket Session Token
Mint a scoped session token embedding the specific array of vehicle IDs to track. You can specify a custom TTL (30s to 86,400s).
curl -X POST https://api.yourtrackinghost.com/api/integration/session \
-H "Content-Type: application/json" \
-d '{
"erpClientId": "odoo-production",
"apiKey": "fk_9d4e5f6a7b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c",
"vehicleIds": [2, 3, 4],
"sessionLengthSeconds": 600
}'
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJraW5kIjoiaW50ZWdyYXRpb24iLCJjaWQiOjIsInZpZHMiOlsyLDMsNF0sImlhdCI6MTcyNDIxODAwMCwiZXhwIjoxNzI0MjE4NjAwfQ.xyz...",
"vehicleIds": [2, 3, 4],
"expiresIn": 600,
"expiresAt": "2026-08-21T10:30:00.000Z",
"wsUrl": "/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Connect WebSocket & Stream Real-Time Frames
Open a persistent WebSocket connection to wss://api.yourtrackinghost.com/ws?token=<token>. Receive an immediate initial snapshot frame, followed by live position and alert pushes.
Authentication & Security Architecture
Two-Tier Security Architecture
The platform combines long-lived tenant API keys with short-lived cryptographically signed session JWT tokens to guarantee isolation and zero trust on WebSocket channels.
| Credential Type | Format / Pattern | Storage / Cryptography | Lifecycle / Expiration | Scope & Boundary |
|---|---|---|---|---|
| Integration API Key | fk_<48 hex chars> |
One-way SHA-256 hash in integration_keys |
Persistent until revoked or auto-rotated | Bound to customer tenant & client_id |
| Session JWT Token | Stateless HMAC-SHA256 JWT | Signed with server JWT_SECRET |
Configurable TTL (30s – 86,400s) | Whitelist array of vehicle IDs (vids) |
| User Dashboard JWT | Stateless HMAC-SHA256 JWT | Signed with server JWT_SECRET |
7 Days (expiresIn: '7d') |
User role (super_admin, admin, user) |
HTTP Status Codes & Security Errors
| Status Code | Error Payload | Root Cause & Mitigation |
|---|---|---|
400 Bad Request |
{"error":"vehicleIds required"} |
Request body omitted the vehicleIds array or passed an empty list. Max limit is 500 IDs per request. |
401 Unauthorized |
{"error":"invalid or revoked integration key"} |
Supplied API key is invalid, improperly formatted, or has been revoked in the database. |
401 Unauthorized |
{"error":"invalid admin credentials"} |
On /api/integration/register, the supplied email or password does not match a tenant admin user. |
403 Forbidden |
{"error":"erpClientId does not match integration key"} |
The erpClientId passed in the session request does not match the bound client_id of the API key. |
403 Forbidden |
{"error":"no allowed vehicles"} |
None of the requested vehicle IDs belong to the customer tenant associated with this API key. |
4001 WS Close |
WebSocket Close Code 4001 |
WebSocket connection attempted with a missing, malformed, or expired session JWT token. |
How to Mint & Manage Integration API Keys
Two Methods for Minting Integration API Keys
Every customer tenant has isolated access to their own vehicle fleet. Integration API keys (format: fk_<48 hex chars>) are bound to both a customer tenant and a unique ERP client identifier (client_id). Only the SHA-256 hash is stored on the server.
ERP developers can programmatically mint their own API key by sending their desired erpClientId along with the email and password of their customer tenant administrator.
curl -X POST https://api.yourtrackinghost.com/api/integration/register \
-H "Content-Type: application/json" \
-d '{
"erpClientId": "odoo-production-instance",
"email": "admin@yourcompany.com",
"password": "TenantAdminPassword123!"
}'
{
"customerId": 2,
"erpClientId": "odoo-production-instance",
"apiKey": "fk_4f8e2a1b9c3d7e5f608192a3b4c5d6e7f8a9b0c1d2e3f4a5"
}
POST /api/integration/register request with the same erpClientId generates a fresh key and immediately replaces the previous key hash in the database.
Administrators can visually generate and manage integration keys through the web dashboard:
- Log in to the web dashboard (
/login) as an administrator account. - In the navigation bar, navigate to Integration (
/admin/integration). - Under New API Key, enter:
- Label / Name: e.g.,
"Odoo Main ERP" - Client ID (erp_client_id): e.g.,
"odoo-prod"
- Label / Name: e.g.,
- Click Generate Key. Copy the raw key (
fk_...) immediately, as it is displayed only once.
Equivalently via the Admin REST API:
POST /api/integration/keys
Authorization: Bearer <admin-jwt-token>
Content-Type: application/json
{
"name": "Odoo Production",
"clientId": "odoo-prod"
}
HTTP/1.1 201 Created
{
"name": "Odoo Production",
"clientId": "odoo-prod",
"key": "fk_9d4e5f6a7b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c"
}
Key Revocation & Leak Recovery
If an API key is ever compromised, revoke it immediately via the Admin dashboard or by calling:
POST /api/integration/keys/:id/revoke
Authorization: Bearer <admin-jwt-token>
HTTP/1.1 204 No Content
Building a Live Telematics Map on Your Own ERP UI
Architecture: Secure Backend Proxy Pattern
To securely render live vehicle markers on your ERP browser interface without exposing your master API key, follow the Backend Session Token Proxy Pattern:
2. ERP Web Server calls
POST /api/integration/session using server-side apiKey (e.g. TTL = 300s).3. ERP Web Server embeds the short-lived
sessionToken into the frontend component.4. Browser opens
wss://api.yourtrackinghost.com/ws?token=<sessionToken>.5. Browser receives live positions, rotates vehicle heading markers, and renders real-time movement trails.
Interactive Frontend Components (Plain HTML, React, Angular)
Select your preferred frontend framework tab below for a full, copy-pasteable implementation:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ERP Live Fleet Tracking Map</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
body { display: flex; height: 100vh; background: #0f172a; color: #f8fafc; overflow: hidden; }
/* Sidebar */
#fleet-sidebar { width: 320px; background: #1e293b; border-right: 1px solid #334155; display: flex; flex-direction: column; z-index: 10; }
.sidebar-head { padding: 16px; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }
.sidebar-head h3 { font-size: 1rem; font-weight: 700; color: #38bdf8; }
.status-pill { font-size: 0.7rem; padding: 2px 8px; border-radius: 9999px; background: #065f46; color: #34d399; font-weight: 600; }
.status-pill.offline { background: #7f1d1d; color: #f87171; }
#vehicle-list { flex: 1; overflow-y: auto; padding: 12px; }
.vehicle-card { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 12px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
.vehicle-card:hover, .vehicle-card.active { border-color: #38bdf8; background: #172554; }
.vehicle-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 4px; display: flex; justify-content: space-between; }
.vehicle-meta { font-size: 0.75rem; color: #94a3b8; display: flex; justify-content: space-between; }
/* Map Area */
#map-container { flex: 1; position: relative; height: 100%; }
#map { width: 100%; height: 100%; background: #0b0f19; }
/* Directional Marker */
.marker-dot { width: 20px; height: 20px; border-radius: 50%; background: #38bdf8; border: 2px solid #ffffff; box-shadow: 0 0 10px rgba(56, 189, 248, 0.6); position: relative; display: flex; align-items: center; justify-content: center; }
.marker-dot.offline { background: #94a3b8; box-shadow: none; }
.marker-dot .arrow { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 10px solid #ffffff; position: absolute; top: -14px; transform-origin: 50% 24px; }
.pulse-ring { position: absolute; width: 100%; height: 100%; border-radius: 50%; animation: pulse 2s infinite; border: 2px solid #38bdf8; opacity: 0; }
@keyframes pulse { 0% { transform: scale(1); opacity: 0.8; } 100% { transform: scale(2.5); opacity: 0; } }
/* Movement Trail */
.leaflet-interactive.trail-line { stroke: #38bdf8; stroke-width: 3; stroke-dasharray: 6, 6; filter: drop-shadow(0 0 4px #0284c7); }
/* Alert Toast */
#alert-toast-container { position: absolute; top: 20px; right: 20px; z-index: 1000; display: flex; flex-direction: column; gap: 8px; }
.alert-toast { background: rgba(30, 41, 59, 0.95); border-left: 4px solid #f59e0b; color: #f8fafc; padding: 12px 16px; border-radius: 6px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.5); font-size: 0.85rem; }
</style>
</head>
<body>
<!-- Sidebar -->
<aside id="fleet-sidebar">
<div class="sidebar-head">
<h3>Fleet Telematics</h3>
<span id="conn-status" class="status-pill">Connecting...</span>
</div>
<div id="vehicle-list"></div>
</aside>
<!-- Map Container -->
<main id="map-container">
<div id="map"></div>
<div id="alert-toast-container"></div>
</main>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const CONFIG = {
wsBaseUrl: "ws://localhost:3000/ws",
sessionToken: "PASTE_YOUR_SESSION_JWT_TOKEN_HERE"
};
const state = {
vehicles: new Map(),
markers: new Map(),
trails: new Map(),
selectedId: null,
ws: null
};
// 1. Initialize Map
const map = L.map('map').setView([51.5074, -0.1278], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map);
// 2. Custom Marker Generator
function createMarkerIcon(heading = 0, isLive = true) {
const headingDeg = heading || 0;
const html = `
<div class="marker-dot ${isLive ? '' : 'offline'}">
${isLive ? '<div class="pulse-ring"></div>' : ''}
<div class="arrow" style="transform: rotate(${headingDeg}deg);"></div>
</div>
`;
return L.divIcon({ className: '', html, iconSize: [20, 20], iconAnchor: [10, 10] });
}
// 3. Render Vehicle Sidebar
function renderSidebar() {
const listEl = document.getElementById('vehicle-list');
listEl.innerHTML = '';
state.vehicles.forEach(v => {
const card = document.createElement('div');
card.className = `vehicle-card ${state.selectedId === v.id ? 'active' : ''}`;
const speedKmh = Math.round((v.speedKn || 0) * 1.852);
card.innerHTML = `
<div class="vehicle-title">
<span>${v.name || 'Vehicle #' + v.id}</span>
<span style="color:${v.valid ? '#34d399' : '#f87171'}">${speedKmh} km/h</span>
</div>
<div class="vehicle-meta">
<span>${v.plate || 'No Plate'}</span>
<span>${v.course || 0}° Heading</span>
</div>
`;
card.onclick = () => focusVehicle(v.id);
listEl.appendChild(card);
});
}
// 4. Focus Vehicle
function focusVehicle(vid) {
state.selectedId = vid;
renderSidebar();
const v = state.vehicles.get(vid);
if (v && v.lat && v.lon) {
map.flyTo([v.lat, v.lon], 16, { duration: 1.2 });
const m = state.markers.get(vid);
if (m) m.openPopup();
}
}
// 5. Update Position
function updateVehiclePosition(vid, pos, meta = {}) {
let v = state.vehicles.get(vid) || { id: vid, ...meta };
v = { ...v, ...pos };
state.vehicles.set(vid, v);
const latLng = [v.lat, v.lon];
let marker = state.markers.get(vid);
if (!marker) {
marker = L.marker(latLng, { icon: createMarkerIcon(v.course, v.valid) }).addTo(map);
marker.on('click', () => focusVehicle(vid));
state.markers.set(vid, marker);
} else {
marker.setLatLng(latLng);
marker.setIcon(createMarkerIcon(v.course, v.valid));
}
const speedKmh = Math.round((v.speedKn || 0) * 1.852);
marker.bindPopup(`
<div style="color:#0f172a; font-family:sans-serif;">
<strong style="font-size:1rem;">${v.name || 'Vehicle #' + vid}</strong><br>
<span style="color:#64748b;">Plate:</span> ${v.plate || '—'}<br>
<span style="color:#64748b;">Speed:</span> <strong>${speedKmh} km/h</strong><br>
<span style="color:#64748b;">Heading:</span> ${v.course || 0}°<br>
<span style="color:#64748b;">Coords:</span> ${v.lat.toFixed(5)}, ${v.lon.toFixed(5)}
</div>
`);
let trail = state.trails.get(vid);
if (!trail) {
trail = L.polyline([latLng], { className: 'trail-line' }).addTo(map);
state.trails.set(vid, trail);
} else {
trail.addLatLng(latLng);
}
renderSidebar();
}
// 6. Connect Realtime WebSocket
function connectStreaming(token) {
const statusEl = document.getElementById('conn-status');
const ws = new WebSocket(`${CONFIG.wsBaseUrl}?token=${token}`);
ws.onopen = () => {
statusEl.textContent = "Live";
statusEl.className = "status-pill";
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'snapshot') {
msg.positions.forEach(p => {
updateVehiclePosition(p.vehicle_id, {
lat: p.lat, lon: p.lon, speedKn: p.speed_kn, course: p.course, valid: p.valid
});
});
const pts = msg.positions.map(p => [p.lat, p.lon]);
if (pts.length) map.fitBounds(pts, { padding: [50, 50] });
} else if (msg.type === 'position') {
updateVehiclePosition(msg.vehicleId, msg.position);
}
} catch (e) { console.error("WS Parse error:", e); }
};
ws.onclose = () => {
statusEl.textContent = "Disconnected";
statusEl.className = "status-pill offline";
setTimeout(() => connectStreaming(token), 4000);
};
}
connectStreaming(CONFIG.sessionToken);
</script>
</body>
</html>
Complete REST API Specification
All REST API endpoints accept and return JSON payloads (Content-Type: application/json). Cross-Origin Resource Sharing (CORS) is enabled platform-wide with permissive headers (*) for cross-domain server and worker communication.
Self-service registration endpoint for 3rd-party ERP systems. Authenticates using tenant administrator email and password to issue or rotate an integration API key. If the erpClientId already exists, its API key hash is updated, immediately revoking any previous key.
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
erpClientId | String | Yes | Unique identifier for the ERP installation (e.g. "odoo-prod-east", "sap-wms-01"). |
email | String | Yes | Email address of an active tenant administrator with role admin. |
password | String | Yes | Plaintext password for the administrator account. |
curl -X POST https://api.yourtrackinghost.com/api/integration/register \
-H "Content-Type: application/json" \
-d '{
"erpClientId": "odoo-main",
"email": "roshan@test.com",
"password": "roshan123"
}'
Mints a short-lived, signed JWT session token authorized strictly for a whitelisted set of vehicle IDs. Supports two credential styles: standard Bearer token in the Authorization header, or direct apiKey in the JSON body.
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
erpClientId / clientId | String | Yes | Must match the client ID bound to the integration key. |
apiKey | String | Conditional | Required if not sending Authorization: Bearer fk_... header. |
vehicleIds | Array<Integer> | Yes | List of vehicle IDs to track (max 500 IDs per session). |
sessionLengthSeconds / ttlSeconds | Integer | No | Session duration in seconds (min 30, max 86400, default 300). |
curl -X POST https://api.yourtrackinghost.com/api/integration/session \
-H "Content-Type: application/json" \
-d '{
"erpClientId": "odoo-main",
"apiKey": "fk_7a2b9c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b7",
"vehicleIds": [2, 3, 4],
"sessionLengthSeconds": 600
}'
Lists all vehicles belonging to the customer tenant associated with the API key. Used by ERP synchronization jobs to map internal fleet assets to tracking IDs and device IMEIs.
curl -X GET https://api.yourtrackinghost.com/api/integration/vehicles \
-H "Authorization: Bearer fk_7a2b9c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b7"
HTTP/1.1 200 OK
Content-Type: application/json
[
{ "id": 2, "name": "Van 12", "plate": "BK-4412", "imei": "867421030123456" },
{ "id": 3, "name": "Van 04", "plate": "BK-4401", "imei": "867421030123457" },
{ "id": 4, "name": "Truck 2", "plate": "TL-8821", "imei": "867421030123458" }
]
Administrative management endpoints for creating, inspecting, and revoking API keys from the web dashboard or management scripts.
POST /api/integration/keys: Creates key with{"name":"...", "clientId":"..."}. Returnskeyonce.GET /api/integration/keys: Returns active and revoked keys with creation and revocation timestamps.POST /api/integration/keys/:id/revoke: Revokes key instantly (HTTP 204 No Content).
Lists vehicles accessible to the authenticated identity, including their most recent telemetry report and any target destination coordinates.
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"id": 2,
"name": "Van 12",
"plate": "BK-4412",
"imei": "867421030123456",
"destination": { "lat": 51.5150, "lon": -0.1410 },
"position": {
"id": 8842,
"recordedAt": "2026-08-21T10:15:30.123Z",
"deviceTime": "2026-08-21T10:15:22.000Z",
"valid": true,
"lat": 51.5074,
"lon": -0.1278,
"speedKn": 14.2,
"course": 88.5
}
}
]
Retrieves historical GPS trail records for route playback, speed profiling, and delivery compliance auditing.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
from | ISO Date / Epoch ms | Now - 6 hours | Start of temporal window. |
to | ISO Date / Epoch ms | Now | End of temporal window (max 30 days range). |
Assigns or clears navigation destination coordinates for a vehicle. Used by dispatch ERPs to broadcast dynamic drop-off targets.
// Set Destination:
PATCH /api/vehicles/2/destination
{ "lat": 51.5150, "lon": -0.1410 } -> HTTP 204 No Content
// Clear Destination:
PATCH /api/vehicles/2/destination
{ "clear": true } -> HTTP 204 No Content
Solves the Traveling Salesperson Problem (TSP) using greedy nearest-neighbor Haversine geodesics. Takes 2 to 50 waypoint coordinates and returns the optimized sequence index array and total kilometers.
POST /api/routes/optimize
{
"waypoints": [
[51.5074, -0.1278],
[51.5200, -0.1000],
[51.5100, -0.1500]
]
}
HTTP/1.1 200 OK
{
"order": [0, 2, 1],
"totalKm": 6.8,
"points": [
[51.5074, -0.1278],
[51.5100, -0.1500],
[51.5200, -0.1000]
]
}
Manages circular spatial geofences (center WGS84 point + radius in meters) and assigns vehicles to active monitoring zones.
GET /api/geofences: Lists all tenant geofences with centerlat,lon,radius_m, andvehicleIds.POST /api/geofences: Creates geofence with{"name":"Warehouse Zone", "lat":51.5074, "lon":-0.1278, "radiusM":500, "vehicleIds":[2,3]}.POST /api/geofences/:id/assign&DELETE /api/geofences/:id/assign: Links or unlinks vehicle with{"vehicleId": 2}.
Queries historical security and spatial boundary events (enter, exit, offline, online) for the authenticated tenant. Supports limit query parameter (max 200).
Enables tenant administrators to provision standard driver/viewer accounts and grant granular vehicle visibility permissions.
Exposes automated billing records generated by the periodic invoice scheduler (every 12 hours). Enables ERP accounting reconciliation and status updates.
Real-Time WebSocket Protocol Specification
WebSocket Connection Lifecycle
The platform provides low-latency push delivery over standard RFC 6455 WebSockets. ERP clients connect by appending their signed session token as a URL query parameter:
wss://api.yourtrackinghost.com/ws?token=<session-jwt-token>
WebSocket Message Frame Types
Provides the current last-known state of all vehicles whitelisted in the session token. Vehicles that have not yet reported are omitted. Note that raw DB snapshot keys use snake_case.
{
"type": "snapshot",
"positions": [
{
"vehicle_id": 2,
"id": 8841,
"recorded_at": "2026-08-21T10:14:02.123Z",
"device_time": "2026-08-21T10:13:55.000Z",
"valid": true,
"lat": 51.5074,
"lon": -0.1278,
"speed_kn": 12.4,
"course": 90.0
}
]
}
Pushed in real time whenever an incoming device datagram is ingested. Position frames use camelCase property formatting.
{
"type": "position",
"vehicleId": 2,
"position": {
"id": 8842,
"recordedAt": "2026-08-21T10:14:17.654Z",
"deviceTime": "2026-08-21T10:14:10.000Z",
"valid": true,
"lat": 51.5081,
"lon": -0.1269,
"speedKn": 13.7,
"course": 91.2
}
}
Pushed immediately when a spatial transition or stale timeout occurs. Alert type values: enter, exit, offline, online.
{
"type": "alert",
"alert": {
"id": 77,
"customer_id": 2,
"vehicle_id": 2,
"geofence_id": 5,
"type": "enter",
"message": "Van 12 entered Distribution Depot",
"lat": 51.5074,
"lon": -0.1278,
"created_at": "2026-08-21T10:14:17.654Z",
"resolved_at": null
}
}
Telemetry Units & Conversion Reference
| Field | Units | Conversion Formula | Description |
|---|---|---|---|
lat, lon |
Decimal Degrees (WGS84) | lat: ±90.0, lon: ±180.0 |
Geodetic coordinates. Negative values indicate South / West hemispheres. |
speedKn / speed_kn |
Knots (Nautical Miles/hr) | km/h = knots * 1.852mph = knots * 1.15078 |
Instantaneous ground speed calculated by the GPS chip. |
course |
Degrees (0.0° – 360.0°) | 0°=North, 90°=East, 180°=South, 270°=West |
True heading relative to true geographic North. |
valid |
Boolean (true | false) |
true = 3D Fix, false = Fix Lost |
When false, coordinates represent last known cached position. |
deviceTime |
ISO 8601 UTC | YYYY-MM-DDTHH:mm:ss.sssZ |
GPS satellite timestamp as reported by the tracker hardware. |
recordedAt |
ISO 8601 UTC | YYYY-MM-DDTHH:mm:ss.sssZ |
Ingestion timestamp when server received the frame. |
Webhook & Out-of-Band Notifications
Automated Event Webhook Delivery
In addition to WebSocket streaming, the telematics platform supports direct HTTP POST webhook dispatching to external ERP endpoints whenever geofence transitions or vehicle offline/online events occur.
Webhook JSON Payload (HTTP POST)
{
"type": "enter",
"message": "Van 12 entered Distribution Depot",
"vehicleId": 2,
"geofenceId": 5,
"lat": 51.5074,
"lon": -0.1278,
"at": "2026-08-21T10:14:17.654Z"
}
- Return an immediate
HTTP 200 OKresponse within 2,000ms. - Offload heavy ERP operations (e.g. updating order statuses, triggering customer SMS alerts) to a background worker queue (Celery, Laravel Queue, RabbitMQ).
- Design receiver idempotency using the combination of
vehicleId,type, andattimestamp.
Sinotrack (H02 Protocol) Telematics Ingest
Raw TCP Ingest Engine (:9000)
Physical tracking units (such as Sinotrack ST-901, ST-902, ST-906) broadcast NMEA ASCII frames over persistent or connectionless TCP connections to port 9000.
*HQ,867421030123456,V1,101410,A,5130.4440,N,00007.6140,W,013.70,091,210826,FFFFFBFF,234,15,0,0#
| Pos | Raw Value | Field Name | Description & Parsing Logic |
|---|---|---|---|
| 1 | *HQ | Header Marker | Fixed protocol identifier preamble. |
| 2 | 867421030123456 | Device IMEI | 8 to 15 digit hardware IMEI identifier. Looked up in memory cache. |
| 3 | V1 | Version Code | Protocol specification level. |
| 4 | 101410 | Time (HHMMSS) | 10:14:10 UTC satellite time. |
| 5 | A | Fix Validity | A = Valid GPS lock, V = Void / Cell-tower estimate. |
| 6–7 | 5130.4440, N | Latitude | 51° + (30.4440 / 60)' = 51.5074° N |
| 8–9 | 00007.6140, W | Longitude | 0° + (07.6140 / 60)' = -0.1269° W (Negative sign for W/S) |
| 10 | 013.70 | Speed (Knots) | 13.70 Knots (~25.37 km/h). |
| 11 | 091 | Heading | 91° True East heading. |
| 12 | 210826 | Date (DDMMYY) | August 21, 2026. |
Offline Stale-Vehicle Detector
The backend executes an offline evaluation tick every 60 seconds. If a vehicle has not emitted a valid report within OFFLINE_AFTER_MIN (default: 5 minutes), an offline alert is generated and dispatched via WebSocket and Webhook. Once the vehicle resumes transmission, the open offline alert is marked resolved (resolved_at = now()) and an online alert is emitted.
Production-Ready Multi-Language Integration SDKs
Copy-paste production implementations for ingesting live vehicle streams into your ERP background services, database layers, and web controllers.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use WebSocket\Client;
/**
* Enterprise Fleet Telematics Ingestion Service for Laravel / PHP
*/
class FleetTrackingService
{
protected string $baseUrl;
protected string $erpClientId;
protected string $apiKey;
public function __construct()
{
$this->baseUrl = config('services.telematics.url', 'https://api.yourtrackinghost.com');
$this->erpClientId = config('services.telematics.client_id', 'odoo-prod');
$this->apiKey = config('services.telematics.api_key', 'fk_...');
}
/**
* Fetch active vehicle catalog
*/
public function getVehicles(): array
{
$response = Http::withToken($this->apiKey)
->get("{$this->baseUrl}/api/integration/vehicles");
if ($response->failed()) {
Log::error("Failed to fetch vehicles: " . $response->body());
throw new \Exception("Vehicle fetch failed: " . $response->status());
}
return $response->json();
}
/**
* Mint a short-lived WebSocket session token
*/
public function createSession(array $vehicleIds, int $ttlSeconds = 600): array
{
$response = Http::post("{$this->baseUrl}/api/integration/session", [
'erpClientId' => $this->erpClientId,
'apiKey' => $this->apiKey,
'vehicleIds' => $vehicleIds,
'sessionLengthSeconds' => $ttlSeconds,
]);
if ($response->failed()) {
Log::error("Session creation failed: " . $response->body());
throw new \Exception("Session minting error: " . $response->status());
}
return $response->json();
}
/**
* Long-running stream worker daemon
*/
public function runStreamDaemon(array $vehicleIds): void
{
while (true) {
try {
Log::info("Minting fresh session token for stream daemon...");
$session = $this->createSession($vehicleIds, 600);
$wsUrl = str_replace(['http://', 'https://'], ['ws://', 'wss://'], $this->baseUrl)
. "/ws?token=" . $session['token'];
$client = new Client($wsUrl, ['timeout' => 500]);
Log::info("Connected to live telematics stream.");
$connectedAt = time();
// Renew connection at 80% of TTL (480 seconds)
while (time() - $connectedAt < 480) {
$message = $client->receive();
if (!$message) continue;
$payload = json_decode($message, true);
$this->handleFrame($payload);
}
$client->close();
} catch (\Exception $e) {
Log::warning("WebSocket stream dropped: " . $e->getMessage() . ". Reconnecting in 5s...");
sleep(5);
}
}
}
protected function handleFrame(array $frame): void
{
$type = $frame['type'] ?? '';
if ($type === 'position') {
$vehicleId = $frame['vehicleId'];
$pos = $frame['position'];
Log::debug("Position update: Vehicle #{$vehicleId} @ [{$pos['lat']}, {$pos['lon']}] Speed: {$pos['speedKn']} kn");
// Update internal ERP database records or broadcast to WebSocket UI
} elseif ($type === 'alert') {
$alert = $frame['alert'];
Log::warning("Fleet Alert: " . $alert['message']);
} elseif ($type === 'snapshot') {
Log::info("Snapshot received with " . count($frame['positions']) . " vehicle states.");
}
}
}
Database Schema & PostGIS Spatial Reference
Relational Data Model & PostGIS Extensions
The underlying database is PostgreSQL with the postgis extension enabled. Spatial points are generated dynamically from coordinates using ST_MakePoint(lon, lat) with SRID 4326.
-- Spatial Geometries & Telematics Tables
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
plan_id BIGINT REFERENCES plans(id),
alert_email TEXT,
alert_webhook TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE vehicles (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
imei TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
plate TEXT NOT NULL DEFAULT '',
dest_lat DOUBLE PRECISION,
dest_lon DOUBLE PRECISION,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_vehicles_imei ON vehicles (imei);
CREATE TABLE positions (
id BIGSERIAL PRIMARY KEY,
vehicle_id BIGINT NOT NULL REFERENCES vehicles(id) ON DELETE CASCADE,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
device_time TIMESTAMPTZ NOT NULL,
valid BOOLEAN NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lon DOUBLE PRECISION NOT NULL,
point geography(Point,4326) GENERATED ALWAYS AS
(ST_SetSRID(ST_MakePoint(lon, lat), 4326)::geography) STORED,
speed_kn DOUBLE PRECISION NOT NULL DEFAULT 0,
course DOUBLE PRECISION NOT NULL DEFAULT 0,
raw_frame TEXT NOT NULL
);
CREATE INDEX idx_positions_vehicle_devicetime ON positions (vehicle_id, device_time DESC);
CREATE TABLE geofences (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
name TEXT NOT NULL,
center geography(Point,4326) NOT NULL,
radius_m DOUBLE PRECISION NOT NULL CHECK (radius_m > 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE integration_keys (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
client_id TEXT UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ
);
Platform UI & Screenshot Gallery
Click any screenshot to expand in high resolution and inspect live telematics indicators, trail replay lines, and multi-tenant management portals.
Live Map Dashboard
Real-time vehicle markers, heading rotation, speed badges, and active state panel.
Route Replay & Movement Trail
Temporal breadcrumbs connecting GPS points for route adherence analysis.
Multi-Tenant Overview
Global fleet oversight across all provisioned client companies and tenancies.
Restricted User Workspace
Scoped dispatcher view displaying strictly assigned vehicles and routes.
Tenant & Billing Portal
Customer onboarding, webhook URLs, plan limits, and invoice tracking.
Troubleshooting, Error Reference & FAQ
Frequently Encountered Integration Questions
Q: Why did my WebSocket connection close with code 4001?
Close code 4001 (Unauthorized) indicates that the session token in the ?token= query parameter was omitted, malformed, or expired. Remember that session JWTs have an expiration timestamp (e.g. 300s or 600s). Re-mint a fresh token via POST /api/integration/session and reconnect.
Q: Can our ERP send a subscribe frame after connecting?
No. Integration sockets are bound strictly to the vehicle IDs cryptographically embedded in the session token during minting. Any subscribe or unsubscribe messages sent over integration sockets are ignored by design to prevent privilege escalation.
Q: What is the maximum number of vehicles per WebSocket session?
A single session token can whitelist up to 500 vehicles. If your fleet exceeds 500 units, partition your fleet into multiple concurrent sessions (e.g. grouped by region or vehicle type) across separate socket connections.
Q: How do we convert speed values to km/h and mph?
The raw GPS chip outputs speed in international nautical knots. Multiply by 1.852 to convert to kilometers per hour (km/h = knots * 1.852), or multiply by 1.15078 for miles per hour (mph = knots * 1.15078).
Q: How does the system handle vehicle reconnects after tunnel loss?
When a vehicle moves through a tunnel, cellular or GPS signal may drop. The device will mark valid: false on un-fixed frames. Once GPS lock is restored, the next valid frame updates coordinates and automatically resolves any open offline alert by emitting an online alert frame.