Docs / ERP Integration Guide

Live GPS Fleet Tracking — ERP Integration Manual

Comprehensive technical integration manual and API reference for Enterprise Resource Planning (ERP), Warehouse Management (WMS), Transportation Management (TMS), and Dispatch Systems. Integrate high-frequency live vehicle telematics, historical route playback, circular geofencing alerts, and embed live tracking maps directly into your own ERP UI.

Protocol Ingestion Sinotrack H02 (TCP :9000)
Realtime Streaming RFC 6455 WebSockets (/ws)
Spatial Engine PostgreSQL 15+ & PostGIS
Authentication API Key (SHA-256) + JWT
UI Embeddable Leaflet / OpenStreetMap / Mapbox
01

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:

GPS Trackers Sinotrack / H02 TCP Port :9000 Raw TCP Fleet Telematics Core Server TCP Ingest Listener IMEI Cache & NMEA Parser Realtime WebSocket Hub RFC 6455 Multiplexer (/ws) REST & Webhook Emitter Auth, Keys, Routes, Geofences Offline Watcher (60s tick) PostgreSQL + PostGIS Spatial Point Geography ▪ positions (ST_Point) ▪ geofences (ST_DWithin) ▪ vehicles (IMEI / plates) ▪ integration_keys (hash) ▪ alerts & invoices Enterprise ERP / WMS Odoo / SAP / Dynamics / Custom 1. Live Location Sync 2. ETA & Route Replay 3. Geofence Boundary 4. Driver / Asset Map 5. Automated Invoicing WebSocket Stream REST APIs (JSON) Webhooks (HTTP POST)

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.

02

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.

1

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.

POST /api/integration/register
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!"
  }'
HTTP 201 CREATED RESPONSE:
{
  "customerId": 2,
  "erpClientId": "odoo-production",
  "apiKey": "fk_9d4e5f6a7b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c"
}
2

Retrieve Vehicle Catalog & Map Internal IDs

Query the customer's fleet directory to match ERP delivery vehicles against system tracking IDs and device IMEIs.

GET /api/integration/vehicles
curl -X GET https://api.yourtrackinghost.com/api/integration/vehicles \
  -H "Authorization: Bearer fk_9d4e5f6a7b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c"
HTTP 200 OK RESPONSE:
[
  { "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" }
]
3

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).

POST /api/integration/session
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
  }'
HTTP 200 OK RESPONSE:
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJraW5kIjoiaW50ZWdyYXRpb24iLCJjaWQiOjIsInZpZHMiOlsyLDMsNF0sImlhdCI6MTcyNDIxODAwMCwiZXhwIjoxNzI0MjE4NjAwfQ.xyz...",
  "vehicleIds": [2, 3, 4],
  "expiresIn": 600,
  "expiresAt": "2026-08-21T10:30:00.000Z",
  "wsUrl": "/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
4

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.

Proactive Reconnect Strategy: The session JWT is single-purpose and stateless. To maintain seamless 24/7 streaming without disconnect gaps, schedule a background timer to mint a new token at 80% TTL elapsed (e.g. at 480 seconds for a 600s token) and gracefully reconnect.
03

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.
12

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.

METHOD A Programmatic Self-Service Registration (API)

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.

POST /api/integration/register
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!"
  }'
HTTP 201 CREATED RESPONSE:
{
  "customerId": 2,
  "erpClientId": "odoo-production-instance",
  "apiKey": "fk_4f8e2a1b9c3d7e5f608192a3b4c5d6e7f8a9b0c1d2e3f4a5"
}
Zero-Downtime Key Rotation: Re-issuing a POST /api/integration/register request with the same erpClientId generates a fresh key and immediately replaces the previous key hash in the database.
METHOD B Web Dashboard UI Generation (Platform Portal)

Administrators can visually generate and manage integration keys through the web dashboard:

  1. Log in to the web dashboard (/login) as an administrator account.
  2. In the navigation bar, navigate to Integration (/admin/integration).
  3. Under New API Key, enter:
    • Label / Name: e.g., "Odoo Main ERP"
    • Client ID (erp_client_id): e.g., "odoo-prod"
  4. 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
13

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:

1. User opens ERP Delivery Dashboard in Browser.
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>
04

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.

POST /api/integration/register Unauthenticated (Admin Creds in Body)

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)
FieldTypeRequiredDescription
erpClientIdStringYesUnique identifier for the ERP installation (e.g. "odoo-prod-east", "sap-wms-01").
emailStringYesEmail address of an active tenant administrator with role admin.
passwordStringYesPlaintext 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"
  }'
POST /api/integration/session API Key (Header or Body)

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)
FieldTypeRequiredDescription
erpClientId / clientIdStringYesMust match the client ID bound to the integration key.
apiKeyStringConditionalRequired if not sending Authorization: Bearer fk_... header.
vehicleIdsArray<Integer>YesList of vehicle IDs to track (max 500 IDs per session).
sessionLengthSeconds / ttlSecondsIntegerNoSession 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
  }'
GET /api/integration/vehicles Bearer fk_... or x-api-key

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.

GET /api/integration/vehicles
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" }
]
POST GET /api/integration/keys  |  /:id/revoke Bearer Admin JWT

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":"..."}. Returns key once.
  • 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).
GET POST /api/vehicles Bearer User / Admin JWT

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
    }
  }
]
GET /api/vehicles/:id/positions Bearer User / Admin JWT

Retrieves historical GPS trail records for route playback, speed profiling, and delivery compliance auditing.

Query Parameters
ParameterTypeDefaultDescription
fromISO Date / Epoch msNow - 6 hoursStart of temporal window.
toISO Date / Epoch msNowEnd of temporal window (max 30 days range).
PATCH /api/vehicles/:id/destination Bearer Admin JWT

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
POST /api/routes/optimize Bearer User / Admin JWT

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]
  ]
}
GET POST /api/geofences  |  /:id/assign Bearer Admin JWT

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 center lat, lon, radius_m, and vehicleIds.
  • 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}.
GET /api/alerts Bearer User / Admin JWT

Queries historical security and spatial boundary events (enter, exit, offline, online) for the authenticated tenant. Supports limit query parameter (max 200).

GET POST /api/users  |  /api/vehicles/:id/assign Bearer Admin JWT

Enables tenant administrators to provision standard driver/viewer accounts and grant granular vehicle visibility permissions.

GET POST /api/invoices  |  /api/invoices/:id/pay Bearer Admin / Super Admin JWT

Exposes automated billing records generated by the periodic invoice scheduler (every 12 hours). Enables ERP accounting reconciliation and status updates.

05

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:

WebSocket Connection URL
wss://api.yourtrackinghost.com/ws?token=<session-jwt-token>

WebSocket Message Frame Types

FRAME 1 Snapshot Frame (Sent Immediately on Connect)

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
    }
  ]
}
FRAME 2 Position Frame (Live GPS Report)

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
  }
}
FRAME 3 Alert Frame (Geofence / Offline Event)

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

FieldUnitsConversion FormulaDescription
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.852
mph = 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.
06

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"
}
Webhook Receiver Best Practices:
  • Return an immediate HTTP 200 OK response 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, and at timestamp.
07

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.

Raw H02 ASCII Datagram:
*HQ,867421030123456,V1,101410,A,5130.4440,N,00007.6140,W,013.70,091,210826,FFFFFBFF,234,15,0,0#
PosRaw ValueField NameDescription & Parsing Logic
1*HQHeader MarkerFixed protocol identifier preamble.
2867421030123456Device IMEI8 to 15 digit hardware IMEI identifier. Looked up in memory cache.
3V1Version CodeProtocol specification level.
4101410Time (HHMMSS)10:14:10 UTC satellite time.
5AFix ValidityA = Valid GPS lock, V = Void / Cell-tower estimate.
6–75130.4440, NLatitude51° + (30.4440 / 60)' = 51.5074° N
8–900007.6140, WLongitude0° + (07.6140 / 60)' = -0.1269° W (Negative sign for W/S)
10013.70Speed (Knots)13.70 Knots (~25.37 km/h).
11091Heading91° True East heading.
12210826Date (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.

08

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.");
        }
    }
}
09

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.

PostgreSQL Schema (Core Entities)
-- 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
);
11

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.

Fleet Telematics Enterprise ERP Integration Documentation • Protocol Version 1.0.0 • Built for High-Availability Telematics Ingestion