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

  1. You register a webhook URL with SALLY
  2. When an event occurs (new alert, route status change, load update), SALLY sends an HTTP POST to your URL
  3. Your server processes the event and returns a 200 response
  4. If delivery fails, SALLY retries with exponential backoff

Supported Events

EventDescriptionPayload Contains
alert.createdA new alert has been generatedFull alert object
alert.updatedAn alert status changed (acknowledged, snoozed)Alert ID, new status, actor
alert.resolvedAn alert has been resolvedAlert ID, resolution details
route.plannedA new route plan was createdPlan ID, summary
route.activatedA route was activatedPlan ID, driver, vehicle
route.completedA route was completedPlan ID, completion summary
route.cancelledA route was cancelledPlan ID, cancellation reason
driver.status_changedDriver status changedDriver ID, old/new status
load.status_changedLoad status changedLoad ID, old/new status
integration.sync_completedAn integration sync finishedIntegration ID, sync results
integration.sync_failedAn integration sync failedIntegration 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:

HeaderDescription
Content-Typeapplication/json
X-Sally-EventEvent type (e.g., alert.created)
X-Sally-DeliveryUnique delivery ID for idempotency
X-Sally-SignatureHMAC-SHA256 signature for verification
X-Sally-TimestampUnix 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:

AttemptDelayTotal Wait
1st retry30 seconds30s
2nd retry2 minutes2m 30s
3rd retry10 minutes12m 30s
4th retry30 minutes42m 30s
5th retry1 hour1h 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

  1. Always verify signatures — Never trust unverified webhook payloads
  2. Return 200 quickly — Process events asynchronously to avoid timeouts
  3. Implement idempotency — Handle duplicate deliveries gracefully
  4. Use HTTPS — SALLY only delivers webhooks to HTTPS endpoints
  5. Monitor delivery failures — Set up alerts if deliveries fail repeatedly
  6. Handle events you do not recognize — New event types may be added; ignore unknown events gracefully

Next Steps