Skip to main content

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"
}
}
}]
}'

  1. 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"
}
}

Slack Integration

Send geofence events to Slack channels using Incoming Webhooks.

Step 1: Create Slack Incoming Webhook

  1. Go to Slack API
  2. Click "Create New App" → "From scratch"
  3. Enable "Incoming Webhooks"
  4. Click "Add New Webhook to Workspace"
  5. Select channel and authorize
  6. 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

  1. Open Discord server settings
  2. Go to Integrations → Webhooks
  3. Click "New Webhook"
  4. Name it and select channel
  5. 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

  1. Open Teams channel → "..." → Connectors
  2. Search for "Incoming Webhook"
  3. Configure and create
  4. 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)

  1. Go to webhook.site
  2. Copy your unique URL
  3. Use it in your workflow webhook action
  4. 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

5. Monitor Webhook Health

Track webhook delivery success rates:

from prometheus_client import Counter, Histogram

webhook_received = Counter('webhooks_received_total', 'Total webhooks received')
webhook_processed = Counter('webhooks_processed_total', 'Successfully processed webhooks')
webhook_failed = Counter('webhooks_failed_total', 'Failed webhook processing')
webhook_duration = Histogram('webhook_processing_seconds', 'Webhook processing time')

@app.route('/webhooks/geofence-events', methods=['POST'])
def handle_webhook():
webhook_received.inc()

with webhook_duration.time():
try:
# Process webhook
process_event(request.get_json())
webhook_processed.inc()
return "OK", 200
except Exception as e:
webhook_failed.inc()
return "Error", 500

Security Best Practices

1. Use HTTPS Only

Never use HTTP for webhook URLs in production:

# ❌ BAD
"url": "http://yourapp.com/webhook"

# ✅ GOOD
"url": "https://yourapp.com/webhook"

2. Validate Event Structure

Check for required fields before processing:

def validate_event(event):
"""Validate webhook event structure."""
required_fields = ['event', 'event_id', 'timestamp', 'geofence', 'device']

for field in required_fields:
if field not in event:
raise ValueError(f"Missing required field: {field}")

# Validate event type
valid_events = ['geofence_entry', 'geofence_exit']
if event['event'] not in valid_events:
raise ValueError(f"Invalid event type: {event['event']}")

return True

@app.route('/webhooks/geofence-events', methods=['POST'])
def handle_webhook():
event = request.get_json()

try:
validate_event(event)
except ValueError as e:
return jsonify({"error": str(e)}), 400

# Process valid event
process_event(event)
return "OK", 200

3. Implement IP Allowlisting (Optional)

For extra security, allowlist SpatialFlow's webhook IPs:

ALLOWED_IPS = ['52.1.2.3', '52.1.2.4']  # Example IPs

@app.before_request
def check_ip():
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)

if client_ip not in ALLOWED_IPS:
return jsonify({"error": "Unauthorized IP"}), 403

Troubleshooting

Webhook Not Received

Check these common issues:

  1. Firewall blocking: Ensure your server accepts HTTPS traffic on port 443
  2. SSL certificate: Verify your SSL certificate is valid (no self-signed certs)
  3. Timeout: Endpoint must respond within 30 seconds
  4. Incorrect URL: Double-check webhook URL has no typos

Test connectivity:

curl -X POST https://your-app.com/webhooks/test \
-H "Content-Type: application/json" \
-d '{"test": "data"}'

Webhook Signature Verification Failing

Common causes:

  1. Wrong secret: Verify you're using the correct webhook secret
  2. Body modification: Don't parse JSON before verification - use raw body
  3. Character encoding: Use UTF-8 encoding

Debug verification:

print(f"Received signature: {signature}")
print(f"Expected signature: sha256={expected_signature}")
print(f"Raw body: {request.get_data()}")

Webhook Retries Exhausted

If webhooks fail all 7 retry attempts, check the Dead Letter Queue (DLQ):

# List failed webhooks
curl -X GET https://api.spatialflow.io/api/v1/webhooks/dlq \
-H "Authorization: Bearer YOUR_API_KEY"

Next Steps