Webhook Setup
Receive real-time Trade Intent status updates via webhooks
Overview
SnipeRoute can send webhooks to your server whenever a Trade Intent's status changes. This allows you to receive real-time updates without polling the API.
Webhooks are HTTP POST requests sent to your specified endpoint whenever an event occurs (e.g., order filled, rejected, etc.).
Webhook Events
SnipeRoute sends webhooks for the following events:
| Event | 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 |
Setting Up Webhooks
Create Webhook Endpoint
Create an HTTPS endpoint on your server to receive webhook events.
Add Webhook URL in Dashboard
Go to Settings → Webhooks and add your endpoint URL.
Verify Webhook Signature
Validate webhook signatures to ensure requests come from SnipeRoute.
Handle Events
Process webhook events in your application.
Creating a Webhook Endpoint
Python (FastAPI)
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import time
app = FastAPI()
# Hex-encoded secret from webhook creation
WEBHOOK_SECRET = "your_webhook_secret_from_dashboard"
TIMESTAMP_TOLERANCE = 300 # 5 minutes
@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")
if not signature_header or not timestamp:
raise HTTPException(status_code=401, detail="Missing required headers")
# Check timestamp (replay protection)
try:
ts = int(timestamp)
if abs(time.time() - ts) > TIMESTAMP_TOLERANCE:
raise HTTPException(status_code=401, detail="Timestamp too old")
except ValueError:
raise HTTPException(status_code=401, detail="Invalid timestamp")
# Parse signature (format: "v1=...")
if not signature_header.startswith("v1="):
raise HTTPException(status_code=401, detail="Invalid signature format")
signature = signature_header[3:]
# Get raw body
body = await request.body()
# Reconstruct signed message
message = f"{timestamp}.{body.decode()}"
# Verify signature (secret is hex-encoded)
expected_signature = hmac.new(
bytes.fromhex(WEBHOOK_SECRET),
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 different event types
if event["type"] == "intent.filled":
print(f"Order filled: {event['data']['intent_id']}")
# Update your database, notify user, etc.
elif event["type"] == "intent.rejected":
print(f"Order rejected: {event['data']['intent_id']}")
# Handle rejection (notify user, retry, etc.)
elif event["type"] == "intent.partially_filled":
print(f"⏳ Order partially filled: {event['data']['intent_id']}")
# Track partial fill
return {"status": "received"}Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret_from_dashboard';
const TIMESTAMP_TOLERANCE = 300; // 5 minutes
app.post('/webhooks/sniperoute', express.raw({ type: 'application/json' }), (req, res) => {
// Extract headers
const signatureHeader = req.headers['sr-signature'];
const timestamp = req.headers['sr-timestamp'];
const eventId = req.headers['sr-event-id'];
if (!signatureHeader || !timestamp) {
return res.status(401).send('Missing required headers');
}
// Check timestamp
const ts = parseInt(timestamp, 10);
if (isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > TIMESTAMP_TOLERANCE) {
return res.status(401).send('Invalid or expired timestamp');
}
// Parse signature header (format: "v1=...")
if (!signatureHeader.startsWith('v1=')) {
return res.status(401).send('Invalid signature format');
}
const signature = signatureHeader.slice(3);
const body = req.body;
// Reconstruct signed message
const message = `${timestamp}.${body.toString()}`;
// Verify signature (secret is hex-encoded)
const expectedSignature = crypto
.createHmac('sha256', Buffer.from(WEBHOOK_SECRET, 'hex'))
.update(message)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
return res.status(401).send('Invalid signature');
}
// Parse event
const event = JSON.parse(body);
// Handle event
switch (event.type) {
case 'intent.filled':
console.log(`Order filled: ${event.data.intent_id}`);
break;
case 'intent.rejected':
console.log(`Order rejected: ${event.data.intent_id}`);
break;
case 'intent.partially_filled':
console.log(`⏳ Order partially filled: ${event.data.intent_id}`);
break;
}
res.status(200).send({ status: 'received' });
});
app.listen(3000);Adding Webhook URL in Dashboard
Go to Settings → Webhooks
Click Add Webhook
Click Add Webhook button
Enter Endpoint URL
Enter your HTTPS endpoint URL (e.g., https://api.yourapp.com/webhooks/sniperoute)
Select Events
Choose which events to receive (or select "All events")
Copy Webhook Secret
Copy the webhook secret shown. You'll use this to verify signatures.
Test Webhook
Click Send Test Event to verify your endpoint is working
Webhook Payload Structure
intent.filled
{
"type": "intent.filled",
"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": [
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"broker_order_id": "order_123",
"status": "filled",
"fills": [
{
"quantity": 10,
"price": 175.50,
"filled_at": "2025-11-30T10:30:00Z"
}
]
}
],
"created_at": "2025-11-30T10:29:59Z",
"updated_at": "2025-11-30T10:30:00Z"
}
}intent.partially_filled
{
"type": "intent.partially_filled",
"created_at": "2025-11-30T10:30:15Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"intent_id": "trade_002",
"symbol": "TSLA",
"side": "buy",
"quantity": 100,
"order_type": "limit",
"limit_price": 250.00,
"status": "partially_filled",
"broker_id": "alpaca_abc123",
"orders": [
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"broker_order_id": "order_456",
"status": "partially_filled",
"fills": [
{
"quantity": 50,
"price": 250.00,
"filled_at": "2025-11-30T10:30:10Z"
}
]
}
]
}
}intent.rejected
{
"type": "intent.rejected",
"created_at": "2025-11-30T10:31:00Z",
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"intent_id": "trade_003",
"symbol": "NVDA",
"side": "buy",
"quantity": 1000,
"order_type": "market",
"status": "rejected",
"broker_id": "alpaca_abc123",
"error": {
"code": "insufficient_funds",
"message": "Insufficient buying power"
}
}
}Verifying Webhook Signatures
Always verify webhook signatures to prevent spoofed requests. Never skip signature verification in production.
How Signatures Work
- SnipeRoute combines timestamp and payload:
{timestamp}.{payload} - Signs with your hex-encoded webhook secret using HMAC-SHA256
- Sends signature in
sr-signatureheader (format:v1={signature}) - Also sends
sr-timestampandsr-event-idheaders - You recompute the signature using the same method and compare
Python Verification
import hmac
import hashlib
import time
def verify_webhook_signature(
payload: bytes,
timestamp: str,
signature_header: str,
secret: str,
tolerance: int = 300
) -> bool:
"""Verify webhook signature with replay protection."""
# Check timestamp
try:
ts = int(timestamp)
if abs(time.time() - ts) > tolerance:
return False
except ValueError:
return False
# Parse signature header (format: "v1=...")
if not signature_header.startswith("v1="):
return False
signature = signature_header[3:]
# Reconstruct signed message
message = f"{timestamp}.{payload.decode()}"
# Compute expected signature (secret is hex-encoded)
expected_signature = hmac.new(
bytes.fromhex(secret),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Usage
signature_header = request.headers.get("sr-signature")
timestamp = request.headers.get("sr-timestamp")
body = await request.body()
if not verify_webhook_signature(body, timestamp, signature_header, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")Node.js Verification
const crypto = require('crypto');
function verifyWebhookSignature(payload, timestamp, signatureHeader, secret, tolerance = 300) {
// Check timestamp
const ts = parseInt(timestamp, 10);
if (isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > tolerance) {
return false;
}
// Parse signature header (format: "v1=...")
if (!signatureHeader.startsWith('v1=')) {
return false;
}
const signature = signatureHeader.slice(3);
// Reconstruct signed message
const message = `${timestamp}.${payload.toString()}`;
// Compute expected signature (secret is hex-encoded)
const expectedSignature = crypto
.createHmac('sha256', Buffer.from(secret, 'hex'))
.update(message)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage
const signatureHeader = req.headers['sr-signature'];
const timestamp = req.headers['sr-timestamp'];
const body = req.body;
if (!verifyWebhookSignature(body, timestamp, signatureHeader, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}Handling Webhook Events
Update Database
@app.post("/webhooks/sniperoute")
async def handle_webhook(request: Request):
# ... verify signature ...
event = await request.json()
if event["type"] == "intent.filled":
# Update database with fill details
await db.execute(
"""
UPDATE trades
SET status = 'filled',
filled_quantity = :quantity,
avg_fill_price = :price,
updated_at = NOW()
WHERE intent_id = :intent_id
""",
{
"intent_id": event["data"]["intent_id"],
"quantity": event["data"]["quantity"],
"price": event["data"]["orders"][0]["fills"][0]["price"]
}
)
return {"status": "received"}Send Notifications
if event["type"] == "intent.filled":
# Send email or push notification
await send_notification(
user_id=event["data"]["user_id"],
message=f"Your order for {event['data']['symbol']} was filled!"
)Retry Failed Orders
if event["type"] == "intent.rejected":
error_code = event["data"]["error"]["code"]
if error_code == "insufficient_funds":
# Don't retry - user needs to add funds
await notify_user("Insufficient funds for order")
elif error_code == "timeout":
# Retry with same parameters
await retry_intent(event["data"]["intent_id"])Testing Webhooks
Send Test Event from Dashboard
- Go to Settings → Webhooks
- Click Send Test Event next to your webhook
- Check your server logs to verify receipt
Use ngrok for Local Testing
# Install ngrok
brew install ngrok
# Start your local server
python app.py
# Expose local server
ngrok http 8000
# Use ngrok URL in webhook settings
# https://abc123.ngrok.io/webhooks/sniperouteWebhook Retries
SnipeRoute will retry failed webhook deliveries:
| Attempt | Delay |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 30 seconds |
| 3rd retry | 2 minutes |
| 4th retry | 10 minutes |
| 5th retry | 30 minutes |
If your endpoint returns a 2xx status code, the webhook is considered delivered and will not be retried.
Best Practices
Return 200 Quickly
Respond with 200 OK as quickly as possible (within 5 seconds). Process the event 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 immediately
return {"status": "received"}Idempotent Event Handling
Webhooks may be delivered multiple times. Use the event ID to ensure idempotent processing.
event = await request.json()
event_id = event["id"]
# Check if already processed
if await db.event_already_processed(event_id):
return {"status": "duplicate"}
# Process event
await process_event(event)
# Mark as processed
await db.mark_event_processed(event_id)Monitor Webhook Failures
Check the webhook logs in your dashboard regularly to identify delivery failures.