YipiiYipii IoT Docs

Live Tracking & WebSocket

Real-time location streaming for authenticated users and public tracking links

Share

Yipii IoT streams real-time vehicle locations over WebSocket using a Pusher-compatible protocol. The IoT backend receives position data from GPS trackers and relays it to connected clients. This guide covers both authenticated fleet tracking and public tracking link integration.

Architecture

Live tracking architecture: GPS Tracker → Ingestion Server → IoT Backend → WebSocket Server → Your App

GPS trackers installed in vehicles send position data via cellular to the ingestion server. The IoT backend processes and enriches this data, then broadcasts location events via a Pusher-compatible WebSocket server. Your application subscribes to channels to receive updates in real time.

ComponentURL
WebSocket Serverwss://ws-live.yipii.io
REST APIhttps://api.yipii.io
Broadcasting Authhttps://api.yipii.io/broadcasting/auth

Data Flow

Authentication flows: Authenticated users via OAuth2 token vs Public tracking via session token

Authenticated Live Stream

For dashboard integrations where you have an OAuth2 access token.

Connection

Connect using any Pusher-compatible client library. The WebSocket server speaks the Pusher protocol.

import Pusher from 'pusher-js';
 
const pusher = new Pusher('your-app-key', {
  wsHost: 'ws-live.yipii.io',
  wsPort: 443,
  wssPort: 443,
  forceTLS: true,
  enabledTransports: ['ws', 'wss'],
  disableStats: true,
  cluster: '',
  authEndpoint: 'https://api.yipii.io/broadcasting/auth',
  auth: {
    headers: {
      'Authorization': `Bearer ${ACCESS_TOKEN}`,
    },
  },
});

Authentication

The same OAuth2 Bearer token from the Authentication guide is used. Pass it in the Authorization header. The auth endpoint validates your token and ensures you only receive updates for assets your account has access to.

Subscribe to Asset Updates

Subscribe to a private channel for your account:

const channel = pusher.subscribe(`private-account.${ACCOUNT_KEY}`);
 
channel.bind('pusher:subscription_succeeded', () => {
  console.log('Connected to live tracking');
});

Events

EventPayloadDescription
.location.updatedLocationUpdate or LocationUpdate[]Position update for assets
.alert.triggeredAlertDataGeofence, speed, or maintenance alert

Note the leading dot (.) — required by the server's event naming convention.

Location Update Payload

{
  "assetId": 123,
  "assetName": "Delivery Van 1",
  "latitude": 35.8992,
  "longitude": 14.5141,
  "speed": 65,
  "course": 180,
  "timestamp": 1707216000000,
  "gpsSignal": 12,
  "status": {
    "name": "moving",
    "color": "green",
    "ignition": true,
    "engine": true
  },
  "geoPoint": {
    "street": "Triq il-Kbira",
    "town": "Valletta",
    "country": "Malta"
  }
}

JavaScript Example

import Pusher from 'pusher-js';
 
const pusher = new Pusher('your-app-key', {
  wsHost: 'ws-live.yipii.io',
  wsPort: 443,
  wssPort: 443,
  forceTLS: true,
  enabledTransports: ['ws', 'wss'],
  disableStats: true,
  cluster: '',
  authEndpoint: 'https://api.yipii.io/broadcasting/auth',
  auth: {
    headers: {
      'Authorization': `Bearer ${ACCESS_TOKEN}`,
    },
  },
});
 
const channel = pusher.subscribe(`private-account.${ACCOUNT_KEY}`);
 
channel.bind('.location.updated', (data) => {
  const updates = Array.isArray(data) ? data : [data];
 
  updates.forEach((loc) => {
    console.log(`${loc.assetName}: ${loc.latitude}, ${loc.longitude}`);
    console.log(`  Speed: ${loc.speed} km/h, Heading: ${loc.course}`);
    console.log(`  Ignition: ${loc.status.ignition}`);
  });
});
 
channel.bind('.alert.triggered', (data) => {
  console.log(`Alert: ${data.message} (${data.alert_type})`);
});
 
// Cleanup
// pusher.disconnect();

Python Example

import pysher
import json
 
pusher = pysher.Pusher(
    key='your-app-key',
    custom_host='ws-live.yipii.io',
    secure=True,
    port=443,
    auth_endpoint='https://api.yipii.io/broadcasting/auth',
    auth_endpoint_headers={
        'Authorization': f'Bearer {ACCESS_TOKEN}'
    }
)
 
def on_connect(data):
    channel = pusher.subscribe(f'private-account.{ACCOUNT_KEY}')
    channel.bind('.location.updated', on_location)
 
def on_location(data):
    updates = json.loads(data)
    if not isinstance(updates, list):
        updates = [updates]
    for loc in updates:
        print(f"{loc['assetName']}: {loc['latitude']}, {loc['longitude']}")
        print(f"  Speed: {loc['speed']} km/h")
 
pusher.connection.bind('pusher:connection_established', on_connect)
pusher.connect()

Public Tracking WebSocket

For public tracking links where viewers don't have full API credentials. Uses session-based authentication with the same WebSocket server.

Overview

1. GET  /api/public/tracking/{uuid}/info      → Link details + access mode
2. POST /api/public/tracking/{uuid}/verify     → Session token (4h validity)
3. Connect WebSocket with session token
4. Subscribe to private-tracking.{uuid}
5. Listen for .location.updated events

Check the tracking link's access mode and status (no auth required):

GET https://api.yipii.io/api/public/tracking/{uuid}/info
{
  "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "display_name": "Bus Route 42",
  "access_mode": "public",
  "is_active": true,
  "is_expired": false,
  "is_within_schedule": true,
  "branding": {
    "logo_url": "https://...",
    "primary_color": "#E8C547",
    "company_name": "Yipii"
  }
}

Access modes:

ModeVerification Required
publicNone — call verify with empty body
emailEmail address
email_codeEmail + 6-digit code (sent via email)
authorized_onlyWhitelisted email address

Step 2: Verify and Get Session Token

POST https://api.yipii.io/api/public/tracking/{uuid}/verify

Public mode (no credentials):

{}

Email mode:

{
  "email": "viewer@example.com"
}

Email + code mode (two requests):

// First: request code
{ "email": "viewer@example.com" }
 
// Second: submit code
{ "email": "viewer@example.com", "access_code": "123456" }

Response:

{
  "success": true,
  "session": {
    "token": "abc123xyz...",
    "expires_at": "2026-01-30T15:00:00Z",
    "link": {
      "uuid": "a1b2c3d4-...",
      "title": "Bus Route 42"
    }
  }
}

The session token is valid for 4 hours by default.

Step 3: Connect WebSocket

import Pusher from 'pusher-js';
 
const pusher = new Pusher('public-tracking', {
  wsHost: 'ws-live.yipii.io',
  wsPort: 443,
  wssPort: 443,
  forceTLS: true,
  enabledTransports: ['ws', 'wss'],
  disableStats: true,
  cluster: '',
  authEndpoint: 'https://api.yipii.io/broadcasting/auth',
  auth: {
    headers: {
      'X-Session-Token': sessionToken,
    },
  },
});

Step 4: Subscribe to Channel

const channel = pusher.subscribe(`private-tracking.${uuid}`);
 
channel.bind('pusher:subscription_succeeded', () => {
  console.log('Subscribed — listening for location updates');
});

Step 5: Listen for Events

EventDescription
.location.updatedVehicle position changed
.link.expiredTracking link expired or disabled
.schedule.changedEntered or exited schedule window
.vehicle.changedRoute-based link reassigned to different vehicle
channel.bind('.location.updated', (data) => {
  const { location } = data;
 
  console.log(`Position: ${location.latitude}, ${location.longitude}`);
  console.log(`Speed: ${location.speed_kmh} km/h`);
  console.log(`Moving: ${location.is_moving}, Ignition: ${location.ignition}`);
 
  if (location.distance_km !== undefined) {
    console.log(`Distance to destination: ${location.distance_km} km`);
  }
  if (location.eta_minutes !== undefined) {
    console.log(`ETA: ${location.eta_minutes} minutes`);
  }
});
 
channel.bind('.link.expired', (data) => {
  console.log(`Link expired: ${data.reason}`);
  // reason: "arrival", "time_expired", "disabled", "schedule_ended"
  pusher.disconnect();
});
 
channel.bind('.schedule.changed', (data) => {
  if (!data.is_active) {
    console.log(data.message); // "Tracking available Mon-Fri 7am-4pm"
  }
});
 
channel.bind('.vehicle.changed', (data) => {
  console.log(data.message);
  if (data.new_location) {
    // Update map to new vehicle's position
  }
});

Location Event Payload

{
  "location": {
    "latitude": 35.9023,
    "longitude": 14.5134,
    "speed_kmh": 45,
    "heading": 180,
    "timestamp": "2026-01-30T10:30:00+00:00",
    "is_moving": true,
    "ignition": true,
    "street": "Triq il-Kbira",
    "town": "Iż-Żejtun",
    "country": "Malta",
    "distance_km": 0.8,
    "eta_minutes": 1
  }
}

Note: No internal identifiers (asset_id, IMEI, account info) are exposed. The distance_km and eta_minutes fields are only present when the link has arrival-based expiration configured.

Polling Fallback

If WebSocket is unavailable, poll the location endpoint every 30 seconds:

GET https://api.yipii.io/api/public/tracking/{uuid}/location
X-Session-Token: {session_token}

The response includes asset data with location fields matching the WebSocket payload structure.

Swift (iOS)

import PusherSwift
 
let options = PusherClientOptions(
    authMethod: .authRequestBuilder(authRequestBuilder: { request in
        var req = request
        req.addValue(sessionToken, forHTTPHeaderField: "X-Session-Token")
        return req
    }),
    host: .host("ws-live.yipii.io"),
    port: 443,
    useTLS: true
)
 
let pusher = Pusher(key: "public-tracking", options: options)
let channel = pusher.subscribe("private-tracking.\(uuid)")
 
channel.bind(eventName: "location.updated") { event in
    guard let data = event.data else { return }
    // Parse and update map
}
 
channel.bind(eventName: "link.expired") { event in
    // Show expired state
    pusher.disconnect()
}
 
pusher.connect()

Kotlin (Android)

val authorizer = HttpAuthorizer("https://api.yipii.io/broadcasting/auth").apply {
    setHeaders(mapOf("X-Session-Token" to sessionToken))
}
 
val options = PusherOptions().apply {
    setHost("ws-live.yipii.io")
    setWsPort(443)
    setWssPort(443)
    isUseTLS = true
    setAuthorizer(authorizer)
}
 
val pusher = Pusher("public-tracking", options)
val channel = pusher.subscribePrivate("private-tracking.$uuid")
 
channel.bind("location.updated") { event ->
    val data = JSONObject(event.data)
    val location = data.getJSONObject("location")
    // Update map
}
 
channel.bind("link.expired") { event ->
    // Show expired state
    pusher.disconnect()
}
 
pusher.connect()

Security

Data Exposed to Public Tracking Viewers

ExposedNOT Exposed
Position (lat/lng)IMEI number
Speed and headingInternal device IDs
Moving/stopped statusAccount information
Address (street, town)Track history
ETA and distance to destinationOther assets
Display nameDriver personal data

Session Security

  • Session tokens are high-entropy random strings tied to a specific tracking link UUID
  • Cross-link protection — a session for link A cannot access link B's channel
  • Expiration — sessions expire after a configurable period (default 4 hours)
  • TLS required — all connections use WSS (port 443)
  • Revocation — disabling a link immediately disconnects all viewers

Access Control for Public Tracking

ModeSecurity LevelUse Case
publicLowestCustomer delivery tracking
emailMediumKnown recipients (parents, partners)
email_codeHighSensitive cargo, compliance
authorized_onlyHighestRestricted access (warehouse staff)

Authenticated Stream Security

  • OAuth2 tokens are validated on connection and channel subscription
  • The server only sends updates for assets the token's account has access to
  • Subscribing to unauthorized channels is rejected
  • Token revocation disconnects the client

Scaling & Performance

Subscribe Only to What You Need

Each subscribed asset generates location events every 10-30 seconds when moving. Subscribing to 1,000 assets when you display 10 wastes bandwidth and processing.

// Good: subscribe to visible assets only
const channel = pusher.subscribe(`private-account.${accountKey}.assets.${assetId}`);
 
// Clean up when no longer needed
pusher.unsubscribe(`private-account.${accountKey}.assets.${assetId}`);

Deduplication

GPS trackers may send duplicate positions. Deduplicate by assetId + timestamp:

const seen = new Map();
const MAX_AGE_MS = 5000;
 
function isDuplicate(update) {
  const key = `${update.assetId}-${update.timestamp}`;
  if (seen.has(key)) return true;
 
  seen.set(key, Date.now());
 
  // Cleanup old entries periodically
  if (seen.size > 1000) {
    const cutoff = Date.now() - MAX_AGE_MS;
    for (const [k, time] of seen) {
      if (time < cutoff) seen.delete(k);
    }
  }
 
  return false;
}

Reconnection

Pusher clients reconnect automatically with exponential backoff. For public tracking, validate the session before reconnecting:

pusher.connection.bind('disconnected', () => {
  fetch(`https://api.yipii.io/api/public/tracking/${uuid}/info`)
    .then(res => res.json())
    .then(info => {
      if (info.is_active && !info.is_expired) {
        pusher.connect(); // Session still good
      } else {
        showExpiredMessage();
      }
    });
});

Rate Limits

Public Tracking:

EndpointLimit
POST /verify30 requests/minute per IP
GET /location (polling)60 requests/minute per session
WebSocket connections per IP100
Messages per second per channel10

Typical Update Frequency:

Vehicle StateUpdate Interval
MovingEvery 10-30 seconds
Stopped, ignition onEvery 60 seconds
Stopped, ignition offEvery 5-10 minutes

WebSocket Authentication Errors

CodeMeaning
4001Invalid session token
4002Session expired
4003Tracking link not found
4004Link expired or inactive
4005Outside schedule window

Next Steps

Was this page helpful?

Live Tracking & WebSocket | Yipii IoT Docs