Skip to Content

Webhooks

Webhooks push events to your HTTP endpoint as they happen, so you do not need to poll the tracking endpoints. Every delivery is signed with HMAC-SHA256 so you can verify it comes from Spedisci.online.

Endpoints can be managed through the API (below) or from the Webhooks page of the web interface; both use the same signature and events.

Events

EventWhenPayload
tracking.updateThe status of a shipment changes (tracking refresh or carrier event)see below
shipment.createdA shipment is created — through the API, the web interface or from an ordersee below
stock.openedThe carrier opens a stock (held shipment) on a shipmentsee below
stock.closedThe stock is closed (shipment released, delivered or returned)same as stock.opened

Only the events listed in events at registration are delivered. An endpoint registered from the web interface without a list receives tracking.update, stock.opened and stock.closed.

Managing endpoints

MethodEndpointDescription
GET/webhooksRegistered endpoints
POST/webhooksRegister an endpoint — requires an active subscription
GET/webhooks/{id}Detail
DELETE/webhooks/{id}Delete the endpoint and its delivery log (204)
GET/webhooks/{id}/deliveriesDeliveries of the last 10 days (paginated)
POST/webhooks/{id}/testSend a real, signed tracking.update — rate limit 5 / min

Register an endpoint

FieldTypeRequiredDescription
urlstring (255)YesHTTPS URL of your endpoint
descriptionstring (255)No
events[]string[]YesOne or more of the events above
{ "url": "https://erp.acme.it/webhooks/spedisci", "description": "ERP sync", "events": ["tracking.update", "shipment.created", "stock.opened", "stock.closed"] }

The signing secret is returned only in the creation response. Store it right away; it cannot be read again. To rotate it, delete the endpoint and register it again.

HTTPcodeWhen
402subscription_requiredThe account has no valid subscription
422validation_failedInvalid URL or unknown event

Delivery

Each event is one HTTP POST to your endpoint with a JSON body and these headers:

HeaderDescriptionExample
Content-Typeapplication/json
User-Agentspedisci.online webhook v1 <subdomain>
Webhook-TimestampUnix timestamp (seconds) of the delivery1756886400
Webhook-Signaturet=<timestamp>,v1=<hex HMAC-SHA256>t=1756886400,v1=4f8a9b2c…d7e1f3

Deliveries are sent asynchronously, independently for each endpoint, with a 10-second timeout:

  • A 2xx answer marks the delivery as success.
  • Network errors and 5xx answers are retried 3 times in total, after 30 seconds, 2 minutes and 5 minutes.
  • A 4xx answer is not retried: it is the endpoint’s final word and is only logged.
  • An endpoint with no 2xx answer for 72 hours is deactivated automatically (isActive: false) and the client is notified by email. Re-enable it from the web interface, or delete and register it again.

Answer quickly (ideally 200 before processing) and process the event asynchronously. Events may occasionally arrive more than once or out of order: use ldv + timestamp to deduplicate.

Verifying the signature

  1. Read Webhook-Timestamp and Webhook-Signature; reject the request if either is missing.
  2. Reject it if the timestamp is older than 300 seconds (replay protection).
  3. Compute HMAC-SHA256("<timestamp>.<raw body>", secret) as lowercase hex.
  4. Compare it to the v1= value with a timing-safe comparison.
const crypto = require('crypto') function verifyWebhook(rawBody, headers, secret) { const timestamp = headers['webhook-timestamp'] const signature = headers['webhook-signature'] if (!timestamp || !signature) return false if (Math.floor(Date.now() / 1000) - parseInt(timestamp, 10) > 300) return false const v1 = signature.split(',').find(p => p.startsWith('v1='))?.slice(3) if (!v1) return false const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex') return v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) }

Sign the raw request body exactly as received. Re-serialising the JSON before hashing changes whitespace and key order and breaks the signature.

Payloads

All payloads carry event, timestamp (ISO 8601), ldv (tracking number), contractCode, statusCode (numeric shipment status) and domain (the platform host).

tracking.update

{ "event": "tracking.update", "timestamp": "2026-09-04T11:50:12+02:00", "ldv": "DEMO05407027", "vector_name": "Brt", "order_id": "ORD-1234", "client_store_id": null, "TrackingDettaglio": [ { "Data": "04/09/2026 11:42", "Stato": "Consegnata", "Luogo": "Roma" }, { "Data": "04/09/2026 08:15", "Stato": "In consegna", "Luogo": "Roma" }, { "Data": "03/09/2026 10:26", "Stato": "Spedizione generata", "Luogo": "Milano" } ], "statusCode": 5, "contractCode": "brt-standard", "domain": "demo.spedisci.online" }
FieldDescription
vector_nameCarrier name
order_idThe reference of the shipment
client_store_idStore the order came from, when created from an order
TrackingDettaglio[]Events from the most recent: Data (dd/mm/yyyy HH:MM, local time), Stato, Luogo
statusCodeStatus code after the update (always an integer)

This payload is identical to the one sent by the legacy webhooks, with event and timestamp added in front: an endpoint written for API v2 keeps working.

shipment.created

{ "event": "shipment.created", "timestamp": "2026-09-03T10:26:55+02:00", "ldv": "DEMO05407027", "shipping_id": 4454058, "client_id": 1520, "client_store_id": null, "order_id": "ORD-1234", "vector_name": "Brt", "contractCode": "brt-standard", "statusCode": 0, "domain": "demo.spedisci.online" }

stock.opened / stock.closed

{ "event": "stock.opened", "timestamp": "2026-09-05T09:15:31+02:00", "ldv": "DEMO05407027", "giacenza_id": "GIA-2026-000123", "opened_at": "2026-09-05T09:12:00+02:00", "stock_id": 9812, "shipping_id": 4454058, "statusCode": 6, "contractCode": "brt-standard", "domain": "demo.spedisci.online" }

stock_id is the id to use with GET /stocks/{id} and POST /stocks/{id}/actions. giacenza_id is the carrier dossier: after a release the carrier can reissue the LDV and open a second dossier on the same parcel.

Delivery log

GET /webhooks/{id}/deliveries — paginated, last 10 days

{ "data": [ { "id": 99120, "event": "tracking.update", "status": "success", "responseCode": 200, "responseBody": "ok", "payload": { "event": "tracking.update", "ldv": "DEMO05407027", "statusCode": 5 }, "createdAt": "2026-09-04T11:50:13+02:00" } ], "links": { "first": "...", "last": "...", "prev": null, "next": null }, "meta": { "current_page": 1, "from": 1, "last_page": 1, "per_page": 50, "to": 1, "total": 1 } }

status is success or failed; responseCode is 0 on connection errors, with the error in responseBody (truncated to 1000 characters).

Testing an endpoint

POST /webhooks/{id}/test — rate limit 5 / min

Sends a real tracking.update built from your most recent shipment in transit, signed with the endpoint’s secret, and returns the outcome:

{ "delivered": true, "responseCode": 200, "responseBody": "ok", "payload": { "event": "tracking.update", "ldv": "DEMO05407027", "statusCode": 3 } }

Answers 404 not_found when the account has no shipment in transit yet.

Last updated on