Asset Monitoring Guide
Build an end-to-end asset monitoring system with SpatialFlow. This guide walks through tracking high-value equipment and assets, setting up boundary geofences for authorized storage areas, detecting unauthorized movement with exit alerts, and automating theft prevention workflows with instant notifications.
Use Case
Track and protect high-value equipment across facilities and job sites:
- Track high-value equipment across job sites and warehouses
- Create boundary geofences for authorized storage areas
- Detect unauthorized asset movement with exit alerts
- Automate theft prevention workflows with instant notifications
- Monitor asset dwell time and utilization across locations
- Integrate with security systems via webhooks
Architecture Overview
Asset Tags/GPS --> Location Updates --> SpatialFlow API
|
Boundary Geofences
|
Geofence Engine
|
Workflow Triggers
/ \
Boundary Alert Normal Movement
|
Webhook --> Security Backend
/ \
SMS/Email Alert Lock Asset
Step 1: Create Boundary Geofences
Define boundaries around authorized asset storage locations. Each boundary type serves a different security purpose.
Warehouse/Yard Boundary
Create a perimeter around a storage facility to detect when assets leave the premises.
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Houston Warehouse - Main Yard",
"description": "Primary equipment storage yard perimeter",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-95.3710, 29.7604],
[-95.3710, 29.7620],
[-95.3685, 29.7620],
[-95.3685, 29.7604],
[-95.3710, 29.7604]
]]
},
"metadata": {
"site_id": "WH-HOU-001",
"site_name": "Houston Main Warehouse",
"security_level": "high",
"operating_hours": "06:00-22:00",
"boundary_type": "warehouse"
}
}'
Response:
{
"id": "gf_wh_houston_main",
"name": "Houston Warehouse - Main Yard",
"geometry": { "..." },
"metadata": {
"site_id": "WH-HOU-001",
"site_name": "Houston Main Warehouse",
"security_level": "high",
"operating_hours": "06:00-22:00",
"boundary_type": "warehouse"
},
"is_active": true,
"created_at": "2025-11-10T10:00:00Z"
}
Job Site Boundary
Create a temporary perimeter around an active job site where assets are deployed.
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Katy Freeway Extension - Site B",
"description": "Active construction site for highway expansion project",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-95.4900, 29.7780],
[-95.4900, 29.7810],
[-95.4850, 29.7810],
[-95.4850, 29.7780],
[-95.4900, 29.7780]
]]
},
"metadata": {
"project_id": "PROJ-2025-042",
"project_name": "Katy Freeway Extension",
"start_date": "2025-03-01",
"end_date": "2025-09-30",
"boundary_type": "job_site"
}
}'
Restricted Zone
Define a high-security area within a larger facility for sensitive or high-value equipment.
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Houston Warehouse - Secure Storage",
"description": "Restricted zone for high-value generators and heavy equipment",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-95.3705, 29.7608],
[-95.3705, 29.7614],
[-95.3695, 29.7614],
[-95.3695, 29.7608],
[-95.3705, 29.7608]
]]
},
"metadata": {
"zone_type": "restricted",
"clearance_required": "level_3",
"alarm_enabled": true,
"parent_site_id": "WH-HOU-001",
"boundary_type": "restricted_zone"
}
}'
Step 2: Set Up Alert Workflows
Configure workflows that trigger when assets cross boundary geofences.
Unauthorized Exit Alert (HIGH PRIORITY)
Triggers when any asset leaves the warehouse boundary. This is the primary theft prevention workflow.
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Unauthorized Exit - Houston Warehouse",
"description": "HIGH PRIORITY: Alert when asset leaves warehouse boundary",
"trigger": {
"type": "geofence_exit",
"geofence_id": "gf_wh_houston_main"
},
"actions": [
{
"type": "webhook",
"config": {
"url": "https://your-backend.com/api/boundary-alert",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_BACKEND_API_KEY"
},
"body": {
"alert_type": "unauthorized_exit",
"priority": "HIGH",
"asset_id": "{{device.id}}",
"asset_name": "{{device.name}}",
"boundary_id": "{{geofence.id}}",
"boundary_name": "{{geofence.name}}",
"exit_time": "{{event.timestamp}}",
"last_known_location": {
"lat": "{{location.latitude}}",
"lng": "{{location.longitude}}"
},
"site_id": "{{geofence.metadata.site_id}}",
"operating_hours": "{{geofence.metadata.operating_hours}}"
}
}
},
{
"type": "sms",
"config": {
"to": ["+15551234567", "+15559876543"],
"message": "ALERT: Asset {{device.name}} has left {{geofence.name}} at {{event.timestamp}}. Verify authorization immediately."
}
},
{
"type": "slack",
"config": {
"webhook_url": "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
"message": "BOUNDARY ALERT: Asset {{device.name}} exited {{geofence.name}}",
"channel": "#security-alerts"
}
}
]
}'
Asset Arrival Workflow
Triggers when an asset arrives at a job site. Logs the delivery and updates inventory.
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Asset Arrival - Katy Freeway Site",
"description": "Log asset delivery to job site",
"trigger": {
"type": "geofence_entry",
"geofence_id": "gf_katy_site_b"
},
"actions": [
{
"type": "webhook",
"config": {
"url": "https://your-backend.com/api/asset-arrival",
"method": "POST",
"body": {
"asset_id": "{{device.id}}",
"asset_name": "{{device.name}}",
"site_id": "{{geofence.metadata.project_id}}",
"site_name": "{{geofence.metadata.project_name}}",
"arrival_time": "{{event.timestamp}}",
"location": {
"lat": "{{location.latitude}}",
"lng": "{{location.longitude}}"
}
}
}
}
]
}'
After-Hours Movement Alert
Triggers when any asset leaves a boundary outside operating hours. Compare the event timestamp against the operating_hours metadata on the geofence to determine if the movement is authorized.
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "After-Hours Movement Detection",
"description": "Detect asset movement outside operating hours",
"trigger": {
"type": "geofence_exit",
"geofence_id": "gf_wh_houston_main"
},
"actions": [
{
"type": "webhook",
"config": {
"url": "https://your-backend.com/api/after-hours-check",
"method": "POST",
"body": {
"asset_id": "{{device.id}}",
"asset_name": "{{device.name}}",
"boundary_id": "{{geofence.id}}",
"event_time": "{{event.timestamp}}",
"operating_hours": "{{geofence.metadata.operating_hours}}",
"security_level": "{{geofence.metadata.security_level}}"
}
}
}
]
}'
Tip: For automated after-hours anomaly detection based on historical movement patterns, see Signals. Signals can learn normal asset movement windows and flag deviations without manual operating-hours configuration.
Step 3: Register Assets as Devices
Register each tracked asset with descriptive metadata for identification and classification.
Heavy Equipment
curl -X POST https://api.spatialflow.io/api/v1/devices \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_id": "asset_gen_500",
"name": "Generator 500kW - Unit A",
"metadata": {
"asset_type": "generator",
"serial_number": "GEN-2024-50021",
"value_usd": 85000,
"assigned_site": "WH-HOU-001",
"last_maintenance": "2025-08-15",
"weight_kg": 4500,
"fuel_type": "diesel"
}
}'
Construction Equipment
curl -X POST https://api.spatialflow.io/api/v1/devices \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_id": "asset_exc_102",
"name": "Excavator CAT 320 - Unit B",
"metadata": {
"asset_type": "excavator",
"serial_number": "EXC-2023-10245",
"value_usd": 250000,
"assigned_site": "PROJ-2025-042",
"last_maintenance": "2025-09-01",
"operating_weight_kg": 22000,
"manufacturer": "Caterpillar"
}
}'
Step 4: Send Location Updates
Integrate GPS trackers or asset tags to send location updates to SpatialFlow.
Single Location Update
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": 29.7612,
"longitude": -95.3698,
"accuracy": 5,
"timestamp": "2025-11-10T14:30:00Z",
"metadata": {
"battery_level": 87,
"signal_strength": -72,
"tracker_type": "cellular"
}
}'
Update Frequency Strategy
Asset trackers should adjust their reporting frequency based on context:
Stationary assets (equipment stored in yard):
- Report every 5-15 minutes
- Conserves battery while maintaining boundary awareness
- Increase frequency if motion is detected
Assets in transit (being transported between sites):
- Report every 30 seconds
- Provides continuous tracking during movement
- Enables route monitoring and theft detection
// Tracker firmware logic (pseudocode)
if (motionDetected()) {
// Switch to high-frequency mode
setReportingInterval(30); // seconds
sendUpdate({ latitude, longitude, metadata: { motion: true } });
} else {
// Stationary mode - conserve battery
setReportingInterval(600); // 10 minutes
sendUpdate({ latitude, longitude, metadata: { motion: false } });
}
Hardware Tracker Integration
SpatialFlow accepts location updates from any GPS-capable device via the REST API. Common tracker types:
| Tracker Type | Best For | Battery Life | Accuracy |
|---|---|---|---|
| Cellular GPS | High-value mobile assets | 1-3 years | 3-10m |
| LoRaWAN | Large-area fixed sites | 3-5 years | 10-50m |
| BLE Beacons | Indoor/warehouse tracking | 1-2 years | 1-5m |
| Satellite | Remote/off-grid locations | 1-2 years | 5-15m |
Most trackers can be configured to POST location data to your middleware, which then forwards updates to the SpatialFlow API in the expected format.
Step 5: Handle Webhook Events
Build backend endpoints to process boundary alerts and asset movement events.
Express.js Security Backend
const express = require('express');
const app = express();
app.use(express.json());
// CRITICAL: Boundary alert handler
app.post('/api/boundary-alert', async (req, res) => {
const {
alert_type,
priority,
asset_id,
asset_name,
boundary_id,
boundary_name,
exit_time,
last_known_location,
site_id,
operating_hours,
} = req.body;
// Check if movement is during operating hours
const isAuthorized = checkOperatingHours(exit_time, operating_hours);
if (!isAuthorized) {
console.log(`UNAUTHORIZED EXIT: ${asset_name} left ${boundary_name} outside operating hours`);
// Cross-reference with dispatch system
const hasApprovedTransfer = await db.transfers.findOne({
asset_id,
status: 'approved',
scheduled_date: new Date(exit_time).toISOString().split('T')[0],
});
if (!hasApprovedTransfer) {
// UNAUTHORIZED MOVEMENT - trigger lockdown
await triggerLockdown(asset_id, {
reason: 'unauthorized_exit',
boundary_id,
exit_time,
location: last_known_location,
});
// Alert security team
await notifySecurityTeam({
type: 'theft_alert',
asset_id,
asset_name,
last_known_location,
exit_time,
site_id,
});
// Log incident
await db.incidents.insert({
type: 'unauthorized_movement',
asset_id,
boundary_id,
exit_time: new Date(exit_time),
location: last_known_location,
status: 'open',
priority: 'critical',
});
}
}
res.json({ received: true, authorized: isAuthorized });
});
// Asset arrival handler
app.post('/api/asset-arrival', async (req, res) => {
const { asset_id, asset_name, site_id, site_name, arrival_time, location } = req.body;
// Update inventory
await db.inventory.upsert({
asset_id,
current_site: site_id,
arrived_at: new Date(arrival_time),
status: 'on_site',
});
// Confirm delivery against manifest
const manifest = await db.transferManifests.findOne({
asset_id,
destination_site: site_id,
status: 'in_transit',
});
if (manifest) {
await db.transferManifests.update(manifest.id, {
status: 'delivered',
actual_arrival: new Date(arrival_time),
});
console.log(`Delivery confirmed: ${asset_name} arrived at ${site_name}`);
}
res.json({ received: true, asset_id });
});
// Asset departure handler
app.post('/api/asset-departure', async (req, res) => {
const { asset_id, asset_name, site_id, departure_time } = req.body;
// Check against approved transfers
const approvedTransfer = await db.transfers.findOne({
asset_id,
origin_site: site_id,
status: 'approved',
});
if (!approvedTransfer) {
console.warn(`UNAPPROVED DEPARTURE: ${asset_name} leaving ${site_id}`);
await db.incidents.insert({
type: 'unapproved_departure',
asset_id,
site_id,
departure_time: new Date(departure_time),
status: 'open',
});
}
// Log departure
await db.inventory.update({
asset_id,
current_site: site_id,
}, {
status: 'departed',
departed_at: new Date(departure_time),
});
res.json({ received: true, approved: !!approvedTransfer });
});
// Helper: Check if time falls within operating hours
function checkOperatingHours(eventTime, operatingHours) {
if (!operatingHours) return true; // No restriction
const [start, end] = operatingHours.split('-');
const eventDate = new Date(eventTime);
const eventHour = eventDate.getUTCHours();
const eventMinute = eventDate.getUTCMinutes();
const eventTimeMinutes = eventHour * 60 + eventMinute;
const [startH, startM] = start.split(':').map(Number);
const [endH, endM] = end.split(':').map(Number);
const startMinutes = startH * 60 + startM;
const endMinutes = endH * 60 + endM;
return eventTimeMinutes >= startMinutes && eventTimeMinutes <= endMinutes;
}
// Helper: Trigger asset lockdown
async function triggerLockdown(assetId, details) {
// Send lock command to asset tracker (if supported)
await fetch(`https://tracker-api.example.com/assets/${assetId}/lock`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(details),
});
}
// Helper: Notify security team
async function notifySecurityTeam(alert) {
await fetch('https://your-sms-service.com/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: ['+15551234567', '+15559876543'],
message: `THEFT ALERT: ${alert.asset_name} unauthorized movement detected at ${alert.exit_time}. Last location: ${alert.last_known_location.lat}, ${alert.last_known_location.lng}`,
}),
});
}
app.listen(3000, () => console.log('Security backend listening on port 3000'));
Theft Prevention Patterns
Advanced strategies for protecting high-value assets using SpatialFlow's geofencing capabilities.
Geofence Layering
Create multiple concentric boundaries around high-value assets. The outer boundary triggers a warning; the inner boundary triggers a lockdown.
# Outer warning zone (larger perimeter)
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Houston Warehouse - Warning Zone",
"description": "Outer perimeter warning zone",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-95.3720, 29.7598],
[-95.3720, 29.7626],
[-95.3675, 29.7626],
[-95.3675, 29.7598],
[-95.3720, 29.7598]
]]
},
"metadata": {
"zone_type": "warning",
"parent_site_id": "WH-HOU-001",
"alert_level": "medium"
}
}'
# Inner secure zone (tight perimeter) — reuses the warehouse boundary
# When asset exits inner zone: immediate HIGH alert
# When asset exits outer zone: CRITICAL escalation
Create separate workflows for each layer:
- Inner zone exit triggers a HIGH priority investigation alert
- Outer zone exit triggers a CRITICAL lockdown with security dispatch
Time-Based Rules
Compare exit events against operating hours stored in geofence metadata.
function evaluateExitEvent(event, geofenceMetadata) {
const exitTime = new Date(event.timestamp);
const { operating_hours, security_level } = geofenceMetadata;
const isWithinHours = checkOperatingHours(event.timestamp, operating_hours);
const isWeekend = [0, 6].includes(exitTime.getUTCDay());
if (!isWithinHours || isWeekend) {
return {
authorized: false,
reason: isWeekend ? 'weekend_movement' : 'after_hours_movement',
severity: security_level === 'high' ? 'critical' : 'high',
};
}
return { authorized: true };
}
Velocity Detection
If a stationary asset suddenly moves at vehicle speed (e.g., a generator on a truck), flag it as potential theft. Include speed data in location update metadata.
function checkVelocityAnomaly(asset, locationUpdate) {
const { speed_kmh } = locationUpdate.metadata || {};
const { asset_type } = asset.metadata || {};
// Expected max speeds by asset type
const maxExpectedSpeeds = {
generator: 5, // Should not move on its own
excavator: 15, // Slow-moving equipment
trailer: 10, // Parked trailers
tool_chest: 3, // Essentially stationary
};
const maxSpeed = maxExpectedSpeeds[asset_type] || 10;
if (speed_kmh && speed_kmh > maxSpeed) {
return {
anomaly: true,
reason: `${asset_type} moving at ${speed_kmh} km/h (max expected: ${maxSpeed} km/h)`,
severity: speed_kmh > maxSpeed * 3 ? 'critical' : 'high',
};
}
return { anomaly: false };
}
Multi-Asset Correlation
If multiple assets leave the same boundary within a short time window, escalate the alert priority. This pattern detects organized theft attempts.
async function checkMultiAssetExit(boundaryId, exitTime, windowMinutes = 15) {
const windowStart = new Date(new Date(exitTime) - windowMinutes * 60 * 1000);
const recentExits = await db.boundaryEvents.find({
boundary_id: boundaryId,
event_type: 'exit',
timestamp: { $gte: windowStart, $lte: new Date(exitTime) },
});
if (recentExits.length >= 3) {
return {
escalate: true,
reason: `${recentExits.length} assets exited ${boundaryId} within ${windowMinutes} minutes`,
severity: 'critical',
affected_assets: recentExits.map(e => e.asset_id),
};
}
return { escalate: false, recent_count: recentExits.length };
}
Best Practices
1. Boundary Sizing
Size geofences based on asset value and mobility:
- High-value stationary assets (generators, heavy equipment): Tight boundaries, 10-20 meter buffer
- Mobile equipment (vehicles, trailers): Wider boundaries, 50-100 meter buffer
- Indoor assets (tool chests, electronics): Use BLE beacon zones instead of GPS boundaries
2. Update Frequency by Asset Type
Balance tracking accuracy with battery life:
| Asset Type | Stationary Interval | In-Transit Interval | Notes |
|---|---|---|---|
| Heavy equipment | 10-15 min | 30 sec | High-value, long battery |
| Vehicles/trailers | 5 min | 15-30 sec | Frequent movement |
| Portable tools | 30 min | 1-2 min | Battery conservation priority |
| BLE-tagged items | Continuous (passive) | N/A | Beacon-based, no battery drain |
3. Battery Management
GPS trackers have limited battery life. Optimize reporting to maximize uptime:
- Use motion-activated reporting (sleep when stationary, wake on movement)
- Monitor battery level in location metadata and alert at 20% threshold
- Schedule periodic "heartbeat" pings even when stationary (every 30-60 minutes)
- Set up a workflow to alert operations when
battery_leveldrops below threshold
4. Geofence Metadata for Authorization
Store authorization rules directly in geofence metadata for real-time evaluation:
operating_hours: Time window for authorized movementclearance_required: Minimum clearance level to move assetsalarm_enabled: Whether boundary exit triggers physical alarmsauthorized_carriers: List of approved transport companies
5. Alert Fatigue Prevention
Prevent security teams from ignoring alerts due to volume:
- Severity levels: Only page on-call for CRITICAL alerts; batch LOW/MEDIUM for daily review
- Grouping: Combine related alerts (e.g., asset exits inner then outer zone = one incident)
- Quiet hours for known movements: Suppress alerts for pre-approved transfers
- Cooldown periods: After an alert is acknowledged, suppress duplicates for 15 minutes
Monitoring and Reporting
Asset Inventory
List all tracked assets with metadata filters:
curl -X GET "https://api.spatialflow.io/api/v1/devices?metadata.asset_type=generator" \
-H "Authorization: Bearer YOUR_API_KEY"
Boundary Event History
Review recent workflow executions for a boundary alert workflow:
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 metrics for asset monitoring effectiveness:
| KPI | Description | Target |
|---|---|---|
| Assets in boundary | Percentage of assets currently within their assigned boundary | > 98% |
| Unauthorized movement count | Number of boundary exits without approved transfer | 0 per week |
| Avg time-on-site | Average duration assets spend at each location | Varies by project |
| Asset utilization rate | Percentage of time assets are actively in use vs. idle | > 70% |
| Alert response time | Time from boundary alert to security acknowledgment | < 5 minutes |
Troubleshooting
False Boundary Alerts
Cause: GPS drift causes stationary assets to appear outside their boundary, especially near walls or under metal structures.
Solutions:
- Add a 10-20 meter buffer to boundary geofences
- Require two consecutive outside-boundary readings before triggering an alert
- Use accuracy filtering: ignore updates with accuracy > 30 meters
- For warehouse interiors, supplement GPS with BLE beacons
Tracker Battery Dies
Cause: GPS reporting frequency drains battery faster than expected.
Solutions:
- Monitor
battery_levelin location metadata - Set up a workflow alert when battery drops below 20%
- Reduce reporting frequency for low-priority assets
- Schedule battery replacement based on historical drain rates
Indoor Tracking Limitations
Cause: GPS signals are weak or unavailable inside warehouses and covered facilities.
Solutions:
- Deploy BLE beacons at warehouse entry/exit points for zone detection
- Use Wi-Fi positioning as a fallback for indoor areas
- Place GPS boundary geofences at building entrances rather than around equipment bays
- Combine GPS (outdoor) with BLE (indoor) for full coverage
Next Steps
- Geofences — Learn about geofence types, properties, and spatial queries
- Workflows — Build multi-step automations with conditional logic
- Devices — Advanced device management and metadata
- Webhook Integration — Build robust webhook receivers with retries
- Error Handling — Handle API errors and edge cases gracefully
- Signals — Configure anomaly detection for asset movement patterns
- Best Practices — Production security patterns and performance tuning