Skip to content

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

  1. Step 1In Settings, add an endpoint: an HTTPS address on your side, and the events you want.
  2. Step 2Copy the endpoint's signing secret. It's shown once.
  3. Step 3When an event happens, Shipment Insight sends a POST with a JSON body to your endpoint.
  4. Step 4Your endpoint checks the signature, stores the event and answers 2xx within 10 seconds.

Event types

EventSent when
shipment.lifecycle.changedA shipment changes status, for example from In transit to At delivery.
tracking.milestone.recordedA milestone is recorded on the timeline, such as arrival at pickup.
intelligence.eta.updatedThe predicted arrival for a stop changes.
intelligence.risk.updatedThe risk of missing the appointment changes, with the new reasons.
tracking.exception.raisedA 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"
  }
}
Example body. Every event has an id, a type, a creation time and data.

Signatures

Each request carries a SI-Signature header with a timestamp and a signature:

SI-Signature: t=1762530130,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 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.

  1. Step 1Read t and v1 from the header.
  2. Step 2Reject the request if t is more than 5 minutes from your clock. This blocks replayed requests.
  3. Step 3Compute the HMAC of t + . + raw body with your secret.
  4. Step 4Compare it with v1 using a constant-time comparison. If they differ, answer 400 and 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);
});
Node.js (Express). Uses the raw body, not the parsed JSON.
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)
}
Go (net/http).

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 id to 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.