Webhook Integration Guide
Learn how to integrate SpatialFlow webhooks with popular services and build custom webhook receivers for real-time location event notifications.
Overview
Webhooks allow SpatialFlow to send real-time HTTP requests to your application when geofence events occur. This guide covers:
- Integrating with popular services (Slack, Discord, custom apps)
- Building custom webhook receivers
- Testing webhooks locally
- Production best practices
- Security and authentication
Quick Start
1. Create a Webhook in a Workflow
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Geofence Entry Notification",
"trigger": {
"type": "geofence_entry",
"geofence_id": "your-geofence-id"
},
"actions": [{
"type": "webhook",
"config": {
"url": "https://your-app.com/webhooks/geofence-entry",
"method": "POST",
"headers": {
"Content-Type": "application/json"
}
}
}]
}'
- Receive Webhook Events
SpatialFlow will POST events to your webhook URL with this payload:
{
"event": "geofence_entry",
"event_id": "evt_abc123",
"timestamp": "2025-11-10T12:00:00Z",
"geofence": {
"id": "gf_xyz789",
"name": "Downtown Office"
},
"device": {
"id": "dev_mobile123",
"name": "John's Phone"
},
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"accuracy": 10,
"timestamp": "2025-11-10T11:59:58Z"
},
"workflow": {
"id": "wf_456",
"name": "Geofence Entry Notification"
}
}
Integrating with Popular Services
Slack Integration
Send geofence events to Slack channels using Incoming Webhooks.
Step 1: Create Slack Incoming Webhook
- Go to Slack API
- Click "Create New App" → "From scratch"
- Enable "Incoming Webhooks"
- Click "Add New Webhook to Workspace"
- Select channel and authorize
- Copy the webhook URL
Step 2: Create SpatialFlow Workflow
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Notify Slack on Entry",
"trigger": {
"type": "geofence_entry",
"geofence_id": "your-geofence-id"
},
"actions": [{
"type": "slack",
"config": {
"webhook_url": "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
"message": "🚗 {{device.name}} entered {{geofence.name}}",
"channel": "#operations"
}
}]
}'
Example Slack Message:
🚗 John's Phone entered Downtown Office
Location: 37.7749, -122.4194
Time: 2025-11-10 12:00 PM
Discord Integration
Send notifications to Discord channels using webhooks.
Step 1: Create Discord Webhook
- Open Discord server settings
- Go to Integrations → Webhooks
- Click "New Webhook"
- Name it and select channel
- Copy webhook URL
Step 2: Create SpatialFlow Workflow
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Notify Discord on Entry",
"trigger": {
"type": "geofence_entry",
"geofence_id": "your-geofence-id"
},
"actions": [{
"type": "discord",
"config": {
"webhook_url": "https://discord.com/api/webhooks/YOUR/WEBHOOK",
"content": "**Device Alert**: {{device.name}} entered {{geofence.name}} at {{location.latitude}}, {{location.longitude}}"
}
}]
}'
Microsoft Teams Integration
Send notifications to Teams channels.
Step 1: Create Teams Incoming Webhook
- Open Teams channel → "..." → Connectors
- Search for "Incoming Webhook"
- Configure and create
- Copy webhook URL
Step 2: Create SpatialFlow Workflow
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Notify Teams on Entry",
"trigger": {
"type": "geofence_entry",
"geofence_id": "your-geofence-id"
},
"actions": [{
"type": "teams",
"config": {
"webhook_url": "https://outlook.office.com/webhook/YOUR/WEBHOOK",
"title": "Geofence Entry Alert",
"text": "{{device.name}} entered {{geofence.name}}"
}
}]
}'
Building a Custom Webhook Receiver
Flask (Python) Example
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
# Your webhook secret from SpatialFlow
WEBHOOK_SECRET = "your_webhook_secret_here"
def verify_signature(payload, signature):
"""Verify HMAC signature from SpatialFlow."""
expected_signature = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
provided_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, provided_signature)
@app.route('/webhooks/geofence-events', methods=['POST'])
def handle_geofence_event():
"""Handle incoming geofence event webhooks."""
# Get raw request body
payload = request.get_data()
# Verify signature
signature = request.headers.get('X-SF-Signature')
if not signature or not verify_signature(payload, signature):
return jsonify({"error": "Invalid signature"}), 401
# Parse event data
event = request.get_json()
# Process different event types
event_type = event.get('event')
if event_type == 'geofence_entry':
device_name = event['device']['name']
geofence_name = event['geofence']['name']
print(f"🚗 {device_name} entered {geofence_name}")
# Your custom logic here
# - Send email notification
# - Update database
# - Trigger other workflows
elif event_type == 'geofence_exit':
device_name = event['device']['name']
geofence_name = event['geofence']['name']
print(f"🚪 {device_name} exited {geofence_name}")
# Return success (2xx status code stops retries)
return jsonify({"status": "success", "event_id": event['event_id']}), 200
if __name__ == '__main__':
app.run(port=5000)
Express (Node.js) Example
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');
const app = express();
// Your webhook secret from SpatialFlow
const WEBHOOK_SECRET = 'your_webhook_secret_here';
// Use raw body parser for signature verification
app.use(bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
function verifySignature(payload, signature) {
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
const providedSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
}
app.post('/webhooks/geofence-events', (req, res) => {
// Verify signature
const signature = req.headers['x-sf-signature'];
if (!signature || !verifySignature(req.rawBody, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body;
const eventType = event.event;
// Process different event types
if (eventType === 'geofence_entry') {
const deviceName = event.device.name;
const geofenceName = event.geofence.name;
console.log(`🚗 ${deviceName} entered ${geofenceName}`);
// Your custom logic here
// - Send notification
// - Update database
// - Trigger actions
} else if (eventType === 'geofence_exit') {
const deviceName = event.device.name;
const geofenceName = event.geofence.name;
console.log(`🚪 ${deviceName} exited ${geofenceName}`);
}
// Return success (2xx stops retries)
res.json({ status: 'success', event_id: event.event_id });
});
app.listen(5000, () => {
console.log('Webhook receiver listening on port 5000');
});
Idempotency Handling
Always handle duplicate webhook deliveries:
# Store processed event IDs in database or cache
processed_events = set()
@app.route('/webhooks/geofence-events', methods=['POST'])
def handle_webhook():
event = request.get_json()
event_id = event['event_id']
# Check if already processed
if event_id in processed_events:
print(f"Duplicate event {event_id}, skipping")
return jsonify({"status": "already_processed"}), 200
# Process event
process_event(event)
# Mark as processed
processed_events.add(event_id)
return jsonify({"status": "success"}), 200
Testing Webhooks Locally
Option 1: webhook.site (Quick Testing)
- Go to webhook.site
- Copy your unique URL
- Use it in your workflow webhook action
- View incoming requests in real-time
Example:
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Test Webhook",
"trigger": {
"type": "geofence_entry",
"geofence_id": "test-geofence"
},
"actions": [{
"type": "webhook",
"config": {
"url": "https://webhook.site/your-unique-id",
"method": "POST"
}
}]
}'
Option 2: ngrok (Local Development)
Expose your local server to the internet for testing:
Step 1: Install ngrok
# Mac
brew install ngrok
# Or download from https://ngrok.com/download
Step 2: Start your local server
python app.py # Runs on localhost:5000
Step 3: Create tunnel
ngrok http 5000
Step 4: Use ngrok URL in workflow
# ngrok provides: https://abc123.ngrok.io
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Local Dev Webhook",
"actions": [{
"type": "webhook",
"config": {
"url": "https://abc123.ngrok.io/webhooks/geofence-events",
"method": "POST"
}
}]
}'
Production Best Practices
1. Always Verify Signatures
Never skip signature verification in production:
# ❌ BAD: No signature verification
@app.route('/webhook', methods=['POST'])
def handle_webhook():
event = request.get_json()
process_event(event) # Dangerous!
return "OK"
# ✅ GOOD: Verify signature
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-SF-Signature')
if not verify_signature(request.get_data(), signature):
return "Invalid signature", 401
event = request.get_json()
process_event(event)
return "OK"
2. Return Quickly (< 5 seconds)
Process webhooks asynchronously to avoid timeouts:
from celery import Celery
celery_app = Celery('tasks', broker='redis://localhost:6379')
@celery_app.task
def process_event_async(event):
"""Process event in background."""
# Your slow processing logic here
send_email(event)
update_database(event)
call_external_api(event)
@app.route('/webhooks/geofence-events', methods=['POST'])
def handle_webhook():
# Verify signature
if not verify_signature(...):
return "Invalid", 401
event = request.get_json()
# Queue for async processing
process_event_async.delay(event)
# Return immediately
return jsonify({"status": "queued"}), 200
3. Handle Retries Gracefully
SpatialFlow retries failed webhooks up to 7 times:
@app.route('/webhooks/geofence-events', methods=['POST'])
def handle_webhook():
try:
event = request.get_json()
# Check if already processed (idempotency)
if is_duplicate(event['event_id']):
return jsonify({"status": "already_processed"}), 200
# Process event
process_event(event)
# Return 2xx to stop retries
return jsonify({"status": "success"}), 200
except Exception as e:
# Log error
print(f"Error processing webhook: {e}")
# Return 5xx to trigger retry
return jsonify({"error": str(e)}), 500
4. Set Rate Limits
Protect your webhook endpoint from overload:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["1000 per hour", "100 per minute"]
)
@app.route('/webhooks/geofence-events', methods=['POST'])
@limiter.limit("60 per minute")
def handle_webhook():
# Your webhook logic
pass