Webhooks
Overview
Webhooks enable real-time notifications when geofencing events occur in SpatialFlow. When a device enters or exits a geofence, SpatialFlow can automatically send HTTP requests to your specified endpoint, allowing you to trigger custom workflows, update external systems, or send notifications through your own infrastructure.
What are Webhooks?
Webhooks are HTTP callbacks that SpatialFlow sends to your server when specific events occur. Rather than continuously polling our API for updates, webhooks provide a push-based mechanism where SpatialFlow proactively notifies your application of important events.
Common Use Cases
- Real-time alerts: Send notifications to your own systems when vehicles arrive at customer locations
- Integration triggers: Update CRM records, ticketing systems, or databases when geofence events occur
- Custom workflows: Trigger complex business logic in your own applications based on location events
- Audit logging: Maintain your own record of all geofencing activity for compliance
Webhook Payload
Every webhook delivery includes a JSON payload with standardized event information.
Request Structure
SpatialFlow makes HTTP POST requests to your webhook endpoint with the following characteristics:
- Method: POST (configurable to PUT, PATCH, or GET)
- Content-Type:
application/json(default, configurable) - Timeout: 30 seconds (default, configurable up to 60s)
- User-Agent:
SpatialFlow-Webhooks/1.0
Headers
Each webhook request includes security and metadata headers:
| Header | Description | Example |
|---|---|---|
X-SF-Signature | HMAC-SHA256 signature for verification | sha256=a1b2c3d4... |
X-SF-Event-ID | Unique identifier for the event | gf_abc123_enter_1729520000 |
X-Idempotency-Key | UUID for idempotent delivery handling | 550e8400-e29b-41d4-a716-446655440000 |
X-SF-Attempt | Current attempt number (1-7) | 1 |
Content-Type | Request content type | application/json |
User-Agent | SpatialFlow identifier | SpatialFlow-Webhooks/1.0 |
For backward compatibility, SpatialFlow also sends X-SpatialFlow-* headers. These are deprecated and will be removed in a future release. Please migrate to the X-SF-* header format.
Custom Headers
You can add custom headers to webhook requests when creating or updating a webhook:
{
"name": "My Webhook",
"url": "https://example.com/webhook",
"headers": {
"X-Custom-API-Key": "your-secret-key",
"X-Tenant-ID": "tenant-123"
}
}
Payload Format
The JSON body includes comprehensive event information:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event": "geofence.enter",
"timestamp": "2025-10-21T14:12:03Z",
"data": {
"event_id": "gf_abc123_enter_1729520000",
"user_id": "user-789",
"geofence_id": "gf_warehouse_a",
"geofence_name": "Warehouse A",
"event_type": "enter",
"point": {
"type": "Point",
"coordinates": [-73.756, 42.651]
},
"metadata": {
"geofence_metadata": {
"zone_type": "delivery",
"priority": "high"
},
"event_metadata": {
"device_type": "mobile",
"app_version": "2.1.0"
}
}
}
}
Event Types
geofence.enter- Device entered a geofencegeofence.exit- Device exited a geofencewebhook.test- Test event from webhook configuration
Webhook Security
Security is critical for webhook delivery. SpatialFlow implements multiple security measures to ensure authenticity and prevent tampering.
Request Signing
Every webhook request includes an HMAC-SHA256 signature in the X-SF-Signature header. This signature proves the request came from SpatialFlow and hasn't been modified in transit.
How Signatures Work
- SpatialFlow generates a unique secret when you create a webhook
- For each delivery, we compute an HMAC-SHA256 hash of the raw JSON payload using your secret
- The signature is sent in the
X-SF-Signatureheader assha256=<hex_digest> - Your endpoint should verify this signature before processing the webhook
Verification Examples
Python
import hmac
import hashlib
def verify_webhook_signature(request_body: bytes, signature_header: str, secret: str) -> bool:
"""
Verify webhook signature.
Args:
request_body: Raw request body bytes (don't parse JSON first!)
signature_header: Value of X-SF-Signature header
secret: Your webhook secret from SpatialFlow
Returns:
True if signature is valid, False otherwise
"""
if not signature_header.startswith("sha256="):
return False
received_signature = signature_header[7:] # Remove "sha256=" prefix
# Compute expected signature
computed_signature = hmac.new(
secret.encode('utf-8'),
request_body,
hashlib.sha256
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(computed_signature, received_signature)
# Usage with Flask
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-SF-Signature')
secret = "your-webhook-secret" # From SpatialFlow webhook config
if not verify_webhook_signature(request.data, signature, secret):
return {"error": "Invalid signature"}, 401
# Process webhook payload
payload = request.json
print(f"Received event: {payload['event']}")
return {"success": True}, 200
Node.js
const crypto = require('crypto');
const express = require('express');
function verifyWebhookSignature(requestBody, signatureHeader, secret) {
/**
* Verify webhook signature.
*
* @param {string|Buffer} requestBody - Raw request body
* @param {string} signatureHeader - Value of X-SF-Signature header
* @param {string} secret - Your webhook secret from SpatialFlow
* @returns {boolean} True if signature is valid
*/
if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
return false;
}
const receivedSignature = signatureHeader.substring(7); // Remove "sha256=" prefix
// Compute expected signature
const hmac = crypto.createHmac('sha256', secret);
hmac.update(requestBody);
const computedSignature = hmac.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(computedSignature)
);
}
// Usage with Express
const app = express();
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-sf-signature'];
const secret = process.env.WEBHOOK_SECRET; // From SpatialFlow
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).json({error: 'Invalid signature'});
}
// Process webhook payload
const payload = JSON.parse(req.body);
console.log(`Received event: ${payload.event}`);
res.json({success: true});
});
app.listen(3000, () => console.log('Webhook server running on port 3000'));
Always verify the signature before processing webhook data. Never trust webhook requests without verification, as malicious actors could attempt to spoof requests to your endpoint.
SpatialFlow sends outbound webhook deliveries with the X-SF-Signature header. If you are using SpatialFlow's inbound webhook verification (e.g., verifying third-party webhooks sent to SpatialFlow), the default header name is X-SpatialFlow-Signature. These are separate systems — most integrations only need X-SF-Signature for verifying deliveries from SpatialFlow.
Idempotency
The X-Idempotency-Key header contains a unique UUID for each delivery attempt. Use this key to prevent duplicate processing if SpatialFlow retries a delivery.
# Example: Track processed deliveries
processed_deliveries = set() # In production, use Redis or database
def handle_webhook(request):
idempotency_key = request.headers.get('X-Idempotency-Key')
# Check if already processed
if idempotency_key in processed_deliveries:
return {"status": "already_processed"}, 200
# Verify signature
if not verify_webhook_signature(request.body, request.headers.get('X-SF-Signature'), secret):
return {"error": "Invalid signature"}, 401
# Process webhook
payload = request.json()
process_event(payload)
# Mark as processed
processed_deliveries.add(idempotency_key)
return {"success": True}, 200
Delivery Guarantees
SpatialFlow implements robust delivery guarantees to ensure your webhooks are delivered reliably.
Retry Logic
If a webhook delivery fails, SpatialFlow automatically retries with exponential backoff:
| Attempt | Delay After Failure | Total Time Elapsed |
|---|---|---|
| 1 | 15 minutes | 0s |
| 2 | 30 minutes | 15m |
| 3 | 1 hour | 45m |
| 4 | 4 hours | 1h 45m |
| 5 | 4 hours | 5h 45m |
| 6 | 4 hours | 9h 45m |
| 7 | (final attempt) | 13h 45m |
Total retry window: Approximately 17 hours 45 minutes from first failure to final attempt.
Success Criteria
A webhook delivery is considered successful if:
- HTTP response status code is 2xx (200-299)
- Response received within timeout period (default 30 seconds)
Failure Conditions
A delivery is retried if:
- HTTP status code is not 2xx (200-299)
- Connection timeout (exceeds configured timeout)
- Connection error (DNS failure, network error, refused connection, etc.)
Dead Letter Queue (DLQ)
When a webhook fails after all 7 retry attempts, it's moved to the Dead Letter Queue (DLQ). The DLQ provides:
- Visibility into permanent failures
- Manual retry capability via API
- Error debugging information with full error details
Viewing Failed Deliveries
GET /api/v1/webhooks/dlq?limit=50&offset=0&requeued=false
Response includes failed deliveries with error details:
{
"results": [
{
"dlq_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_id": "660e9500-f39c-52e5-b827-557766551111",
"webhook": {
"id": "770fa600-g4ad-63f6-c938-668877662222",
"name": "Production Alerts",
"url": "https://example.com/webhook"
},
"event_type": "geofence.enter",
"event_id": "gf_abc123_enter_1729520000",
"error_message": "HTTP 500: Internal Server Error",
"retry_count": 7,
"last_attempt_at": "2025-10-21T14:30:00Z",
"queued_at": "2025-10-21T14:30:00Z",
"requeued": false
}
],
"pagination": {
"total": 12,
"limit": 50,
"offset": 0,
"has_more": false
}
}
Retrying from DLQ
After fixing the underlying issue (e.g., updating webhook URL or fixing your endpoint), retry failed deliveries:
POST /api/v1/webhooks/dlq/{dlq_id}/retry
This creates a new delivery attempt with a fresh retry counter.
Creating Webhook Endpoints
Requirements
Your webhook endpoint must:
- Respond quickly: Return a response within the configured timeout (default 30s)
- Return 2xx status: Any status code in the 200-299 range indicates success
- Handle retries: Process the same event multiple times safely (use
X-Idempotency-Key) - Verify signatures: Always verify the
X-SF-Signatureheader before processing
Best Practices
1. Respond Immediately
Process webhooks asynchronously. Acknowledge receipt immediately, then queue the actual work:
from flask import Flask, request
import queue
app = Flask(__name__)
work_queue = queue.Queue()
@app.route('/webhook', methods=['POST'])
def webhook():
# Verify signature
if not verify_signature(request.data, request.headers.get('X-SF-Signature')):
return {"error": "Invalid signature"}, 401
# Queue for async processing
work_queue.put(request.json)
# Respond immediately
return {"success": True}, 200
# Separate worker processes the queue
def process_webhook_queue():
while True:
payload = work_queue.get()
# Do expensive processing here
process_event(payload)
2. Implement Idempotency
Track processed deliveries to prevent duplicate processing:
from redis import Redis
redis = Redis()
def handle_webhook(request):
idempotency_key = request.headers.get('X-Idempotency-Key')
# Check Redis for processed key
if redis.exists(f"webhook:processed:{idempotency_key}"):
return {"status": "already_processed"}, 200
# Process webhook
process_event(request.json)
# Mark as processed (expire after 7 days)
redis.setex(f"webhook:processed:{idempotency_key}", 604800, "1")
return {"success": True}, 200
3. Handle Errors Gracefully
Log errors and return appropriate status codes:
@app.route('/webhook', methods=['POST'])
def webhook():
try:
# Verify signature
if not verify_signature(request.data, request.headers.get('X-SF-Signature')):
return {"error": "Invalid signature"}, 401
# Process webhook
result = process_event(request.json)
return {"success": True, "result": result}, 200
except ValidationError as e:
# Validation errors should not be retried
logger.error(f"Validation error: {e}")
return {"error": "Invalid payload"}, 400
except Exception as e:
# Unexpected errors trigger retries
logger.error(f"Webhook processing error: {e}")
return {"error": "Internal server error"}, 500
4. Monitor and Alert
Set up monitoring for webhook delivery failures:
import logging
from prometheus_client import Counter
webhook_received = Counter('webhook_received_total', 'Total webhooks received')
webhook_processed = Counter('webhook_processed_total', 'Total webhooks processed successfully')
webhook_failed = Counter('webhook_failed_total', 'Total webhook processing failures')
@app.route('/webhook', methods=['POST'])
def webhook():
webhook_received.inc()
try:
process_event(request.json)
webhook_processed.inc()
return {"success": True}, 200
except Exception as e:
webhook_failed.inc()
logger.error(f"Webhook failed: {e}")
return {"error": str(e)}, 500
Example Implementation
Complete webhook receiver with all best practices:
import hmac
import hashlib
import logging
from flask import Flask, request
from redis import Redis
app = Flask(__name__)
redis = Redis()
logger = logging.getLogger(__name__)
WEBHOOK_SECRET = "your-webhook-secret"
def verify_signature(body: bytes, signature: str) -> bool:
"""Verify HMAC signature"""
if not signature or not signature.startswith("sha256="):
return False
received = signature[7:]
computed = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, received)
@app.route('/webhook', methods=['POST'])
def webhook():
"""Handle SpatialFlow webhook"""
# 1. Verify signature
signature = request.headers.get('X-SF-Signature')
if not verify_signature(request.data, signature):
logger.warning("Invalid webhook signature")
return {"error": "Invalid signature"}, 401
# 2. Check idempotency
idempotency_key = request.headers.get('X-Idempotency-Key')
if redis.exists(f"webhook:processed:{idempotency_key}"):
logger.info(f"Duplicate webhook: {idempotency_key}")
return {"status": "already_processed"}, 200
# 3. Parse payload
try:
payload = request.json
event_type = payload.get('event')
event_data = payload.get('data', {})
logger.info(f"Received webhook: {event_type}")
# 4. Queue for async processing
redis.rpush('webhook:queue', request.data)
# 5. Mark as processed
redis.setex(f"webhook:processed:{idempotency_key}", 604800, "1")
# 6. Respond immediately
return {"success": True}, 200
except Exception as e:
logger.error(f"Webhook error: {e}")
return {"error": "Internal server error"}, 500
if __name__ == '__main__':
app.run(port=3000)
Testing Webhooks
Using Test Events
Trigger a test webhook delivery from the SpatialFlow dashboard or API:
POST /api/v1/webhooks/{webhook_id}/test
This sends a test event with the webhook.test event type.
Local Development
For local testing, use tools like ngrok to expose your local server:
# Start your webhook server locally
python webhook_server.py
# In another terminal, expose it
ngrok http 3000
# Use the ngrok URL in SpatialFlow
# https://abc123.ngrok.io/webhook
Debugging Tips
- Check signature verification: Ensure you're using the correct secret
- Verify raw body: Use the raw request body for signature verification, not parsed JSON
- Test with curl: Simulate webhook requests manually
- Check logs: Review webhook delivery logs in SpatialFlow dashboard
- Monitor DLQ: Check the Dead Letter Queue for failed deliveries
Monitoring & Debugging
Webhook Delivery Logs
View delivery history and status for each webhook:
GET /api/v1/webhooks/{webhook_id}/deliveries?limit=50
Response includes delivery attempts with timing and status:
{
"results": [
{
"id": "delivery-123",
"status": "success",
"event_type": "geofence.enter",
"response_status_code": 200,
"response_time_ms": 145.3,
"attempt_count": 1,
"created_at": "2025-10-21T14:12:03Z",
"delivered_at": "2025-10-21T14:12:03Z"
}
]
}
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Signature verification failed | Check webhook secret, verify you're using raw request body |
| Timeout | Response took longer than configured timeout | Implement async processing, return 200 immediately |
| Connection refused | Webhook URL unreachable | Verify URL is correct and server is running |
| SSL errors | Certificate issues | Ensure HTTPS endpoint has valid SSL certificate |
| 500 errors | Application error in your endpoint | Check application logs, fix bugs in webhook handler |
DLQ Statistics
Monitor Dead Letter Queue depth:
GET /api/v1/webhooks/dlq/stats
{
"total_entries": 12,
"not_requeued": 8,
"requeued": 4,
"top_failed_webhooks": [
{
"webhook_id": "770fa600-g4ad-63f6-c938-668877662222",
"webhook_name": "Production Alerts",
"count": 5
}
]
}
Set up alerts when DLQ depth exceeds thresholds.
Rate Limits
Webhook delivery respects configurable rate limits to protect your endpoints:
- Default: 60 requests per minute per webhook
- Configurable: Set custom rate limits when creating webhooks
- Enforcement: Requests exceeding rate limits are queued and delivered in subsequent windows
Configure rate limits when creating a webhook:
{
"name": "My Webhook",
"url": "https://example.com/webhook",
"rate_limit_per_minute": 120
}
Next Steps
- Workflows Guide: Learn how to use webhooks in automated workflows
- API Reference: Complete webhook API documentation
- Webhook Integration Guide: Sample webhook integrations with popular services
- Error Handling: Retry strategies and error recovery