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:
- A new tracking event is recorded for the shipment.
- At least one active webhook has a subscription for that
ldv. - 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
| Event | Description |
|---|---|
tracking.updated | The 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
| Field | Type | Description |
|---|---|---|
event | string | Event type — currently always tracking.updated |
ldv | string | The shipment number, uppercased |
status | number | null | New delivery status code |
status_description | string | null | Italian label of the status, e.g. Consegnata. null for unmapped codes |
occurred_at | string | null | ISO-8601 timestamp of the tracking event that triggered the notification |
tracking | object | null | The 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
| Header | Description | Example |
|---|---|---|
Content-Type | Always application/json | application/json |
X-Signature | HMAC-SHA256 of the raw body, prefixed with sha256= | sha256=4f8a9b2c...d7e1f3 |
X-Timestamp | Unix timestamp (seconds) when the request was sent | 1776520325 |
User-Agent | Identifies the sender | Delivery-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
- Take the raw request body exactly as received — no re-serialising, no whitespace changes.
- Compute
HMAC-SHA256of that string using thesecretreturned when you registered the webhook. - Encode the result as a lowercase hex string.
- The
X-Signatureheader issha256=<hex>.
Verification steps
- Check
X-Signatureis present — return401if missing. - Strip the
sha256=prefix. - Recompute the HMAC over the raw body using your secret.
- Compare using a timing-safe equality function.
- Optionally reject requests whose
X-Timestampis far in the past to limit replay windows.
Node.js
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.
| Code | Meaning |
|---|---|
2xx | Notification accepted — the delivery is marked successful |
401 | Signature verification failed |
4xx / 5xx | Treated 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
| Rule | Value |
|---|---|
| HTTP timeout per attempt | 15 seconds |
| Attempts per notification | 5 |
| Consecutive failed notifications before the webhook is disabled | 10 |
| Failure counter reset | On 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.