Webhook Events
Complete reference for all webhook event types and payloads
Event Types
| Event | Trigger |
|---|---|
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 |
Event Structure
All webhook events follow this structure:
{
"type": "event_type",
"id": "evt_unique_id",
"created_at": "2025-11-30T10:30:00Z",
"data": {
// Trade Intent object
}
}intent.created
Sent when a Trade Intent is successfully created.
{
"type": "intent.created",
"id": "evt_abc123",
"created_at": "2025-11-30T10:29:59Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"intent_id": "trade_001",
"symbol": "AAPL",
"side": "buy",
"quantity": 10,
"order_type": "market",
"status": "pending",
"broker_id": "alpaca_abc123",
"orders": [],
"created_at": "2025-11-30T10:29:59Z",
"updated_at": "2025-11-30T10:29:59Z"
}
}Use case: Log intent creation, update UI
intent.pending
Sent when the order is submitted to the broker and awaiting execution.
{
"type": "intent.pending",
"id": "evt_def456",
"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": "pending",
"broker_id": "alpaca_abc123",
"orders": [
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"broker_order_id": "alpaca_order_123",
"status": "pending",
"fills": [],
"created_at": "2025-11-30T10:30:00Z",
"updated_at": "2025-11-30T10:30:00Z"
}
],
"created_at": "2025-11-30T10:29:59Z",
"updated_at": "2025-11-30T10:30:00Z"
}
}Use case: Show "Order submitted" status to user
intent.filled
Sent when the order is fully executed.
{
"type": "intent.filled",
"id": "evt_ghi789",
"created_at": "2025-11-30T10:30:05Z",
"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": "alpaca_order_123",
"status": "filled",
"fills": [
{
"quantity": 10,
"price": 175.50,
"filled_at": "2025-11-30T10:30:05Z"
}
],
"created_at": "2025-11-30T10:30:00Z",
"updated_at": "2025-11-30T10:30:05Z"
}
],
"created_at": "2025-11-30T10:29:59Z",
"updated_at": "2025-11-30T10:30:05Z"
}
}Use case: Update portfolio, send notification, log fill price
intent.partially_filled
Sent when the order is partially executed.
{
"type": "intent.partially_filled",
"id": "evt_jkl012",
"created_at": "2025-11-30T10:30:10Z",
"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": "alpaca_order_456",
"status": "partially_filled",
"fills": [
{
"quantity": 50,
"price": 250.00,
"filled_at": "2025-11-30T10:30:10Z"
}
],
"created_at": "2025-11-30T10:30:00Z",
"updated_at": "2025-11-30T10:30:10Z"
}
],
"created_at": "2025-11-30T10:29:59Z",
"updated_at": "2025-11-30T10:30:10Z"
}
}Use case: Track partial fills, update UI with progress
intent.canceled
Sent when the order is canceled before full execution.
{
"type": "intent.canceled",
"id": "evt_mno345",
"created_at": "2025-11-30T10:35:00Z",
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"intent_id": "trade_003",
"symbol": "NVDA",
"side": "buy",
"quantity": 20,
"order_type": "limit",
"limit_price": 500.00,
"status": "canceled",
"broker_id": "alpaca_abc123",
"orders": [
{
"id": "990e8400-e29b-41d4-a716-446655440000",
"broker_order_id": "alpaca_order_789",
"status": "canceled",
"fills": [],
"created_at": "2025-11-30T10:30:00Z",
"updated_at": "2025-11-30T10:35:00Z"
}
],
"created_at": "2025-11-30T10:29:59Z",
"updated_at": "2025-11-30T10:35:00Z"
}
}Use case: Notify user, log cancellation
intent.rejected
Sent when the broker rejects the order.
{
"type": "intent.rejected",
"id": "evt_pqr678",
"created_at": "2025-11-30T10:30:02Z",
"data": {
"id": "aa0e8400-e29b-41d4-a716-446655440000",
"intent_id": "trade_004",
"symbol": "AAPL",
"side": "buy",
"quantity": 1000,
"order_type": "market",
"status": "rejected",
"broker_id": "alpaca_abc123",
"orders": [
{
"id": "bb0e8400-e29b-41d4-a716-446655440000",
"broker_order_id": "alpaca_order_000",
"status": "rejected",
"fills": [],
"created_at": "2025-11-30T10:30:00Z",
"updated_at": "2025-11-30T10:30:02Z"
}
],
"error": {
"code": "insufficient_funds",
"message": "Insufficient buying power"
},
"created_at": "2025-11-30T10:29:59Z",
"updated_at": "2025-11-30T10:30:02Z"
}
}Use case: Alert user, log error, potentially retry
Common rejection codes:
insufficient_funds- Not enough buying powerinvalid_symbol- Symbol not foundmarket_closed- Market is closedhalted- Trading halted for symbol
intent.failed
Sent when routing or validation fails (before reaching broker).
{
"type": "intent.failed",
"id": "evt_stu901",
"created_at": "2025-11-30T10:30:01Z",
"data": {
"id": "cc0e8400-e29b-41d4-a716-446655440000",
"intent_id": "trade_005",
"symbol": "AAPL",
"side": "buy",
"quantity": 10,
"order_type": "market",
"status": "failed",
"broker_id": "invalid_broker",
"orders": [],
"error": {
"code": "broker_not_found",
"message": "Broker connection not found"
},
"created_at": "2025-11-30T10:29:59Z",
"updated_at": "2025-11-30T10:30:01Z"
}
}Use case: Alert user, log error, check configuration
Common failure codes:
broker_not_found- Invalid broker_idbroker_unauthorized- OAuth token expiredvalidation_error- Invalid parameters
Handling Events
Python Example
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import time
app = FastAPI()
WEBHOOK_SECRET = "your_hex_encoded_secret" # 64 character hex string
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
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:]
body = await request.body()
message = f"{timestamp}.{body.decode()}"
expected_sig = hmac.new(
bytes.fromhex(WEBHOOK_SECRET),
message.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected_sig):
raise HTTPException(status_code=401, detail="Invalid signature")
# Parse event
event = await request.json()
event_type = event["type"]
data = event["data"]
# Handle events
if event_type == "intent.filled":
# Update database
await db.execute(
"UPDATE trades SET status = 'filled', avg_price = :price WHERE intent_id = :id",
{"price": data["orders"][0]["fills"][0]["price"], "id": data["intent_id"]}
)
# Send notification
await notify_user(f"Order filled: {data['symbol']}")
elif event_type == "intent.rejected":
# Log error
error_msg = data.get("error", {}).get("message", "Unknown error")
await log_error(f"Order rejected: {error_msg}")
# Notify user
await notify_user(f"Order rejected: {error_msg}")
elif event_type == "intent.partially_filled":
# Track partial fill
fills = data["orders"][0]["fills"]
filled_qty = sum(f["quantity"] for f in fills)
await db.execute(
"UPDATE trades SET filled_quantity = :qty WHERE intent_id = :id",
{"qty": filled_qty, "id": data["intent_id"]}
)
return {"status": "received"}Node.js Example
app.post('/webhooks/sniperoute', async (req, res) => {
const event = req.body;
switch (event.type) {
case 'intent.filled':
console.log(`Order filled: ${event.data.intent_id}`);
// Update database, send notification
break;
case 'intent.rejected':
console.log(`Order rejected: ${event.data.error.message}`);
// Log error, notify user
break;
case 'intent.partially_filled':
const fills = event.data.orders[0].fills;
const filledQty = fills.reduce((sum, f) => sum + f.quantity, 0);
console.log(`Partially filled: ${filledQty}/${event.data.quantity}`);
break;
}
res.status(200).send({ status: 'received' });
});