Webhooks Overview
Receive real-time Trade Intent status updates via webhooks
What Are Webhooks?
Webhooks are HTTP POST requests that SnipeRoute sends to your server whenever a Trade Intent's status changes. Instead of polling the API for updates, webhooks deliver events in real-time.
Webhooks are the recommended way to track Trade Intent status. They're more efficient than polling and provide instant updates.
How Webhooks Work
Configure Webhook URL
Set your webhook endpoint URL in the SnipeRoute dashboard (Settings → Webhooks).
Trade Intent Status Changes
When a Trade Intent's status changes (e.g., from pending to filled), SnipeRoute generates a webhook event.
SnipeRoute Sends HTTP POST
SnipeRoute sends an HTTP POST request to your webhook URL with the event data.
Your Server Processes Event
Your server receives the webhook, verifies the signature, and processes the event.
Respond with 200 OK
Your server responds with 200 OK to acknowledge receipt.
Webhook Events
| Event Type | Description |
|---|---|
intent.created | Trade Intent successfully created |
intent.pending | Order submitted to broker |
intent.filled | Order fully executed |
intent.partially_filled | Order partially executed |
intent.canceled | Order canceled |
intent.rejected | Broker rejected the order |
intent.failed | Routing or validation failed |
View complete event payload examples
Webhook Payload Structure
All webhook events have the same structure:
{
"type": "intent.filled",
"id": "evt_abc123",
"created_at": "2025-11-30T10:30:00Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"intent_id": "trade_001",
"symbol": "AAPL",
"side": "buy",
"quantity": 10,
"order_type": "market",
"status": "filled",
"broker_id": "alpaca_abc123",
"orders": [...]
}
}| Field | Type | Description |
|---|---|---|
type | string | Event type (e.g., "intent.filled") |
id | string | Unique event ID (for idempotency) |
created_at | string | Event timestamp (ISO 8601) |
data | object | Trade Intent object |
Setting Up Webhooks
1. Create Webhook Endpoint
Create an HTTPS endpoint on your server:
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
app = FastAPI()
# Hex-encoded secret from webhook creation
WEBHOOK_SECRET = "your_webhook_secret_from_dashboard"
@app.post("/webhooks/sniperoute")
async def handle_webhook(request: Request):
# Get headers
signature_header = request.headers.get("sr-signature")
timestamp = request.headers.get("sr-timestamp")
event_id = request.headers.get("sr-event-id")
body = await request.body()
# Verify signature (Stripe-style with timestamp)
if not signature_header or not signature_header.startswith("v1="):
raise HTTPException(status_code=401, detail="Invalid signature format")
signature = signature_header[3:] # Remove "v1=" prefix
message = f"{timestamp}.{body.decode()}"
expected_signature = hmac.new(
bytes.fromhex(WEBHOOK_SECRET), # Secret is hex-encoded
message.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected_signature):
raise HTTPException(status_code=401, detail="Invalid signature")
# Parse event
event = await request.json()
# Handle event
if event["type"] == "intent.filled":
print(f"Order filled: {event['data']['intent_id']}")
return {"status": "received"}2. Configure in Dashboard
Go to Settings → Webhooks
Add Webhook
Click Add Webhook and enter your HTTPS endpoint URL
Select Events
Choose which events to receive (or "All events")
Copy Secret
Save the webhook secret for signature verification
Test
Click Send Test Event to verify your endpoint works
Security
Signature Verification
Always verify webhook signatures to ensure requests come from SnipeRoute and haven't been tampered with.
SnipeRoute signs each webhook with HMAC-SHA256 using your webhook secret (Stripe-style):
Headers sent with each webhook:
sr-signature: v1=<hex-encoded-signature>
sr-timestamp: <unix-timestamp>
sr-event-id: <unique-event-id>Signed message format: {timestamp}.{payload}
Verification:
import hmac
import hashlib
def verify_signature(payload: bytes, timestamp: str, signature_header: str, secret: str) -> bool:
# Extract signature from header (format: "v1=...")
if not signature_header.startswith("v1="):
return False
signature = signature_header[3:]
# Reconstruct signed message
message = f"{timestamp}.{payload.decode()}"
# Secret is hex-encoded, decode it first
expected = hmac.new(
bytes.fromhex(secret),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Complete signature verification examples
Webhook Retries
If your endpoint doesn't respond with 2xx, SnipeRoute will retry:
| Attempt | Delay |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 30 seconds |
| 3rd retry | 2 minutes |
| 4th retry | 10 minutes |
| 5th retry | 30 minutes |
After 5 failed attempts, the webhook is moved to the dead letter queue and logged in your dashboard.
Best Practices
Respond Quickly
Return 200 OK within 5 seconds. Process events asynchronously if needed.
@app.post("/webhooks/sniperoute")
async def handle_webhook(request: Request):
# Verify signature
# Queue event for async processing
await event_queue.put(await request.json())
return {"status": "received"} # Respond immediatelyHandle Duplicates
Webhooks may be delivered multiple times. Use event.id for idempotency.
event = await request.json()
event_id = event["id"]
if await db.event_already_processed(event_id):
return {"status": "duplicate"}
await process_event(event)
await db.mark_event_processed(event_id)Use HTTPS
Webhook URLs must use HTTPS (not HTTP) for security.
Monitor Failures
Check the webhook logs in your dashboard to identify delivery issues.
Testing Webhooks Locally
Use ngrok to test webhooks on your local development machine:
# Install ngrok
brew install ngrok
# Start your local server
python app.py # Running on localhost:8000
# Expose local server
ngrok http 8000
# Use ngrok URL in webhook settings
# https://abc123.ngrok.io/webhooks/sniperouteWebhook Logs
View webhook delivery logs in your dashboard:
- Settings → Webhooks → Logs
Logs include:
- Event type
- Delivery status (success/failed)
- Response code
- Retry attempts
- Timestamp