Webhooks
Webhooks let SALLY push event notifications to your systems as they happen. Instead of polling the API for changes, you register a URL and SALLY sends HTTP POST requests to it whenever relevant events occur.
How Webhooks Work
- You register a webhook URL with SALLY
- When an event occurs (new alert, route status change, load update), SALLY sends an HTTP POST to your URL
- Your server processes the event and returns a
200response - If delivery fails, SALLY retries with exponential backoff
Supported Events
| Event | Description | Payload Contains |
|---|---|---|
alert.created | A new alert has been generated | Full alert object |
alert.updated | An alert status changed (acknowledged, snoozed) | Alert ID, new status, actor |
alert.resolved | An alert has been resolved | Alert ID, resolution details |
route.planned | A new route plan was created | Plan ID, summary |
route.activated | A route was activated | Plan ID, driver, vehicle |
route.completed | A route was completed | Plan ID, completion summary |
route.cancelled | A route was cancelled | Plan ID, cancellation reason |
driver.status_changed | Driver status changed | Driver ID, old/new status |
load.status_changed | Load status changed | Load ID, old/new status |
integration.sync_completed | An integration sync finished | Integration ID, sync results |
integration.sync_failed | An integration sync failed | Integration ID, error details |
Registering a Webhook
curl -X POST https://sally-api.apps.appshore.in/api/v1/webhooks \
-H "X-API-Key: $SALLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/sally",
"events": ["alert.created", "alert.resolved", "route.activated"],
"secret": "whsec_your_signing_secret_here",
"description": "Production alert handler"
}'JavaScript (fetch):
const response = await fetch(
"https://sally-api.apps.appshore.in/api/v1/webhooks",
{
method: "POST",
headers: {
"X-API-Key": process.env.SALLY_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://your-server.com/webhooks/sally",
events: ["alert.created", "alert.resolved", "route.activated"],
secret: process.env.WEBHOOK_SIGNING_SECRET,
description: "Production alert handler",
}),
}
);
const webhook = await response.json();Response:
{
"id": "wh_a1b2c3d4",
"url": "https://your-server.com/webhooks/sally",
"events": ["alert.created", "alert.resolved", "route.activated"],
"status": "ACTIVE",
"description": "Production alert handler",
"createdAt": "2026-02-10T10:00:00Z"
}Use "events": ["*"] to subscribe to all event types.
Webhook Payload Format
Every webhook delivery includes a standard envelope:
{
"id": "evt_m5n6o7p8",
"event": "alert.created",
"timestamp": "2026-02-10T16:15:00Z",
"webhookId": "wh_a1b2c3d4",
"data": {
"id": "alt_x1y2z3w4",
"type": "HOS_APPROACHING_DRIVE_LIMIT",
"category": "HOS_VIOLATION",
"priority": "HIGH",
"status": "ACTIVE",
"title": "Driver approaching 11-hour drive limit",
"message": "Mike Johnson (TRK-4821) has 45 minutes of drive time remaining.",
"driverId": "drv_a1b2c3d4",
"routePlanId": "rte_f8e7d6c5",
"createdAt": "2026-02-10T16:15:00Z"
}
}Headers
Every webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Sally-Event | Event type (e.g., alert.created) |
X-Sally-Delivery | Unique delivery ID for idempotency |
X-Sally-Signature | HMAC-SHA256 signature for verification |
X-Sally-Timestamp | Unix timestamp of when the event was sent |
Verifying Webhook Signatures
Always verify webhook signatures to ensure the request came from SALLY and was not tampered with. SALLY signs the payload using the secret you provided during registration.
Node.js Verification
import crypto from "crypto";
function verifyWebhookSignature(payload, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js example
app.post("/webhooks/sally", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-sally-signature"];
const timestamp = req.headers["x-sally-timestamp"];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Verify timestamp is recent (prevent replay attacks)
const eventTime = parseInt(timestamp);
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - eventTime) > 300) {
return res.status(401).send("Timestamp too old");
}
const event = JSON.parse(payload);
console.log("Received event:", event.event);
// Process the event
handleEvent(event);
res.status(200).send("OK");
});Python Verification
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Handling Webhook Events
Here is a complete Express.js handler that processes different event types:
function handleEvent(event) {
switch (event.event) {
case "alert.created":
handleNewAlert(event.data);
break;
case "alert.resolved":
handleAlertResolved(event.data);
break;
case "route.activated":
handleRouteActivated(event.data);
break;
case "route.completed":
handleRouteCompleted(event.data);
break;
case "load.status_changed":
handleLoadStatusChange(event.data);
break;
default:
console.log("Unhandled event type:", event.event);
}
}
function handleNewAlert(alert) {
if (alert.priority === "CRITICAL") {
// Send SMS to on-call dispatcher
sendSMS(onCallDispatcher.phone, `CRITICAL ALERT: ${alert.title}`);
// Post to Slack
postToSlack("#dispatch-alerts", alert);
}
}
function handleRouteActivated(route) {
// Notify the customer that the shipment is on its way
notifyCustomer(route.loadIds, "Your shipment is now in transit.");
}Retry Policy
If your server does not return a 2xx response, SALLY retries the delivery:
| Attempt | Delay | Total Wait |
|---|---|---|
| 1st retry | 30 seconds | 30s |
| 2nd retry | 2 minutes | 2m 30s |
| 3rd retry | 10 minutes | 12m 30s |
| 4th retry | 30 minutes | 42m 30s |
| 5th retry | 1 hour | 1h 42m 30s |
After 5 failed retries, the delivery is marked as failed. You can view failed deliveries in the webhook dashboard and retry them manually.
SALLY considers any 2xx status code as successful delivery. Return 200 OK as quickly as possible — process the event asynchronously if it requires significant work.
Idempotency
Webhook deliveries may be sent more than once in rare cases (network issues, retries). Use the X-Sally-Delivery header as an idempotency key:
const processedDeliveries = new Set();
app.post("/webhooks/sally", (req, res) => {
const deliveryId = req.headers["x-sally-delivery"];
if (processedDeliveries.has(deliveryId)) {
return res.status(200).send("Already processed");
}
processedDeliveries.add(deliveryId);
handleEvent(req.body);
res.status(200).send("OK");
});For production use, store processed delivery IDs in a database or Redis rather than an in-memory set.
Managing Webhooks
List Webhooks
curl https://sally-api.apps.appshore.in/api/v1/webhooks \
-H "X-API-Key: $SALLY_API_KEY"Update a Webhook
curl -X PUT https://sally-api.apps.appshore.in/api/v1/webhooks/wh_a1b2c3d4 \
-H "X-API-Key: $SALLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": ["alert.created", "alert.resolved", "route.activated", "route.completed"],
"url": "https://your-server.com/webhooks/sally-v2"
}'Disable a Webhook
curl -X DELETE https://sally-api.apps.appshore.in/api/v1/webhooks/wh_a1b2c3d4 \
-H "X-API-Key: $SALLY_API_KEY"Best Practices
- Always verify signatures — Never trust unverified webhook payloads
- Return 200 quickly — Process events asynchronously to avoid timeouts
- Implement idempotency — Handle duplicate deliveries gracefully
- Use HTTPS — SALLY only delivers webhooks to HTTPS endpoints
- Monitor delivery failures — Set up alerts if deliveries fail repeatedly
- Handle events you do not recognize — New event types may be added; ignore unknown events gracefully
Next Steps
- Error Handling — Handle errors and implement retry logic
- Real-time Events — Alternative: SSE and WebSocket for browser clients
- Integrations Overview — All supported integrations