Webhooks

Shipment Tracking Webhooks

Have shipment changes pushed to your ERP, TMS or internal tooling the moment they happen — across ocean, air and 1,000+ couriers — instead of polling an API and hoping something moved.

How it works

Webhooks are available on the Pro and Business plans — see pricing. Add your endpoint URL and an optional signing secret in your Shyppy account. From then on, whenever a carrier reports a change on any of your shipments, Shyppy sends a single JSON POST to that URL. One webhook covers every shipment on the account — there is nothing to subscribe to per shipment.

Why push rather than poll. Carrier data updates unpredictably: a container can sit unchanged for six days and then log three events in an hour. Polling on a fixed schedule either wastes calls on nothing or finds out late. A webhook fires on the change itself, which is what your planning actually depends on.

Request format

PropertyValue
MethodPOST
Content typeapplication/json
User agentShyppy-Webhook/1.0
Signature headerX-Shyppy-Signature: sha256=<hmac> — sent when a secret is configured
Timeout5 seconds
RedirectsNot followed, by design

Ocean and air payload

Status change on a container shipment

{
  "type": "ocean",
  "identifier": "MAEU3423831",
  "oldStatus": "SAILING",
  "newStatus": "DISCHARGED",
  "latestEvent": {
    "location": "LIMASSOL",
    "move": "Discharged",
    "date": "2026-07-28T12:00:00.000Z",
    "vessel": "CMA CGM TAGE",
    "isActual": true
  },
  "etaInfo": {
    "oldEta": "2026-07-26",
    "newEta": "2026-07-28",
    "initialEta": "2026-07-21",
    "delayDays": 2,
    "transitTime": 24,
    "transitPct": 100
  },
  "source": "shyppy",
  "timestamp": "2026-07-28T12:04:11.902Z"
}

type is ocean or air. For air shipments latestEvent.vessel carries the flight number instead of a vessel name. etaInfo is null when the arrival estimate hasn't moved, and delayDays is negative when a shipment is running early.

Courier payload

Milestone change on a parcel

{
  "type": "courier",
  "identifier": "KARM DESIGN TO EAC",
  "trackingNumber": "7105413615",
  "oldMilestone": "info_received",
  "newMilestone": "in_transit",
  "latestEvent": {
    "status": "Shipment picked up",
    "description": "Shipment picked up",
    "location": "NEWCASTLE - UK",
    "date": "2026-08-06T16:43:00+01:00",
    "courier": "dhl"
  },
  "source": "shyppy",
  "timestamp": "2026-08-06T16:47:03.117Z"
}

identifier is the shipment's title where you've set one, falling back to the tracking number. Milestones follow the standard set: pending, info_received, in_transit, out_for_delivery, available_for_pickup, delivered, failed_attempt, exception, expired.

Verifying the signature

When you configure a secret, every delivery is signed. Compute an HMAC-SHA256 over the raw request body — before any JSON parsing, since re-serialising changes the bytes — and compare it to the header using a constant-time comparison.

Node.js / Express

const crypto = require("crypto");

// Capture the raw body: express.json() alone will not give you the bytes
app.post("/webhooks/shyppy",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("X-Shyppy-Signature") || "";
    const expected = "sha256=" + crypto
      .createHmac("sha256", process.env.SHYPPY_WEBHOOK_SECRET)
      .update(req.body)                 // raw Buffer, not a parsed object
      .digest("hex");

    const a = Buffer.from(signature);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).end();
    }

    const event = JSON.parse(req.body.toString("utf8"));
    // ... hand off to your queue, then respond promptly
    res.status(200).end();
  });

Delivery behaviour worth knowing

Two things to design around, stated plainly rather than discovered in production:

There is no retry queue. If your endpoint is down or takes longer than five seconds, that delivery is dropped. Treat webhooks as a fast notification, not a guaranteed ledger — nothing is lost from the shipment record itself, which remains visible in the app and its event history. Respond 200 quickly and do your real work on a queue.

Private addresses are refused. URLs that resolve to private, loopback or link-local ranges are rejected when saved and re-checked at delivery time, and redirects are never followed. This closes off using a webhook URL to probe internal infrastructure — worth knowing if you're testing against a local tunnel.

Common questions

Which events fire a webhook?

A change in the carrier's reported status or milestone, and for ocean and air a revised ETA. Repeat reports of an unchanged status don't fire, so you get real changes rather than heartbeat noise.

Can I have different URLs per shipment?

Not today — one endpoint serves the whole account, and the payload's type and identifier tell you which shipment moved. Route internally from there.

What should my endpoint return?

Any 2xx, as fast as possible. Don't do slow work inline: you have five seconds before the delivery is abandoned.

Which plans include webhooks?

Pro and Business. Accounts that configured one earlier keep it — introducing the requirement doesn't break a running integration.

Do webhooks replace the email notifications?

No, they run alongside them. Email is for people; webhooks are for systems. Both are driven by the same status change.

Related

Wire your systems to your shipments

Add an endpoint in your account settings and updates start arriving.

Start Tracking Free