DocsDevice Tracking

Device Tracking

Real-time location tracking is deceptively hard to do well over HTTP. The naive approach - polling GET /location every second - works for demos but falls apart in production: you're paying for requests whether or not the device moved, you can't go faster than your polling interval without hammering the server, and every poll adds a round-trip of latency between the device and anyone watching it.

PickPoint's Device Tracking API uses a persistent WebSocket connection instead. The device keeps a single TCP connection open and pushes location events as they happen - no polling, no wasted requests, and latency measured in milliseconds rather than seconds. Every location is also persisted and queryable via REST for historical analysis and replay.

Free includes 1 tracking device (1 Hz ingest). A fleet needs Pro or Ultra. Three actors are involved: a device that sends locations, the PickPoint server that stores and fans them out, and optional subscribers (your backend or dashboard) that receive a live feed.

How it works

Deviceyour app / hardwarePickPointtracking.pickpoint.ioSubscriberyour backend / dashboardPOST /v2/devices (register)201 { uid, secret }WebSocket connect (tracking.v2)Hellopublish({ lat, lon })trackStarted { trackUid }WebSocket connect (accessToken)subscribe(deviceUid)publish({ lat, lon, … })location { lat, lon, … }close() → TrackStop
Request / connectResponseSDK call (device → server)Push event (server → subscriber)

The PickPoint SDK

Device tracking uses a dedicated WebSocket endpoint with the tracking.v2 subprotocol. The examples below use official SDKs — see the Integration guide for full package list and SDK benefits. Custom clients can implement the wire format from pickpoint-proto.

  • JavaScript / TypeScript — @pickpoint/sdk/tracking (pickpoint-js, npm)
  • Python — pickpoint.tracking (python-sdk, PyPI pickpoint)
  • Go — github.com/pickpoint/go-sdk/tracking (go-sdk)
  • Rust — pickpoint::tracking (rust-sdk, crates.io pickpoint)
  • Kotlin / Java — io.pickpoint.tracking (jvm-sdk, Maven Central io.pickpoint:pickpoint)
  • Dart — package:pickpoint/tracking.dart (dart-sdk, pub.dev pickpoint)
  • C++ — pickpoint::tracking (cpp-sdk)

There is no official Swift SDK. For native iOS clients, implement the wire format from pickpoint-proto. The Ruby gem covers HTTP only (device registry, no live WebSocket). For GPS beacons on constrained hardware, pickpoint-nano provides HTTP ingest (POST /v2/ingest) without a full SDK.

Devices and tracks

A device is a persistent entity with a stable uid and secret - it represents a vehicle, phone, watch, or any tracked asset. Devices live in your dashboard and accumulate a history over their lifetime.

A track is a single continuous trip: the sequence of location points between one TrackStart and the corresponding TrackStop. The first publish() on a fresh connection starts a track automatically. Call close() (or stopTrack()) when the trip ends - that finalizes the record in storage.

Unlike polling-based designs, a brief disconnect does not start a new trip. The SDK reconnects and sends a Resume message with the last acknowledged sequence number, continuing the same trackUid. Only call startTrack() when you deliberately want a new trip - a new delivery order, a route change, or after a TRACK_NOT_FOUND error.

Connection

Endpointwss://tracking.pickpoint.io/v2/ws
Subprotocoltracking.v2 (handled by the SDK)
Device authclientId = device uid, clientSecret = device secret
Subscriber authaccessToken from POST /v2/client-tokens (scope devices)

Devices authenticate with their per-device credentials - safe to embed in mobile and IoT apps. Subscribers (dashboards, backends) use a short-lived client token minted by your server, not the API key directly.

Step-by-Step Integration

1

Register the device

Each tracked unit needs a uid / secret pair. Create one in the Tracking section of your dashboard, or via REST:

curl -X POST "https://api.pickpoint.io/v2/devices" \
  -H "X-Api-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Vehicle 42"}'

Response:

{
  "uid":       "d_7f3a9b2c",
  "secret":    "sk_4e1d8f0a2c9b7e3d",
  "name":      "Vehicle 42",
  "createdAt": "2024-09-01T10:00:00Z"
}

Store uid and secret securely on the device. They are static - rotation requires creating a new device.

2

Connect and publish locations

Connect with the device credentials from step 1. The SDK completes the Hello handshake, then you call publish() with latitude and longitude. The first publish on a fresh connection sends TrackStart automatically; the server replies with trackStarted and a trackUid.

// npm install @pickpoint/sdk

import { connect } from '@pickpoint/sdk/tracking';

const session = await connect({
  endpoint: 'wss://tracking.pickpoint.io', // SDK appends /v2/ws
  auth: {
    clientId:     'CLIENT_ID',     // device uid
    clientSecret: 'CLIENT_SECRET', // device secret
  },
});

session.on('trackStarted', (trackUid) => {
  console.log('Track started:', trackUid);
});

// First publish starts the trip if none is live
session.publish({
  latitude:  55.7558,
  longitude: 37.6173,
  accuracy:  4.2,
});

// When done: sends TrackStop and closes the socket
session.close();
// Gradle: implementation("io.pickpoint:pickpoint:2.0.0")

import io.pickpoint.tracking.Config
import io.pickpoint.tracking.DeviceAuth
import io.pickpoint.tracking.connect
import io.pickpoint.tracking.latLng

// suspend — use viewModelScope / runBlocking in apps
val session = connect(
    Config(
        endpoint = "wss://tracking.pickpoint.io",
        device = DeviceAuth(clientId = "CLIENT_ID", clientSecret = "CLIENT_SECRET"),
    ),
)

session.publish(latLng(55.7558, 37.6173)) // TrackStart if idle
session.close() // TrackStop + hang up
// Cargo.toml: pickpoint = "2"

use pickpoint::tracking::{self, DeviceAuth, LatLng};

#[tokio::main]
async fn main() -> Result<(), tracking::Error> {
    let session = tracking::connect(tracking::Config {
        endpoint: "wss://tracking.pickpoint.io".into(),
        device: Some(DeviceAuth {
            client_id: "CLIENT_ID".into(),
            client_secret: "CLIENT_SECRET".into(),
        }),
        ..Default::default()
    })
    .await?;

    session.publish(LatLng::new(55.7558, 37.6173)).await?;
    session.close().await?;
    Ok(())
}
3

Send location updates

Call publish() as frequently as needed - up to 50 Hz. Only latitude and longitude are required; the SDK assigns sequence numbers and handles acknowledgements:

session.publish({
  latitude:  55.7558,
  longitude: 37.6173,
  accuracy:  4.2,
  altitude:  144,
  // timestampMs: Date.now(), // optional; server stamps live points by default
});

Live points are stamped by the server at ingest time. Pass timestampMs when flushing buffered offline points so the recorded time reflects when the measurement was taken on the device.

4

Subscribe to live updates (optional)

Your backend or dashboard can receive real-time events for any device. Mint a client token with scope devices on your server, connect with accessToken, and pass the device uid in subscribe. The SDK emits location for every incoming point and presence when a device goes online or offline.

import { connect } from '@pickpoint/sdk/tracking';

// Mint a client-token on your backend (scope: devices) — never use the API key here
const { accessToken } = await fetch('/api/pickpoint/client-token').then(r => r.json());

const session = await connect({
  endpoint: 'wss://tracking.pickpoint.io',
  auth: { accessToken },
  subscribe: 'DEVICE_UID',
});

session.on('location', (msg) => {
  console.log(msg.deviceUid, msg.point.latitude, msg.point.longitude);
});

session.on('presence', (msg) => {
  console.log(msg.deviceUid, msg.online ? 'online' : 'offline');
});
// dart pub add pickpoint

import 'package:pickpoint/tracking.dart';

final session = await connect(TrackingConfig(
  endpoint: 'wss://tracking.pickpoint.io',
  auth: ListenerAuth(accessToken: accessToken),
  subscribe: 'DEVICE_UID',
));

session.onLocation.listen((msg) {
  print('${msg.deviceUid} ${msg.point.latitude} ${msg.point.longitude}');
});
import io.pickpoint.mintClientTokens
import io.pickpoint.tracking.Config
import io.pickpoint.tracking.ListenerAuth
import io.pickpoint.tracking.connect

val pair = mintClientTokens(
    io.pickpoint.Config(apiKey = System.getenv("PICKPOINT_API_KEY")),
    listOf("devices"),
    600,
)
val session = connect(
    Config(
        endpoint = "wss://tracking.pickpoint.io",
        listener = ListenerAuth(accessToken = pair.accessToken),
        subscribe = listOf("DEVICE_UID"),
    ),
)

session.onLocation { loc ->
    println("${loc.point.latitude} ${loc.point.longitude}")
}
use pickpoint::{mint_client_tokens, Config};
use pickpoint::tracking::{self, ListenerAuth, ServerEvt};

let pair = mint_client_tokens(
    &Config::with_api_key(std::env::var("PICKPOINT_API_KEY").unwrap()),
    &["devices".into()],
    Some(600),
)
.await?;

let session = tracking::connect(tracking::Config {
    endpoint: "wss://tracking.pickpoint.io".into(),
    listener: Some(ListenerAuth {
        access_token: pair.access_token,
    }),
    subscribe: vec!["DEVICE_UID".into()],
    ..Default::default()
})
.await?;

loop {
    match session.recv().await? {
        ServerEvt::LocationAdded { device_uid, point, .. } => {
            println!("{device_uid} {} {}", point.latitude, point.longitude);
        }
        _ => {}
    }
}
5

Query track history

All location updates are stored indefinitely. Query them per device with optional time range filtering:

curl "https://api.pickpoint.io/v2/devices/d_7f3a9b2c/tracks?from=2024-09-01T00:00:00Z&to=2024-09-01T23:59:59Z" \
  -H "X-Api-Key: YOUR_KEY"

Response:

{
  "tracks": [
    {
      "uid":       "t_1a2b3c4d",
      "startedAt": "2024-09-01T10:15:00Z",
      "endedAt":   "2024-09-01T11:42:00Z",
      "points": [
        {
          "latitude":  55.7558,
          "longitude": 37.6173,
          "timestamp": "2024-09-01T10:15:00.000Z",
          "accuracy":  4.2,
          "speed":     8.3,
          "heading":   270,
          "altitude":  144
        }
      ]
    }
  ]
}

Each track maps to one trip (TrackStart … TrackStop). A device that completed three deliveries in a day will have three track entries. Brief disconnects within a trip resume the same track - only an explicit close() or stopTrack() finalizes it.

Reconnection handling

The SDK reconnects automatically with exponential backoff (enabled by default). After a drop it sends a Resume message with the last acknowledged sequence number, continuing the same trackUid - you do not need to call startTrack() again after every reconnect. Only start a new trip when the business logic requires it:

const session = await connect({
  endpoint: 'wss://tracking.pickpoint.io',
  auth: { clientId: DEVICE_UID, clientSecret: DEVICE_SECRET },
  reconnect: true, // default — exponential backoff, auto-resume
});

// After a drop, the SDK reconnects and resumes the same trackUid + seq cursor
session.on('resumeOk', ({ trackUid, lastAckedSeq }) => {
  console.log('Resumed', trackUid, 'from seq', lastAckedSeq);
});

// Only call startTrack() to begin a *new* trip (new order, route change, TRACK_NOT_FOUND)
// await session.startTrack({ location: { latitude: 55.75, longitude: 37.61 } });

SDK API Reference

Method / EventRoleDescription
connect()BothOpen WebSocket, complete Hello handshake
publish(point)DeviceSend a location; auto TrackStart if no trip is live
startTrack(opts?)DeviceBegin a new trip (optionally with route + metadata)
stopTrack() / close()DeviceEnd the trip and close the connection
trackStartedDevice eventTrip started; provides trackUid
resumeOkDevice eventReconnected; same trackUid and last acked seq
subscribe(uid)ListenerWatch a device (also pass subscribe in connect() options)
locationListener eventLive position push for a subscribed device
presenceListener eventDevice online / offline status change
errorBoth eventAuth or protocol error; see error code

Location data and what the fields mean

Only latitude and longitude are required. The optional fields are worth understanding:

  • timestampMs - Unix epoch milliseconds when the measurement was taken on the device. Live publishes omit it and the server stamps ingest time. Provide it when flushing buffered offline points.
  • accuracy - horizontal accuracy radius in metres (the 68th-percentile circle in which the true position lies, per the standard GPS HDOP definition). Consumer smartphone GPS is typically 3–10 m in open sky, degrading to 30–100 m indoors or in dense urban canyons. Use this field to filter out poor-quality fixes before plotting them on a map.
  • speed - ground speed in m/s derived by the GPS chipset from Doppler shift, not from successive coordinates. It's more accurate than computing distance / time from two points and works even when the device is stationary.
  • heading - direction of travel in degrees clockwise from true north (0–360). Only meaningful when the device is moving; GPS heading at standstill is effectively random.
  • altitude - height above the WGS 84 reference ellipsoid, not above mean sea level. The difference (geoid height) varies from −100 m to +90 m depending on where you are. For most applications this doesn't matter; for aviation or precise surveying it does.

Update frequency

The API supports up to 50 Hz (50 location updates per second) per device on paid plans. Free is capped at 1 Hz ingest. In practice, the right rate depends on your hardware and use case:

  • 1 Hz - consumer smartphones and most fleet trackers. Matches standard GPS chip output and is sufficient for delivery tracking, geofencing, and most field-service applications.
  • 5–10 Hz - sports GPS watches (Garmin, Polar, Suunto) and dedicated fitness hardware. Needed for accurate pace calculation on curves and for biomechanical analysis.
  • 10–50 Hz - GNSS modules in autonomous vehicles, racing telemetry, and drone flight controllers. At these rates the bottleneck is the hardware, not the API.

Note that "GPS" in common usage usually means GNSS - a combination of GPS (US), GLONASS (Russia), Galileo (EU), and BeiDou (China). Modern chipsets use all four constellations simultaneously, which improves accuracy and fix time especially in urban canyons where some satellites are obscured.

API Reference ↗GeocodingRouting