Skip to main content

Send locations from your devices

This guide builds an end-to-end tracking system with SpatialFlow: geofences, entry/exit workflows, sending location updates, and receiving real-time notifications. The walkthrough uses a delivery fleet and depot locations as the running example; Patterns for other use cases below adapts the same steps for field service technicians, logistics and ETAs, and asset monitoring.

Use Case​

Track vehicles, technicians or equipment entering and exiting locations that matter to you:

  • Automatically log check-ins and check-outs
  • Notify a team of arrivals and departures
  • Calculate dwell time at a site
  • Monitor movements in real time

Architecture Overview​

Mobile App → Location Updates → SpatialFlow API
↓
Geofence Engine
↓
Workflow Triggers
↓
Webhook → Your Backend
↓
Dispatch Dashboard

Step 1: Create Depot Geofences​

Create geofences for each depot location.

Via API​

curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Depot - North Bay",
"description": "Primary vehicle depot for Bay Area operations",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-122.4200, 37.7745],
[-122.4200, 37.7753],
[-122.4188, 37.7753],
[-122.4188, 37.7745],
[-122.4200, 37.7745]
]]
},
"metadata": {
"depot_code": "NB001",
"region": "bay_area",
"capacity": 50,
"manager": "operations@example.com"
}
}'

Response:

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Depot - North Bay",
"geometry": { ... },
"metadata": {
"depot_code": "NB001",
"region": "bay_area"
},
"is_active": true,
"created_at": "2025-11-10T10:00:00Z"
}

Via Dashboard​

Open Geofences and click Create Geofence, draw the boundary or type an address, fill in the name and metadata, and click Create Geofence (or Save geofence from the address tab). See Geofences for the full walkthrough.

Step 2: Set Up Entry/Exit Workflows​

Create workflows to handle vehicle arrivals and departures.

Vehicle Check-In Workflow (Entry)​

curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Vehicle Check-In - North Bay Depot",
"description": "Log vehicle arrivals and notify dispatch",
"nodes": [
{
"id": "trigger-1",
"type": "trigger",
"data": {
"triggerType": "geofence_enter",
"label": "Geofence Entry",
"config": {
"geofence_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
}
}
},
{
"id": "action-1",
"type": "action",
"data": {
"actionType": "webhook",
"label": "HTTP Request",
"config": {
"url": "https://your-backend.com/api/vehicle-checkin",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"vehicle_id": "{{trigger.device_id}}",
"vehicle_name": "{{trigger.device_name}}",
"depot_id": "{{trigger.geofence_id}}",
"depot_name": "{{trigger.geofence_name}}",
"depot_code": "{{trigger.geofence.metadata.depot_code}}",
"checkin_time": "{{trigger.timestamp}}",
"location": {
"lat": "{{trigger.location.latitude}}",
"lng": "{{trigger.location.longitude}}"
}
}
}
}
},
{
"id": "action-2",
"type": "action",
"data": {
"actionType": "slack",
"label": "Slack Notification",
"config": {
"integration_id": "YOUR_SLACK_INTEGRATION_ID",
"message": "✅ Vehicle {{trigger.device_name}} checked in at {{trigger.geofence_name}}"
}
}
}
],
"edges": [
{"id": "edge-1", "source": "trigger-1", "target": "action-1"},
{"id": "edge-2", "source": "action-1", "target": "action-2"}
]
}'

The webhook step sends no credentials, because a step can't store them. If your backend needs a token, connect a Webhook integration that holds it and pick that integration as the action's destination instead of a URL; see Generic HTTP Alerting.

Resolving {{trigger.geofence.metadata.*}}

SpatialFlow populates the geofence.metadata portion of the trigger payload from the geofence's metadata JSON field at trigger time. Any key you add to a geofence's metadata becomes available to that geofence's triggers via {{trigger.geofence.metadata.<key>}}. For example, {{trigger.geofence.metadata.depot_code}} resolves to whatever value you stored under depot_code when you created the geofence.

Vehicle Check-Out Workflow (Exit)​

curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Vehicle Check-Out - North Bay Depot",
"description": "Log vehicle departures",
"nodes": [
{
"id": "trigger-1",
"type": "trigger",
"data": {
"triggerType": "geofence_exit",
"label": "Geofence Exit",
"config": {
"geofence_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
}
}
},
{
"id": "action-1",
"type": "action",
"data": {
"actionType": "webhook",
"label": "HTTP Request",
"config": {
"url": "https://your-backend.com/api/vehicle-checkout",
"method": "POST",
"body": {
"vehicle_id": "{{trigger.device_id}}",
"depot_id": "{{trigger.geofence_id}}",
"checkout_time": "{{trigger.timestamp}}"
}
}
}
},
{
"id": "action-2",
"type": "action",
"data": {
"actionType": "slack",
"label": "Slack Notification",
"config": {
"integration_id": "YOUR_SLACK_INTEGRATION_ID",
"message": "🚪 Vehicle {{trigger.device_name}} departed from {{trigger.geofence_name}}"
}
}
}
],
"edges": [
{"id": "edge-1", "source": "trigger-1", "target": "action-1"},
{"id": "edge-2", "source": "action-1", "target": "action-2"}
]
}'

Step 3: Register Vehicles as Devices​

Register each vehicle in your fleet. Set device_type explicitly: it defaults to mobile if omitted, which is wrong for a vehicle tracker. Valid values are mobile, vehicle, iot, tracker, and other.

curl -X POST https://api.spatialflow.io/api/v1/devices \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_id": "vehicle_101",
"name": "Delivery Van 101",
"device_type": "vehicle",
"metadata": {
"license_plate": "ABC123",
"driver": "John Doe",
"vehicle_type": "van",
"capacity_kg": 1000
}
}'

Bulk Registration​

There's no bulk device-registration endpoint or dashboard upload; create each device individually with POST /devices. (The dashboard's CSV import under a device's session history is for backfilling historical locations onto an existing device, not for registering new ones.)

Step 4: Send Location Updates​

Send vehicle location updates from your mobile app or GPS tracker.

Single Location Update​

Use the device UUID (id) returned from the registration response.

curl -X POST https://api.spatialflow.io/api/v1/devices/{device_uuid}/location \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"latitude": 37.7749,
"longitude": -122.4194,
"accuracy": 10,
"timestamp": "2025-11-10T12:00:00Z",
"speed": 12.5,
"heading": 180
}'

Batch Location Updates​

Send multiple updates efficiently:

note

The batch-update endpoint only processes devices owned by the authenticated user. Each device_id is matched against devices where the API key owner is the device creator.

curl -X POST https://api.spatialflow.io/api/v1/devices/batch-update \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"device_id": "vehicle_101",
"locations": [
{
"latitude": 37.7749,
"longitude": -122.4194,
"timestamp": "2025-11-10T12:00:00Z"
}
]
},
{
"device_id": "vehicle_102",
"locations": [
{
"latitude": 37.7850,
"longitude": -122.4100,
"timestamp": "2025-11-10T12:00:05Z"
}
]
}
]'

Mobile App Integration (React Native Example)​

import * as Location from "expo-location";

// Request location permissions
const { status } = await Location.requestForegroundPermissionsAsync();

// Start tracking
Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 30000, // Every 30 seconds
distanceInterval: 100, // Every 100 meters
},
async (location) => {
// Send to SpatialFlow
await fetch(
"https://api.spatialflow.io/api/v1/devices/{device_uuid}/location",
{
method: "POST",
headers: {
"X-API-KEY": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
latitude: location.coords.latitude,
longitude: location.coords.longitude,
accuracy: location.coords.accuracy,
timestamp: new Date(location.timestamp).toISOString(),
speed: location.coords.speed, // meters per second (top-level; coords.speed is already m/s)
heading: location.coords.heading,
}),
},
);
},
);

Step 5: Handle Webhook Events​

Build a backend endpoint to receive check-in/check-out events.

Express (Node.js) Backend​

const express = require("express");
const app = express();

app.use(express.json());

// Vehicle check-in endpoint
app.post("/api/vehicle-checkin", async (req, res) => {
const {
vehicle_id,
vehicle_name,
depot_id,
depot_name,
depot_code,
checkin_time,
location,
} = req.body;

// Log to database
await db.vehicleCheckins.insert({
vehicle_id,
depot_id,
depot_code,
checkin_time: new Date(checkin_time),
latitude: location.lat,
longitude: location.lng,
});

// Update vehicle status
await db.vehicles.update(vehicle_id, {
status: "at_depot",
current_depot: depot_id,
last_checkin: new Date(checkin_time),
});

// Calculate any outstanding deliveries
const pendingDeliveries = await db.deliveries.find({
vehicle_id,
status: "in_transit",
});

console.log(`✅ ${vehicle_name} checked in at ${depot_name}`);
console.log(` Pending deliveries: ${pendingDeliveries.length}`);

res.json({ success: true, vehicle_id });
});

// Vehicle check-out endpoint
app.post("/api/vehicle-checkout", async (req, res) => {
const { vehicle_id, depot_id, checkout_time } = req.body;

// Calculate dwell time
const checkin = await db.vehicleCheckins.findLast({ vehicle_id, depot_id });
const dwellMinutes =
(new Date(checkout_time) - new Date(checkin.checkin_time)) / 60000;

// Log checkout
await db.vehicleCheckouts.insert({
vehicle_id,
depot_id,
checkout_time: new Date(checkout_time),
dwell_minutes: dwellMinutes,
});

// Update vehicle status
await db.vehicles.update(vehicle_id, {
status: "on_route",
current_depot: null,
last_checkout: new Date(checkout_time),
});

console.log(
`🚪 Vehicle ${vehicle_id} departed after ${dwellMinutes} minutes`,
);

res.json({ success: true, dwell_minutes: dwellMinutes });
});

app.listen(3000, () => console.log("Backend listening on port 3000"));

Patterns for other use cases​

The five steps above are the same whether you're tracking delivery vehicles, field service technicians, a logistics fleet, or monitored assets: geofences at the locations that matter, a trigger and a webhook per event, and a device per tracked unit. These variations layer on top of that base.

Field service: multi-stop routes and SLA monitoring​

For a technician with several jobs in a day, create a geofence per job site before the route starts (delete or deactivate them once the job is done, so old sites don't add noise), and store the planned stop sequence in each geofence's metadata. Compare the arrival order your webhook receives against that sequence to catch route deviations.

To track SLA compliance, compare each arrival's timestamp to its scheduled time in your own backend:

function slaCompliance(scheduledTime, actualArrival) {
const delayMinutes = (actualArrival - scheduledTime) / 60000;
if (delayMinutes <= 0) return { status: "early", delay: 0 };
if (delayMinutes <= 15) return { status: "on_time", delay: delayMinutes };
if (delayMinutes <= 30) return { status: "late", delay: delayMinutes };
return { status: "sla_breach", delay: delayMinutes };
}

Logistics: ETAs and route corridors​

ETA from zone entry. When a driver enters a delivery zone, use the entry timestamp plus an average speed and the remaining stops' order to estimate each stop's arrival time, then push the updated ETA to each customer as a notification. This is ordinary backend logic on top of the geofence_enter webhook payload; SpatialFlow doesn't compute ETAs for you.

Route deviation. Draw a polygon around the expected route corridor and trigger on geofence_exit:

{
"triggerType": "geofence_exit",
"config": { "geofence_ids": ["<route-corridor-geofence-id>"] }
}

This catches a driver who leaves the corridor entirely. For deviation sensitivity tuned to an actual planned route rather than a hand-drawn polygon, use the route_deviation signal instead; see Signals.

Asset monitoring: theft prevention​

Geofence layering. Draw two concentric boundaries around a high-value asset's normal location: an outer warning zone and a tighter inner zone. Wire a geofence_exit workflow on each, and treat the inner-zone exit as the higher-priority alert (it means the asset left its expected spot, not just the wider yard).

Velocity anomaly. A location update carries a top-level speed field in meters per second. An asset that shouldn't move on its own (a generator, a tool chest) suddenly reporting vehicle speed is a theft signal; check incoming updates against an expected max speed for that asset type in your own backend, since SpatialFlow doesn't know which assets are supposed to be stationary.

Multi-asset correlation. If several assets exit the same boundary within a short window, escalate: it suggests an organized removal rather than one asset being moved for routine work.

Best Practices​

1. Update Frequency​

Balance accuracy with battery life and data usage:

Recommended intervals:

  • High accuracy mode: Every 10-30 seconds or 50-100 meters
  • Normal mode: Every 30-60 seconds or 100-200 meters
  • Power saver mode: Every 2-5 minutes or 500+ meters

2. Accuracy Filtering​

Filter out low-accuracy readings:

// Only send if accuracy is acceptable
if (location.coords.accuracy < 50) {
// Within 50 meters
await sendLocationUpdate(location);
}

3. Offline Handling​

Queue updates when offline:

const queue = [];

async function sendLocationUpdate(location) {
try {
await fetch(API_URL, { ... });
} catch (error) {
// Store for later if offline
queue.push(location);
}
}

// Retry queued updates when back online
window.addEventListener('online', async () => {
while (queue.length > 0) {
const location = queue.shift();
await sendLocationUpdate(location);
}
});

4. Geofence Sizing​

Size depot geofences appropriately:

  • Too small: Vehicles might not trigger if GPS drifts
  • Too large: Early/late triggers
  • Recommended: 50-100 meter radius around depot boundary

5. Dwell Time Calculation​

Calculate time spent at depot:

const checkinTime = new Date(checkin.timestamp);
const checkoutTime = new Date(checkout.timestamp);
const dwellMinutes = (checkoutTime - checkinTime) / 60000;

console.log(`Vehicle spent ${dwellMinutes} minutes at depot`);

Monitoring and Reporting​

View Active Vehicles​

curl -X GET https://api.spatialflow.io/api/v1/devices \
-H "X-API-KEY: YOUR_API_KEY"

Check Geofence Events​

curl -X GET https://api.spatialflow.io/api/v1/workflows/{workflow_id}/executions?limit=50 \
-H "X-API-KEY: YOUR_API_KEY"

Dashboard Metrics​

Track these KPIs:

  • Average dwell time per depot
  • Vehicle utilization (time on route vs. at depot)
  • Check-in/check-out frequency
  • Geofence entry/exit counts

Troubleshooting​

Vehicle Not Triggering Geofence​

Check:

  1. GPS accuracy is < 50 meters
  2. Vehicle actually crosses geofence boundary
  3. Geofence is active (is_active: true)
  4. Workflow is active and configured correctly

Test with point-in-polygon:

curl -X POST https://api.spatialflow.io/api/v1/geofences/test-point \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"geometry": {
"type": "Point",
"coordinates": [-122.4194, 37.7749]
}
}'

Duplicate Notifications​

Cause: GPS bounces at the geofence boundary.

Solution: SpatialFlow already smooths this out (a 25 m minimum movement, a 30 s minimum between transitions, and a 60 s cooldown per device and geofence), so duplicates from ordinary jitter shouldn't reach your webhook; see GPS Jitter Filtering. If you still see repeats, check whether your own webhook receiver retries on a slow response, since that looks the same as a duplicate delivery from SpatialFlow's side; see Handle Duplicates with Idempotency.

Delayed Notifications​

Check:

  • Webhook endpoint response time (the default timeout is 30 seconds)
  • Network connectivity
  • SpatialFlow workflow execution logs

Next Steps​

  • Geofences - Learn more about geofence types and properties
  • Workflows - Build complex multi-step automations
  • Devices - Advanced device management
  • Webhooks - Build robust webhook receivers
  • Errors - Handle failures gracefully