Skip to main content
Track and retrieve a user’s real-time location using the device’s native GPS. Supports live updates to your frontend as the user moves, chronological replay on app reopen, optional server delivery on each point, and distance-based triggers for high-accuracy use cases like running or navigation apps. Foreground tracking works out of the box with no additional setup. Background tracking, where the GPS keeps recording after the user leaves your app, requires a one-time toggle in the Despia Editor and a rebuild.

Installation

npm install despia-native
import despia from 'despia-native';

Despia Editor setup

Foreground tracking is enabled by default. Enable Background Location only if your app needs to keep recording GPS while the user is in another app or has the screen locked, such as run trackers, delivery apps, or navigation. If your use case is a one-time location read or in-app tracking only, skip this setup entirely.
1

Open the Background Location addon

In the Despia Editor, navigate to App > Addons > Background Location.
2

Enable the addon

Toggle Background Location on. This adds the iOS background modes entitlement and the Android foreground service permission to your next build, both of which are required for the OS to allow GPS to keep running while your app is not in the foreground.
3

Rebuild your app

Trigger a fresh build from the Despia Editor. Background location requires native entitlements that have to be compiled into the app binary, so this cannot be applied over-the-air. After the rebuild, calls to location:// keep recording when the app is backgrounded.
Skipping the rebuild leaves background tracking inactive even if the toggle reads enabled. The location:// call will still work in the foreground, but as soon as the user backgrounds the app, the GPS stops recording silently. If your tracking sessions are missing the middle section, this is almost always the cause.

How it works

Call location:// to start the native GPS session and stoplocation:// to end it and retrieve the full session array. While tracking is active, every GPS point is delivered to window.onLocationChange in real time and stored locally on the device. When tracking stops, the complete session is returned to your JavaScript for processing.
import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia && !despia.locationTracking) {
    despia('location://?buffer=10')
}
To stop tracking and retrieve the session:
const data = await despia('stoplocation://', ['locationSession'])
const locations = data.locationSession

Parameters

location:// accepts the following query parameters.
ParameterRequiredDescription
bufferYesMinimum seconds between time-based GPS updates. Use 5 for high frequency, 30 or higher for battery-conscious use cases
serverNoRemote API endpoint that receives each location update as a POST request. Omit if you only need local storage or frontend updates
movementNoDistance threshold in centimetres. When set, an additional update fires immediately whenever the device moves this distance, regardless of the buffer timer. Use 100 for 1 metre precision

Live updates with window.onLocationChange

Define window.onLocationChange before calling location:// to receive every GPS point in real time as the user moves. Each call receives a single location object with an active flag indicating whether tracking is still running.
import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

window.onLocationChange = function (data) {
    if (!data.active) {
        // Tracking ended, finalize the route or show summary
        finalizeRoute()
        return
    }

    // Coordinates, use directly for map rendering
    drawPointOnMap(data.latitude, data.longitude)

    // Speed, convert m/s to min/km pace
    if (data.speed !== null && data.speed > 0) {
        const paceMinPerKm = (1000 / data.speed / 60).toFixed(2)
        updatePaceDisplay(paceMinPerKm + ' min/km')
    }

    // Course, rotate a heading arrow
    if (data.course !== null) {
        headingArrow.style.transform = `rotate(${data.course}deg)`
    }

    // Accuracy, skip noisy points
    if (data.horizontalAccuracy > 10) return

    // Battery, show live drain
    updateBatteryIndicator(data.battery + '%')
}

if (isDespia && !despia.locationTracking) {
    despia('location://?buffer=60&movement=100')
}
When the app reopens during an active session, all buffered points are replayed into window.onLocationChange in chronological order so your frontend can reconstruct the full route without any additional code.

High-accuracy tracking

For use cases where each metre counts, combine movement with a long buffer to get distance-triggered updates as the primary signal and time-based updates as a heartbeat fallback.
window.onLocationChange = function (data) {
    if (!data.active) {
        finalizeRoute()
        return
    }
    drawPointOnMap(data.latitude, data.longitude)
    updatePace(data.speed)
}

// Fire on every 1 metre moved, with a 60s heartbeat
if (isDespia) {
    despia('location://?buffer=60&movement=100')
}
Filter for high-accuracy points using horizontalAccuracy before calculating distances. A value below 10 indicates a precise fix.
const accurate = locations.filter(loc => loc.horizontalAccuracy < 10)

Server delivery

When server is set, each GPS point is POSTed to your endpoint as it is recorded. Server delivery, window.onLocationChange, and local session storage all run simultaneously and independently. Loss of network does not affect local storage or frontend callbacks.
if (isDespia) {
    despia('location://?server=https://api.example.com/track?user=USER_ID&buffer=30&movement=100')
}
Each POST body matches the location object shape below.

Location object

Every location point, whether delivered via window.onLocationChange, the session array, or server POST, has the following shape.
{
    "latitude": 25.276987,
    "longitude": 55.296249,
    "timestamp": 1737219125.5,
    "gpsTimestamp": 1737219125.3,
    "speed": 2.5,
    "course": 87.3,
    "altitude": 12.4,
    "horizontalAccuracy": 5.2,
    "verticalAccuracy": 3.8,
    "battery": 85,
    "active": true
}
FieldDescriptionUsage
latitudeGPS latitude coordinateMap rendering, distance calculation
longitudeGPS longitude coordinateMap rendering, distance calculation
timestampUnix timestamp when the point was recorded on the device (seconds)Chronological ordering, elapsed time
gpsTimestampRaw GPS chip timestamp (seconds), may differ slightly from timestampHigh-precision timing
speedSpeed in metres per second. null if unavailablePace display: (1000 / data.speed / 60).toFixed(2) gives min/km
courseDirection of travel in degrees from 0 to 360. null if unavailableRotate a heading arrow: arrow.style.transform = 'rotate(' + data.course + 'deg)'
altitudeElevation above sea level in metresElevation gain charts
horizontalAccuracyAccuracy radius of the lat/lng in metres. Lower is betterFilter noisy points: discard if > 10
verticalAccuracyAccuracy of the altitude measurement in metresFilter noisy elevation data
batteryDevice battery percentage recorded at the exact moment of each GPS pointBattery drain per route: firstPoint.battery - lastPoint.battery
activetrue while tracking is running. false on the final event when tracking stopsDrive UI state, finalize route
Battery percentage is sampled at every GPS point throughout the session, giving you a precise drain curve for the entire route rather than just a start and end value.

Tracking state

Use despia.locationTracking to reflect the current tracking state in your UI. It is set to true when location:// is called and false when stoplocation:// is called.
if (despia.locationTracking) {
    // show stop button and active indicator
} else {
    // show start button
}

Calculate distance and analyse movement

Use the Haversine formula to calculate total distance from the session array after stopping.
const data = await despia('stoplocation://', ['locationSession'])
const locations = data.locationSession

// Filter for high-accuracy points only
const accurate = locations.filter(loc => loc.horizontalAccuracy < 10)

function calculateDistance(lat1, lon1, lat2, lon2) {
    const R  = 6371000
    const p1 = lat1 * Math.PI / 180
    const p2 = lat2 * Math.PI / 180
    const dp = (lat2 - lat1) * Math.PI / 180
    const dl = (lon2 - lon1) * Math.PI / 180
    const a  = Math.sin(dp / 2) ** 2 + Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) ** 2
    return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
}

let totalDistance = 0
for (let i = 1; i < accurate.length; i++) {
    totalDistance += calculateDistance(
        accurate[i - 1].latitude, accurate[i - 1].longitude,
        accurate[i].latitude,     accurate[i].longitude
    )
}

console.log(`Total distance: ${(totalDistance / 1000).toFixed(2)} km`)

// Check movement
const isMoving = locations.some(loc => loc.speed !== null && loc.speed > 0.5)

// Battery drain
const drain = locations[0]?.battery - locations[locations.length - 1]?.battery
console.log(`Battery used: ${drain}%`)

Background tracking notes

Foreground tracking works with no additional permissions and continues as long as the app is in the foreground. To track while the app is backgrounded, follow the Despia Editor setup at the top of this page. Once enabled, Despia manages the iOS blue badge indicator automatically and dismisses it as soon as stoplocation:// is called. On Android, Despia handles background location delivery across all major manufacturers including Samsung, Huawei, Xiaomi, and OnePlus, which apply aggressive background restrictions by default. No additional configuration is required. If you encounter missing server events on a specific Android device, contact location@despia.com.

Resources

NPM Package

despia-native