Webhooks
This page covers the workspace webhook, created through the API and independent of any workflow. A workflow's Webhook (HTTP Request) action, documented in Workflows, is a separate feature with its own signing and retry contract; see Workflow Webhook action vs. workspace webhook below for the differences.
Overview
A workspace webhook is a signed HTTP endpoint with retries, a delivery log, and a Dead Letter Queue. You create one through the API, and it receives test deliveries only for now: the test event and geofence test endpoints send to it, and real geofence events are not delivered to it. To receive real geofence events at your own endpoint, add a Webhook action to a workflow; Settings > Webhooks in the dashboard links there. See Webhook (HTTP Request) for the action and its signing.
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 (use a workflow Webhook action for real events)
- 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
Workflow Webhook action vs. workspace webhook
SpatialFlow has two ways to send an outbound HTTP request, and they don't share a signing or retry contract:
| Workspace webhook (this page) | Workflow Webhook action | |
|---|---|---|
| Configured | The API (POST /api/v1/webhooks/), independent of any workflow | An action node (actionType: "webhook") inside a workflow, connected to a trigger |
| Fires on | Test deliveries only for now, sent from the test endpoints. Real geofence events are not delivered to it | Whatever trigger you connect it to |
| Signing | Always signs. Header X-SF-Signature, HMAC-SHA256 of the raw body, no timestamp | Opt-in. Send through a webhook integration with a webhook_secret to get X-SpatialFlow-Signature, HMAC-SHA256 of <timestamp>.<body>, plus X-SpatialFlow-Timestamp. No secret means no signature header at all |
| Retries | Up to 7 retries (8 attempts total) on a fixed schedule, then the Dead Letter Queue | Up to 3 attempts by default, retrying connection failures, timeouts and 5xx or 429 responses only. retry_count in the action config sets the retries: 0 sends once, 1 to 10 gives 2 to 11 attempts. No Dead Letter Queue |
The rest of this page documents the workspace webhook. For the workflow action's other settings (headers, auth, timeout), see Webhook (HTTP Request) in the Workflows reference.
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 300s)
- 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 (not signed) | gf_abc123_enter_1729520000 |
X-Idempotency-Key | Delivery UUID, the same value as the body's id (not signed) | 550e8400-e29b-41d4-a716-446655440000 |
X-SF-Attempt | Current attempt number (1-8) | 1 |
Content-Type | Request content type | application/json |
User-Agent | SpatialFlow identifier | SpatialFlow-Webhooks/1.0 |
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": "770fa600-g4ad-63f6-c938-668877662222",
"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
For now only the test endpoints send these to a workspace webhook. A workflow Webhook action sends the body you write, not these event types.
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
Getting the signing secret
SpatialFlow shows a webhook's secret once, when you create the webhook: the 201 response to POST /api/v1/webhooks/ includes a secret field. Listing, getting and updating a webhook never return it.
Keep the secret where your endpoint can read it, such as an environment variable or a secret manager. If you don't have a webhook's secret, because you lost it or the webhook was created before SpatialFlow showed secrets, rotate it to get a new one.
Rotating the secret
Owners and managers can replace a webhook's secret at any time:
POST /api/v1/webhooks/{webhook_id}/rotate-secret
Authorization: Bearer YOUR_JWT_TOKEN
The response is the webhook with its new secret, which is shown only this once. Rotation takes effect immediately, with no overlap period: every delivery signed from then on uses the new secret, including retries of events that failed earlier, and the old secret stops matching. Update your endpoint right after you rotate. Deliveries it rejects in the meantime, including one that was already on its way when you rotated, go through the usual retry schedule and are signed with the new secret when they're retried.
Verification Examples
These verifiers check the signature only. The workspace webhook signature has no timestamp, so they cannot reject an old request. To defend against replays, deduplicate on the signed id as described under Idempotency.
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 or 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 of bytes; comparing str raises TypeError on a
# non-ASCII header instead of returning False.
return hmac.compare_digest(computed_signature.encode(), received_signature.encode())
# Usage with Flask
from flask import Flask, request
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 # Flask answers 413 for larger bodies
@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; timingSafeEqual throws on buffers of different lengths
const received = Buffer.from(receivedSignature);
const computed = Buffer.from(computedSignature);
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
// Usage with Express
const app = express();
app.post('/webhook', express.raw({type: 'application/json', limit: '1mb'}), (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 receiver (POST /api/v1/webhooks/receive/{webhook_id}, for third-party webhooks sent to SpatialFlow), the signature is read from the X-Webhook-Signature header (with an X-Webhook-Timestamp companion). These are separate systems; most integrations only need X-SF-Signature for verifying deliveries from SpatialFlow.
Idempotency
The id field of the signed body is a unique UUID for each delivery, and a retry of the same delivery carries the same id. Verify the signature first, then deduplicate on that id so processing happens once. SpatialFlow records any 2xx response as a successful delivery and does not send it again, so acknowledge only work that is committed.
The X-Idempotency-Key and X-SF-Event-ID headers are not covered by the signature. Anyone who holds a valid delivery can resend the same body and signature with a different header, so deduplicating on a header does not stop a replay.
The default payload carries a signed id, the delivery UUID. A custom_payload_template replaces the whole body with the rendered template, so the template must include a stable id for replay protection: "id": "{{ event.id }}". In a template, event is the default payload, and data, timestamp, and event_type are also available. If a verified body has no id, reject it with a 400 instead of processing it without replay protection.
Record the id in the same transaction as the work. Insert the id with a targeted conflict clause. If no row comes back, the delivery was already processed, so return 200 without doing the work. Otherwise do the work in the same transaction. Any exception rolls back the marker and the work together and returns a non-2xx response, so the retry processes the event. Do not catch a broad integrity error around the work: a constraint failure inside process_event must not look like a duplicate.
def handle_webhook(request):
# Verify signature first
if not verify_webhook_signature(request.body, request.headers.get('X-SF-Signature'), secret):
return {"error": "Invalid signature"}, 401
payload = request.json()
delivery_id = payload.get('id')
if not delivery_id:
# A custom_payload_template without an id leaves nothing to deduplicate on
return {"error": "Missing delivery id"}, 400
with db.transaction():
# processed_webhooks.id is the primary key
row = db.execute(
"INSERT INTO processed_webhooks (id) VALUES (%s) "
"ON CONFLICT (id) DO NOTHING RETURNING id",
[delivery_id],
).fetchone()
if row is None:
return {"status": "already_processed"}, 200
process_event(payload) # its writes join the same transaction
# An exception rolls back the marker and the work, and surfaces as a 5xx
return {"success": True}, 200
A queue counts as accepting the work only if it has persisted the message before you respond. Redis does that only with appendfsync always. Do not claim the id in a cache before the work runs: a crash, or a retry that arrives while the first request is still running, would get a 200 for work that never finished.
The receiver under Example Implementation uses the transaction pattern.
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 | 4 hours | 13h 45m |
| 8 | (final attempt) | 17h 45m |
The delivery is tried once, then retried up to 7 times (8 attempts total) on the fixed schedule [15m, 30m, 1h, 4h, 4h, 4h, 4h] (±10% jitter per retry). This schedule is a platform-wide setting, not something a webhook's own configuration changes.
Total retry window: Approximately 17 hours 45 minutes from first failure to final attempt.
max_retries on the webhook actually controlsA webhook has its own max_retries field (default 3, 0 to 10), but it doesn't change the automatic retry schedule above, which always runs its full 7 retries regardless of this value. max_retries instead gates the manual retry endpoint: once a delivery's attempt count reaches it, a manual retry needs {"force": true} in the body. In practice this means a delivery still in its automatic retry cycle can already require force for a manual retry, well before it reaches the DLQ.
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 all 8 delivery attempts (the initial try plus 7 retries), 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. Because a DLQ'd delivery has already used all of its attempts, you must pass {"force": true} in the body to re-run it:
POST /api/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/retry
Content-Type: application/json
{"force": true}
This creates a new delivery attempt with a fresh retry counter. (Alternatively, replay by DLQ entry with POST /api/v1/webhooks/dlq/{dlq_id}/retry, which resets the counter and needs no body.)
Registering a Webhook
Owners and managers create and manage webhooks with a JWT bearer token; the webhook management endpoints don't accept an API key, even though Webhooks is one of the permission resources an API key can be scoped to. Creating a webhook is rate limited to 20 per hour.
POST /api/v1/webhooks/
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"name": "Fleet Entry Alerts",
"description": "Notifies dispatch when vehicles enter warehouse zones",
"url": "https://example.com/webhooks/geofence-events",
"events": ["enter", "exit"],
"auth_type": "bearer",
"auth_config": {"token": "your-bearer-token"},
"max_retries": 3,
"timeout_seconds": 30,
"rate_limit_per_minute": 60
}
events takes the bare event names (enter, exit), not the namespaced form the delivered payload uses (geofence.enter, geofence.exit). auth_type is none, bearer, basic, or api_key; SpatialFlow adds the corresponding header to every outbound delivery using auth_config.
The 201 response is the new webhook plus a secret field holding its signing secret. No other read returns the secret, so store it now. See Getting the signing secret.
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 (deduplicate on the signed
idin the body) - Verify signatures: Always verify the
X-SF-Signatureheader before processing
Best Practices
The snippets in this section and under Idempotency are fragments, with db, verify_signature, and process_event standing in for your own code. The receiver under Example Implementation runs as written.
1. Respond Immediately
Keep the handler short, but acknowledge only work that is committed. For slow work, write the delivery to an inbox table in a transaction and respond once it commits. A separate worker processes the rows:
from flask import Flask, request
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 # Flask answers 413 for larger bodies
@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
payload = request.get_json()
delivery_id = payload.get('id')
if not delivery_id:
return {"error": "Missing delivery id"}, 400
with db.transaction():
# webhook_inbox.id is the primary key; a duplicate delivery inserts nothing
db.execute(
"INSERT INTO webhook_inbox (id, body) VALUES (%s, %s) ON CONFLICT (id) DO NOTHING",
[delivery_id, request.data],
)
# Respond after the commit
return {"success": True}, 200
# A separate worker reads unprocessed rows from webhook_inbox and calls process_event
2. Implement Idempotency
Deduplicate on the signed id and acknowledge only work that is committed. Record the id in the same database transaction as the work, as in the receiver below. See Idempotency for the transaction example.
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. It needs only Flask and the standard library, and it uses SQLite (3.35 or later) so it runs as is. Swap in your own database and process_event.
import hashlib
import hmac
import json
import logging
import sqlite3
import threading
from flask import Flask, request
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 # Flask answers 413 for larger bodies
logger = logging.getLogger(__name__)
WEBHOOK_SECRET = "your-webhook-secret"
# One connection with explicit transactions, guarded by a lock
db = sqlite3.connect("webhooks.db", isolation_level=None, check_same_thread=False)
db_lock = threading.Lock()
db.executescript("""
CREATE TABLE IF NOT EXISTS processed_webhooks (id TEXT PRIMARY KEY);
CREATE TABLE IF NOT EXISTS events (delivery_id TEXT, event TEXT, data TEXT);
""")
def process_event(payload: dict) -> None:
"""Your business logic. Its writes join the caller's transaction."""
db.execute(
"INSERT INTO events (delivery_id, event, data) VALUES (?, ?, ?)",
(payload["id"], payload.get("event"), json.dumps(payload.get("data"))),
)
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()
# Compare bytes: str comparison raises TypeError on a non-ASCII header.
return hmac.compare_digest(computed.encode(), received.encode())
@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. Read the signed delivery id
payload = request.get_json(silent=True) or {}
delivery_id = payload.get('id')
if not delivery_id:
return {"error": "Missing delivery id"}, 400
with db_lock:
db.execute("BEGIN")
try:
# 3. Record the id and do the work in one transaction
row = db.execute(
"INSERT INTO processed_webhooks (id) VALUES (?) "
"ON CONFLICT (id) DO NOTHING RETURNING id",
(delivery_id,),
).fetchone()
if row is None:
db.execute("ROLLBACK")
logger.info(f"Duplicate webhook: {delivery_id}")
return {"status": "already_processed"}, 200
logger.info(f"Received webhook: {payload.get('event')}")
process_event(payload)
db.execute("COMMIT")
except Exception as e:
# Roll back the marker and the work, so SpatialFlow's retry processes the event
db.execute("ROLLBACK")
logger.error(f"Webhook error: {e}")
return {"error": "Internal server error"}, 500
# 4. Respond only after the commit
return {"success": True}, 200
if __name__ == '__main__':
app.run(port=3000)
Testing Webhooks
Using Test Events
Trigger a test webhook delivery through the 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 the webhook's delivery log through the API
- 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",
"next_retry_at": null
}
]
}
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Signature verification failed | Check you're using the webhook's current secret (rotating replaces it immediately) and the 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
Each webhook stores a configurable per-minute rate-limit value:
- Default: 60 requests per minute per webhook
- Configurable: Set a custom
rate_limit_per_minutewhen creating webhooks
This value is stored and surfaced on the webhook, but per-webhook delivery is not currently throttled against it at send time; deliveries are dispatched as events occur. Treat rate_limit_per_minute as configuration for a future enforcement pass, not a live guarantee. Design your receiver to absorb bursts.
Configure the value 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
- Errors: Retry strategies and error recovery