Logistics and Fleet Management Guide
Build a logistics and fleet management system with SpatialFlow. This guide goes beyond basic vehicle tracking to cover delivery zone management, route corridor monitoring, automated customer notifications, ETA calculation patterns, and fleet-wide batch operations.
For basic vehicle entry/exit tracking, see the Vehicle Tracking Guide first.
Use Case
Manage delivery fleets with route monitoring and automated notifications:
- Create delivery zone geofences for distribution areas
- Monitor route compliance with corridor geofences
- Automate delivery notifications when drivers enter customer zones
- Calculate and broadcast ETAs using geofence proximity
- Track fleet-wide metrics across all delivery vehicles
Architecture Overview
Route Planner --> Planned Routes --> SpatialFlow API
|
Zone Geofences + Route Corridors
|
Fleet GPS --> Location Updates --> Geofence Engine
|
Workflow Triggers
|
Webhook --> Logistics Backend
| |
Customer Notifications Fleet Dashboard
Step 1: Create Delivery Zone Geofences
Set up multiple geofence types to cover different aspects of your delivery operations.
Delivery Zone
Create a neighborhood or district geofence for a delivery area:
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Delivery Zone - Lincoln Park",
"description": "Residential delivery zone covering Lincoln Park neighborhood",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-87.6500, 41.9200],
[-87.6500, 41.9320],
[-87.6340, 41.9320],
[-87.6340, 41.9200],
[-87.6500, 41.9200]
]]
},
"metadata": {
"zone_code": "CHI-LP-01",
"district": "lincoln_park",
"delivery_window": "08:00-18:00",
"driver_assigned": "truck_305"
}
}'
Response:
{
"id": "gf_zone_lincoln_park",
"name": "Delivery Zone - Lincoln Park",
"geometry": { "type": "Polygon", "coordinates": [[ ... ]] },
"metadata": {
"zone_code": "CHI-LP-01",
"district": "lincoln_park",
"delivery_window": "08:00-18:00",
"driver_assigned": "truck_305"
},
"is_active": true,
"created_at": "2025-11-12T07:00:00Z"
}
Route Corridor
Create a narrow corridor along a highway or road for route compliance monitoring:
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Route Corridor - I-90 Distribution Run",
"description": "Expected route corridor along I-90 from warehouse to Lincoln Park zone",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-87.7400, 41.8800],
[-87.7400, 41.8820],
[-87.6500, 41.9200],
[-87.6480, 41.9180],
[-87.7380, 41.8780],
[-87.7400, 41.8800]
]]
},
"metadata": {
"route_id": "RT-CHI-042",
"route_name": "Warehouse to Lincoln Park",
"expected_transit_minutes": 35,
"corridor_width_meters": 200
}
}'
Response:
{
"id": "gf_corridor_i90_lp",
"name": "Route Corridor - I-90 Distribution Run",
"geometry": { "type": "Polygon", "coordinates": [[ ... ]] },
"metadata": {
"route_id": "RT-CHI-042",
"route_name": "Warehouse to Lincoln Park",
"expected_transit_minutes": 35,
"corridor_width_meters": 200
},
"is_active": true,
"created_at": "2025-11-12T07:05:00Z"
}
Customer Location
Create a small geofence (~50m radius) around an individual delivery destination:
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Customer - 2150 N Clark St",
"description": "Delivery destination for order ORD-88421",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-87.6380, 41.9210],
[-87.6380, 41.9218],
[-87.6368, 41.9218],
[-87.6368, 41.9210],
[-87.6380, 41.9210]
]]
},
"metadata": {
"order_id": "ORD-88421",
"customer_name": "Alex Rivera",
"delivery_window": "10:00-12:00",
"package_count": 2,
"requires_signature": true
}
}'
Response:
{
"id": "gf_customer_clark_st",
"name": "Customer - 2150 N Clark St",
"geometry": { "type": "Polygon", "coordinates": [[ ... ]] },
"metadata": {
"order_id": "ORD-88421",
"customer_name": "Alex Rivera",
"delivery_window": "10:00-12:00",
"package_count": 2,
"requires_signature": true
},
"is_active": true,
"created_at": "2025-11-12T07:10:00Z"
}
Step 2: Set Up Delivery Workflows
Create workflows for each geofence type to automate different stages of the delivery process.
Zone Entry Workflow
Notify the logistics backend when a driver enters a delivery zone:
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Zone Entry - Lincoln Park",
"description": "Track driver entry into delivery zone",
"trigger": {
"type": "geofence_entry",
"geofence_id": "gf_zone_lincoln_park"
},
"actions": [
{
"type": "webhook",
"config": {
"url": "https://your-backend.com/api/zone-entry",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_BACKEND_API_KEY"
},
"body": {
"driver_id": "{{device.id}}",
"driver_name": "{{device.name}}",
"zone_id": "{{geofence.id}}",
"zone_code": "{{geofence.metadata.zone_code}}",
"entry_time": "{{event.timestamp}}"
}
}
}
]
}'
Customer Proximity Workflow
Trigger a customer notification when the driver enters the customer location geofence:
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Customer Proximity - ORD-88421",
"description": "Notify customer that delivery is arriving",
"trigger": {
"type": "geofence_entry",
"geofence_id": "gf_customer_clark_st"
},
"actions": [
{
"type": "webhook",
"config": {
"url": "https://your-backend.com/api/customer-proximity",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_BACKEND_API_KEY"
},
"body": {
"driver_id": "{{device.id}}",
"order_id": "{{geofence.metadata.order_id}}",
"customer_name": "{{geofence.metadata.customer_name}}",
"arrival_time": "{{event.timestamp}}",
"location": {
"lat": "{{location.latitude}}",
"lng": "{{location.longitude}}"
}
}
}
}
]
}'
Route Deviation Workflow
Alert fleet operations when a driver exits the expected route corridor:
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Route Deviation - I-90 Distribution Run",
"description": "Alert when driver deviates from planned route corridor",
"trigger": {
"type": "geofence_exit",
"geofence_id": "gf_corridor_i90_lp"
},
"actions": [
{
"type": "webhook",
"config": {
"url": "https://your-backend.com/api/route-deviation",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_BACKEND_API_KEY"
},
"body": {
"driver_id": "{{device.id}}",
"driver_name": "{{device.name}}",
"route_id": "{{geofence.metadata.route_id}}",
"route_name": "{{geofence.metadata.route_name}}",
"deviation_time": "{{event.timestamp}}",
"location": {
"lat": "{{location.latitude}}",
"lng": "{{location.longitude}}"
}
}
}
},
{
"type": "slack",
"config": {
"webhook_url": "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
"message": "ROUTE DEVIATION: {{device.name}} left corridor for {{geofence.metadata.route_name}} at {{event.timestamp}}",
"channel": "#fleet-ops"
}
}
]
}'
For advanced route deviation detection with sensitivity tuning, see Signals.
Step 3: Register Fleet Vehicles
Register each delivery vehicle in your fleet:
curl -X POST https://api.spatialflow.io/api/v1/devices \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_id": "truck_305",
"name": "Delivery Truck 305 - Downtown Route",
"metadata": {
"fleet_id": "CHI-FLEET-01",
"vehicle_type": "box_truck",
"max_capacity_kg": 4500,
"current_route": "RT-CHI-042",
"driver_name": "Marcus Chen",
"license_plate": "IL-DLV-3052"
}
}'
Response:
{
"id": "d_truck_305_uuid",
"device_id": "truck_305",
"name": "Delivery Truck 305 - Downtown Route",
"metadata": {
"fleet_id": "CHI-FLEET-01",
"vehicle_type": "box_truck",
"max_capacity_kg": 4500,
"current_route": "RT-CHI-042",
"driver_name": "Marcus Chen",
"license_plate": "IL-DLV-3052"
},
"is_active": true,
"created_at": "2025-11-12T07:30:00Z"
}
Step 4: Send Location Updates
Single Location Update
Send a location update with speed and heading metadata:
curl -X POST https://api.spatialflow.io/api/v1/devices/{device_uuid}/location \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"latitude": 41.9050,
"longitude": -87.6800,
"accuracy": 5,
"timestamp": "2025-11-12T09:15:00Z",
"metadata": {
"speed_kmh": 72,
"heading": 45,
"fuel_level_pct": 68
}
}'
Batch Updates for Fleet-Wide Tracking
Send location updates for multiple vehicles in a single request:
curl -X POST https://api.spatialflow.io/api/v1/devices/batch-update \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"device_id": "truck_305",
"locations": [
{
"latitude": 41.9050,
"longitude": -87.6800,
"timestamp": "2025-11-12T09:15:00Z",
"metadata": { "speed_kmh": 72, "heading": 45 }
}
]
},
{
"device_id": "truck_412",
"locations": [
{
"latitude": 41.8820,
"longitude": -87.6290,
"timestamp": "2025-11-12T09:15:02Z",
"metadata": { "speed_kmh": 55, "heading": 180 }
}
]
},
{
"device_id": "van_118",
"locations": [
{
"latitude": 41.9310,
"longitude": -87.6450,
"timestamp": "2025-11-12T09:15:05Z",
"metadata": { "speed_kmh": 30, "heading": 270 }
}
]
}
]'
GPS Tracker Integration
For hardware GPS trackers (vs. mobile app), configure your tracker's reporting endpoint to point to https://api.spatialflow.io/api/v1/devices/{device_uuid}/location. Most commercial fleet trackers (CalAmp, Queclink, Teltonika) support custom HTTP endpoints with configurable payloads. Map the tracker's fields to the SpatialFlow location schema in your tracker's configuration portal.
Step 5: Handle Webhook Events
Build backend endpoints to process delivery events from SpatialFlow.
Express (Node.js) Backend
const express = require('express');
const app = express();
app.use(express.json());
// Zone entry: driver entered a delivery zone
app.post('/api/zone-entry', async (req, res) => {
const { driver_id, driver_name, zone_id, zone_code, entry_time } = req.body;
// Update zone status
await db.zoneActivity.insert({
driver_id,
zone_id,
zone_code,
entry_time: new Date(entry_time),
status: 'active',
});
// Recalculate ETAs for remaining stops in this zone
const pendingDeliveries = await db.deliveries.find({
driver_id,
zone_code,
status: 'pending',
});
for (const delivery of pendingDeliveries) {
const eta = calculateETA(delivery, entry_time);
await db.deliveries.update(delivery.id, { estimated_arrival: eta });
await notifyCustomer(delivery.customer_id, `Your delivery ETA: ${eta}`);
}
console.log(`Driver ${driver_name} entered zone ${zone_code} with ${pendingDeliveries.length} pending deliveries`);
res.json({ success: true, pending_count: pendingDeliveries.length });
});
// Customer proximity: driver is near customer location
app.post('/api/customer-proximity', async (req, res) => {
const { driver_id, order_id, customer_name, arrival_time, location } = req.body;
// Send customer notification
await notificationService.send({
customer_name,
order_id,
type: 'sms',
message: `Your delivery is arriving! Driver is near your location.`,
});
// Update delivery status
await db.deliveries.update(order_id, {
status: 'arriving',
driver_arrival_time: new Date(arrival_time),
});
console.log(`Delivery ${order_id} arriving for ${customer_name}`);
res.json({ success: true, order_id, status: 'arriving' });
});
// Route deviation: driver left the expected corridor
app.post('/api/route-deviation', async (req, res) => {
const {
driver_id,
driver_name,
route_id,
route_name,
deviation_time,
location,
} = req.body;
// Log the deviation
await db.routeDeviations.insert({
driver_id,
route_id,
deviation_time: new Date(deviation_time),
latitude: location.lat,
longitude: location.lng,
});
// Alert fleet manager
await alertService.send({
severity: 'warning',
title: `Route deviation: ${driver_name}`,
message: `Driver left corridor for ${route_name} at ${deviation_time}`,
action_url: `/fleet/drivers/${driver_id}/live`,
});
// Optionally trigger rerouting
const activeDeliveries = await db.deliveries.find({
driver_id,
status: 'in_transit',
});
if (activeDeliveries.length > 0) {
await routeService.recalculate(driver_id, activeDeliveries);
}
console.log(`Route deviation: ${driver_name} left ${route_name}`);
res.json({ success: true, driver_id, route_id });
});
app.listen(3000, () => console.log('Logistics backend listening on port 3000'));
ETA Calculation Pattern
Use geofence proximity events to estimate and broadcast delivery arrival times.
Basic ETA from Zone Entry
When a driver enters a delivery zone, estimate arrival times for each stop within the zone:
function calculateETAForStops(zoneEntryTime, stops, averageSpeedKmh = 25) {
const sortedStops = stops.sort((a, b) => a.sequence - b.sequence);
let cumulativeMinutes = 0;
return sortedStops.map((stop) => {
// Estimate travel time between stops (simplified straight-line distance)
const distanceKm = haversineDistance(
stop.previousLat || sortedStops[0].lat,
stop.previousLng || sortedStops[0].lng,
stop.lat,
stop.lng
);
const travelMinutes = (distanceKm / averageSpeedKmh) * 60;
const serviceMinutes = stop.estimated_service_time || 5; // Default 5 min per stop
cumulativeMinutes += travelMinutes + serviceMinutes;
const eta = new Date(new Date(zoneEntryTime).getTime() + cumulativeMinutes * 60000);
return {
order_id: stop.order_id,
customer_name: stop.customer_name,
estimated_arrival: eta.toISOString(),
minutes_from_now: cumulativeMinutes,
};
});
}
// Haversine formula for distance between two coordinates
function haversineDistance(lat1, lng1, lat2, lng2) {
const R = 6371; // Earth radius in km
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLng = ((lng2 - lng1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLng / 2) *
Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
Broadcasting ETA Updates to Customers
Push updated ETAs to customers via webhooks whenever a delivery milestone occurs:
async function broadcastETAUpdate(driverId, zoneCode, eventTime) {
// Get remaining deliveries in this zone
const remaining = await db.deliveries.find({
driver_id: driverId,
zone_code: zoneCode,
status: { $in: ['pending', 'in_transit'] },
});
// Calculate ETAs for each remaining stop
const etas = calculateETAForStops(eventTime, remaining);
// Send ETA update to each customer
for (const eta of etas) {
await notificationService.send({
order_id: eta.order_id,
customer_name: eta.customer_name,
type: 'push',
message: `Estimated delivery: ${formatTime(eta.estimated_arrival)} (${Math.round(eta.minutes_from_now)} min)`,
});
}
return etas;
}
Refining ETAs with Real-Time Location
Recalculate ETAs each time a new location update is received, comparing the driver's current position against remaining stops:
async function refineETAs(driverId, currentLat, currentLng, currentTime) {
const remaining = await db.deliveries.find({
driver_id: driverId,
status: 'pending',
sort: { sequence: 1 },
});
let prevLat = currentLat;
let prevLng = currentLng;
let cumulativeMinutes = 0;
for (const delivery of remaining) {
const distanceKm = haversineDistance(prevLat, prevLng, delivery.lat, delivery.lng);
const travelMinutes = (distanceKm / 25) * 60; // 25 km/h urban average
cumulativeMinutes += travelMinutes + (delivery.service_minutes || 5);
const newETA = new Date(new Date(currentTime).getTime() + cumulativeMinutes * 60000);
// Only notify customer if ETA changed by more than 5 minutes
const previousETA = new Date(delivery.estimated_arrival);
const diffMinutes = Math.abs(newETA - previousETA) / 60000;
if (diffMinutes > 5) {
await db.deliveries.update(delivery.id, { estimated_arrival: newETA });
await notifyCustomer(delivery.customer_id, `Updated ETA: ${formatTime(newETA)}`);
}
prevLat = delivery.lat;
prevLng = delivery.lng;
}
}
Best Practices
1. Delivery Zone Sizing
Balance granularity with management overhead:
- Too many small zones: High maintenance, frequent zone entry/exit noise
- Too few large zones: ETAs become imprecise, lose visibility into driver progress
- Recommended: Zones covering 10-20 delivery stops each, roughly neighborhood-sized (0.5-2 km across)
2. Route Corridor Width
Account for lane changes, GPS drift, and minor detours:
- Highway corridors: 150-200 meter width (GPS drift at speed + lane width)
- Urban corridors: 100-150 meter width (more constrained roads)
- Last-mile corridors: Avoid corridors for last-mile (too many turns); use zone geofences instead
A corridor that is too narrow generates false deviation alerts. Start wider and tighten based on observed data.
3. Fleet-Wide Batch Updates
Use the batch endpoint for efficiency when tracking many vehicles:
// Collect updates from all fleet GPS devices
const batchPayload = fleetDevices.map((device) => ({
device_id: device.id,
locations: [
{
latitude: device.lastLat,
longitude: device.lastLng,
timestamp: device.lastTimestamp,
metadata: {
speed_kmh: device.speed,
heading: device.heading,
},
},
],
}));
// Send as single batch request
await fetch('https://api.spatialflow.io/api/v1/devices/batch-update', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(batchPayload),
});
4. Peak Hour Considerations
Adjust update frequency based on delivery activity:
- Active delivery window (8am-6pm): Update every 15-30 seconds
- In-transit between zones: Update every 30-60 seconds
- Idle/parked: Reduce to every 5 minutes to conserve battery and bandwidth
- Overnight: Disable tracking or reduce to every 30 minutes for security monitoring only
Monitoring and Reporting
Fleet Status
curl -X GET https://api.spatialflow.io/api/v1/devices \
-H "Authorization: Bearer YOUR_API_KEY"
Delivery Zone Activity
curl -X GET https://api.spatialflow.io/api/v1/workflows/{workflow_id}/executions?limit=50 \
-H "Authorization: Bearer YOUR_API_KEY"
Dashboard KPIs
Track these logistics metrics:
- Deliveries per hour: Average delivery throughput per driver and fleet-wide
- Route compliance %: Percentage of routes completed within the defined corridor
- Average delivery time: Time from zone entry to last delivery in zone
- On-time delivery rate: Percentage of deliveries within the customer's delivery window
- ETA accuracy: Compare predicted ETAs to actual arrival times
Troubleshooting
Route Deviation False Positives
Cause: Route corridor is too narrow for the road conditions or GPS accuracy at speed.
Solutions:
- Widen the corridor by 50-100 meters (start at 200m for highways)
- Add buffer zones at intersections where drivers may briefly exit the corridor
- Use dwell-based filtering: only flag deviations lasting > 60 seconds outside the corridor
- Consider removing corridor monitoring for last-mile segments where routes are inherently variable
Missed Customer Delivery Triggers
Cause: Customer location geofence is too small for the GPS accuracy in that area.
Solutions:
- Increase customer geofence radius to at least 50 meters (75-100m for suburban areas)
- Check GPS accuracy values in location updates (reject updates with accuracy > 30m for trigger purposes)
- Use the point-in-polygon test to verify the geofence covers the actual delivery location:
curl -X POST https://api.spatialflow.io/api/v1/geofences/test-point \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"geometry": {
"type": "Point",
"coordinates": [-87.6374, 41.9214]
}
}'
High-Frequency Updates Overwhelming Webhook Endpoint
Cause: Large fleet with frequent updates generates high webhook volume.
Solutions:
- Batch processing: Buffer incoming webhooks and process in batches every 5-10 seconds
- Respond with 200 immediately and process asynchronously via a job queue
- Use separate webhook endpoints for high-priority events (customer proximity) vs. informational events (zone entry)
- Implement backpressure: if your endpoint returns 429, SpatialFlow will retry with exponential backoff
Next Steps
- Geofences - Learn about geofence types, spatial queries, and bulk operations
- Workflows - Build complex multi-step automations with conditions and filters
- Devices - Advanced device management and fleet organization
- Webhook Integration - Build robust webhook receivers with retry handling
- Error Handling - Handle failures and edge cases gracefully
- Signals - Detect route deviation anomalies and fleet behavior patterns
- Vehicle Tracking - Basic vehicle entry/exit tracking setup