Skip to main content

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:

HeaderDescriptionExample
X-SF-SignatureHMAC-SHA256 signature for verificationsha256=a1b2c3d4...
X-SF-Event-IDUnique identifier for the eventgf_abc123_enter_1729520000
X-Idempotency-KeyUUID for idempotent delivery handling550e8400-e29b-41d4-a716-446655440000
X-SF-AttemptCurrent attempt number (1-7)1
Content-TypeRequest content typeapplication/json
User-AgentSpatialFlow identifierSpatialFlow-Webhooks/1.0
Legacy Headers

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 geofence
  • geofence.exit - Device exited a geofence
  • webhook.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

  1. SpatialFlow generates a unique secret when you create a webhook
  2. For each delivery, we compute an HMAC-SHA256 hash of the raw JSON payload using your secret
  3. The signature is sent in the X-SF-Signature header as sha256=<hex_digest>
  4. 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'));
Important

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.

Header Naming

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:

AttemptDelay After FailureTotal Time Elapsed
115 minutes0s
230 minutes15m
31 hour45m
44 hours1h 45m
54 hours5h 45m
64 hours9h 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-Signature header 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

  1. Check signature verification: Ensure you're using the correct secret
  2. Verify raw body: Use the raw request body for signature verification, not parsed JSON
  3. Test with curl: Simulate webhook requests manually
  4. Check logs: Review webhook delivery logs in SpatialFlow dashboard
  5. 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

IssueCauseSolution
401 UnauthorizedSignature verification failedCheck webhook secret, verify you're using raw request body
TimeoutResponse took longer than configured timeoutImplement async processing, return 200 immediately
Connection refusedWebhook URL unreachableVerify URL is correct and server is running
SSL errorsCertificate issuesEnsure HTTPS endpoint has valid SSL certificate
500 errorsApplication error in your endpointCheck 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