Skip to Content
API ReferenceWebhook Notifications

Webhook Notifications

When a shipment you are subscribed to changes status, the platform sends an HTTP POST to your registered webhook. Each request is signed with HMAC-SHA256 so you can verify it genuinely came from Spedisci.online.

When a Notification Fires

A notification is sent when all of the following are true:

  1. A new tracking event is recorded for the shipment.
  2. At least one active webhook has a subscription for that ldv.
  3. The resulting status code is different from the last one you were notified about.

Notifications are status-change driven, not event driven. Several tracking events can map to the same status — you receive one notification for the first of them, not one per event. The full event list is always embedded in the payload, so nothing is lost.

Events

EventDescription
tracking.updatedThe delivery status of a subscribed shipment changed

tracking.updated is currently the only event type. Design your handler with a switch on event so new types can be added without breaking you.

Payload

{ "event": "tracking.updated", "ldv": "ABC123456789", "status": 5, "status_description": "Consegnata", "occurred_at": "2026-04-18T14:32:05+00:00", "tracking": { "tracking_number": "ABC123456789", "events": [ { "code": 1, "date": "2026-04-16T09:12:44+00:00", "description": "Presa in carico", "place": "Napoli" }, { "code": 9, "date": "2026-04-18T14:32:05+00:00", "description": "Consegnata", "place": "Telese Terme (firmato Rossi)" } ] } }

Payload Fields

FieldTypeDescription
eventstringEvent type — currently always tracking.updated
ldvstringThe shipment number, uppercased
statusnumber | nullNew delivery status code
status_descriptionstring | nullItalian label of the status, e.g. Consegnata. null for unmapped codes
occurred_atstring | nullISO-8601 timestamp of the tracking event that triggered the notification
trackingobject | nullThe same object returned by GET /tracking/{ldv}. null if the shipment fell outside the 90-day lookup window

tracking can be null. Always null-check it before reading tracking.events.

Request Headers

HeaderDescriptionExample
Content-TypeAlways application/jsonapplication/json
X-SignatureHMAC-SHA256 of the raw body, prefixed with sha256=sha256=4f8a9b2c...d7e1f3
X-TimestampUnix timestamp (seconds) when the request was sent1776520325
User-AgentIdentifies the senderDelivery-Webhook/2026-04

The signature covers the raw request body only — the timestamp is not part of the signed string. X-Timestamp is informational; use it for replay-window checks and logging, but do not include it when recomputing the HMAC.

Verifying Signatures

How the signature is constructed

  1. Take the raw request body exactly as received — no re-serialising, no whitespace changes.
  2. Compute HMAC-SHA256 of that string using the secret returned when you registered the webhook.
  3. Encode the result as a lowercase hex string.
  4. The X-Signature header is sha256=<hex>.

Verification steps

  1. Check X-Signature is present — return 401 if missing.
  2. Strip the sha256= prefix.
  3. Recompute the HMAC over the raw body using your secret.
  4. Compare using a timing-safe equality function.
  5. Optionally reject requests whose X-Timestamp is far in the past to limit replay windows.
const crypto = require('crypto') // Requires the RAW body. In Express: // app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf } })) function verifyWebhook(req, webhookSecret) { const header = req.headers['x-signature'] if (!header || !header.startsWith('sha256=')) { return false // Missing or malformed signature header } const received = header.slice('sha256='.length) const expected = crypto .createHmac('sha256', webhookSecret) .update(req.rawBody) .digest('hex') const a = Buffer.from(received, 'utf8') const b = Buffer.from(expected, 'utf8') // timingSafeEqual throws on length mismatch — guard first return a.length === b.length && crypto.timingSafeEqual(a, b) }

Always compare with a timing-safe function. Never use ==, === or equals() on HMAC values.

Responding to Notifications

Your endpoint must answer within 15 seconds — the sender’s HTTP timeout.

CodeMeaning
2xxNotification accepted — the delivery is marked successful
401Signature verification failed
4xx / 5xxTreated as a failure and retried

Do all heavy work asynchronously. Persist the payload, answer 200 immediately, and process it in a background job — a slow handler is indistinguishable from a broken one and burns your retries.

Retries and Auto-disable

RuleValue
HTTP timeout per attempt15 seconds
Attempts per notification5
Consecutive failed notifications before the webhook is disabled10
Failure counter resetOn the first successful delivery

A notification that fails all 5 attempts increments the webhook’s failures_count. When that counter reaches 10, the webhook is set to is_active: false and stops receiving notifications entirely — see Manage Webhook for how to recover.

Check failures_count with GET /webhook if you suspect you are missing notifications. A non-zero value means recent deliveries failed.

Idempotency

Retries mean the same notification can arrive more than once. Make your handler idempotent — for example by treating (ldv, status, occurred_at) as a natural key and ignoring a payload you have already processed.

Notifications are also not ordered: a retried older notification can arrive after a newer one. Discard a payload whose occurred_at is older than the latest one you have stored for that ldv.

Last updated on