Guides
Webhook Setup

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:

EventDescription
intent.createdTrade Intent successfully created
intent.pendingOrder submitted to broker
intent.filledOrder fully executed
intent.partially_filledOrder partially executed
intent.canceledOrder canceled
intent.rejectedBroker rejected the order
intent.failedRouting or validation failed

Setting Up Webhooks

1

Create Webhook Endpoint

Create an HTTPS endpoint on your server to receive webhook events.

2

Add Webhook URL in Dashboard

Go to Settings → Webhooks and add your endpoint URL.

3

Verify Webhook Signature

Validate webhook signatures to ensure requests come from SnipeRoute.

4

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

1

Go to Settings → Webhooks

2

Click Add Webhook

Click Add Webhook button

3

Enter Endpoint URL

Enter your HTTPS endpoint URL (e.g., https://api.yourapp.com/webhooks/sniperoute)

4

Select Events

Choose which events to receive (or select "All events")

5

Copy Webhook Secret

Copy the webhook secret shown. You'll use this to verify signatures.

6

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

  1. SnipeRoute combines timestamp and payload: {timestamp}.{payload}
  2. Signs with your hex-encoded webhook secret using HMAC-SHA256
  3. Sends signature in sr-signature header (format: v1={signature})
  4. Also sends sr-timestamp and sr-event-id headers
  5. 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

  1. Go to Settings → Webhooks
  2. Click Send Test Event next to your webhook
  3. 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/sniperoute

Webhook Retries

SnipeRoute will retry failed webhook deliveries:

AttemptDelay
1st retry10 seconds
2nd retry30 seconds
3rd retry2 minutes
4th retry10 minutes
5th retry30 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.

Next Steps