Documentation/Integrations and API·4 min read·Updated Nov 7, 2026
Webhooks
Webhooks will send events to your own systems as they happen: status changes, milestones, new ETAs, risk changes and exceptions. Every delivery is signed, so you can check it came from Shipment Insight. Planned.
Preview
Webhooks are not available yet. This page describes the planned design. Event names and fields may still change before launch.
How it works
- Step 1In Settings, add an endpoint: an HTTPS address on your side, and the events you want.
- Step 2Copy the endpoint's signing secret. It's shown once.
- Step 3When an event happens, Shipment Insight sends a
POSTwith a JSON body to your endpoint. - Step 4Your endpoint checks the signature, stores the event and answers
2xxwithin 10 seconds.
Event types
| Event | Sent when |
|---|---|
shipment.lifecycle.changed | A shipment changes status, for example from In transit to At delivery. |
tracking.milestone.recorded | A milestone is recorded on the timeline, such as arrival at pickup. |
intelligence.eta.updated | The predicted arrival for a stop changes. |
intelligence.risk.updated | The risk of missing the appointment changes, with the new reasons. |
tracking.exception.raised | A shipment needs attention and appears in Exceptions. |
{
"id": "evt_01HZX5B7K3",
"type": "intelligence.eta.updated",
"created_at": "2026-11-07T15:42:10Z",
"data": {
"shipment_id": "shp_0123456789",
"stop_id": "stp_02",
"eta": "2026-11-07T18:40:00Z",
"previous_eta": "2026-11-07T18:05:00Z"
}
}Signatures
Each request carries a SI-Signature header with a timestamp and a signature:
SI-Signature: t=1762530130,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdv1 is an HMAC-SHA256, in hex, of the string <timestamp>.<body>, computed with your endpoint's signing secret. <body> is the raw request body, exactly as received, before any JSON parsing.
- Step 1Read
tandv1from the header. - Step 2Reject the request if
tis more than 5 minutes from your clock. This blocks replayed requests. - Step 3Compute the HMAC of
t+.+ raw body with your secret. - Step 4Compare it with
v1using a constant-time comparison. If they differ, answer400and ignore the event.
import crypto from "node:crypto";
import express from "express";
const secret = process.env.SI_WEBHOOK_SECRET;
const app = express();
app.post("/webhooks/shipmentinsight", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("SI-Signature") ?? "";
const pairs = header.split(",").map((p) => p.split("=", 2));
const ts = pairs.find(([k]) => k === "t")?.[1] ?? "";
const sigs = pairs.filter(([k]) => k === "v1").map(([, v]) => v); // two during secret rotation
// Reject old or future timestamps (replay protection).
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
const expected = crypto.createHmac("sha256", secret).update(`${ts}.`).update(req.body).digest("hex");
const a = Buffer.from(expected);
const valid = sigs.some((s) => {
const b = Buffer.from(s);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
if (!valid) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
// Store the event, then process it in the background.
res.sendStatus(200);
});package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"math"
"net/http"
"os"
"strconv"
"strings"
"time"
)
var secret = []byte(os.Getenv("SI_WEBHOOK_SECRET"))
func Handle(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
var ts string
var sigs []string // two during secret rotation
for _, part := range strings.Split(r.Header.Get("SI-Signature"), ",") {
if k, v, ok := strings.Cut(part, "="); ok {
switch k {
case "t":
ts = v
case "v1":
sigs = append(sigs, v)
}
}
}
// Reject old or future timestamps (replay protection).
t, err := strconv.ParseInt(ts, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-t)) > 300 {
http.Error(w, "bad timestamp", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(ts + "."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
valid := false
for _, s := range sigs {
if hmac.Equal([]byte(expected), []byte(s)) {
valid = true
}
}
if !valid {
http.Error(w, "bad signature", http.StatusBadRequest)
return
}
// Store the event, then process it in the background.
w.WriteHeader(http.StatusOK)
}Retries
A delivery succeeds when your endpoint answers with a 2xx status within 10 seconds. Anything else (an error, a timeout, a redirect) is retried with growing delays, from seconds to hours, for up to 24 hours. After that the event is marked as failed, and you can see it and send it again from Settings.
Tip
Answer quickly. Store the event and return 200 first, then do the slow work in the background. A slow endpoint causes timeouts, and timeouts cause retries.
Handle each event once
- The same event can arrive more than once, for example after a timeout. Use the event
idto skip events you've already processed. - Events can arrive out of order. Compare
created_at, or fetch the latest state from the API, before overwriting newer data with older data. - Ignore event types and fields you don't know. New ones are added over time.
Security checklist
- Use HTTPS. Endpoints without TLS are not accepted.
- Always verify the signature. Don't rely on IP addresses alone.
- Keep the signing secret in a secrets manager. Rotate it in Settings if it leaks.
- Treat text fields in events (notes, references) as untrusted input.