Webhooks
Ricevi notifiche in tempo reale sugli eventi delle spedizioni
I webhook permettono a MoovExpress di notificare il tuo sistema appena cambia qualcosa su una spedizione, senza doverla interrogare di continuo (niente polling).
Registrazione
Registri il tuo endpoint via API, autenticandoti con la tua API key (la stessa dei tuoi contratti attivi — vedi Autenticazione). Il secret di firma (whsec_…) viene generato dal sistema e restituito una sola volta, nella risposta alla creazione: annotalo subito.
curl -X POST https://moovexpress.com/api/v1/webhooks \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tuo-dominio.it/hooks/moov",
"eventi": ["shipment.created","tracking.updated","tracking.delivered","tracking.exception"]
}'
Risposta 201:
{
"id": "…",
"url": "https://tuo-dominio.it/hooks/moov",
"eventi": ["shipment.created","tracking.updated","tracking.delivered","tracking.exception"],
"contratto_id": "…",
"attivo": true,
"secret": "whsec_…"
}
| Metodo | Endpoint | Descrizione |
|---|---|---|
POST | /api/v1/webhooks | Crea un webhook: { "url": "https://…", "eventi"?: [...] }. Restituisce il secret (solo qui). |
GET | /api/v1/webhooks | Elenca i webhook dell'account (secret mascherato). |
PATCH | /api/v1/webhooks/{id} | Aggiorna: { "attivo": false }, { "eventi": [...] }, oppure { "rotateSecret": true } (rigenera e restituisce un nuovo secret). |
DELETE | /api/v1/webhooks/{id} | Elimina un webhook. |
L'URL deve essere https. Se ometti eventi (o lo lasci vuoto) ricevi tutti gli eventi.
Ogni webhook è legato al contratto della API key con cui lo crei. Se integri più contratti attivi (una key per contratto), registra il tuo URL una volta per ciascuna key: lo stesso indirizzo va bene, ogni contratto invierà i propri eventi senza duplicati.
Il secret in chiaro si vede solo alla creazione; in GET è mascherato. Se lo perdi, rigeneralo con PATCH { "rotateSecret": true } (il vecchio smette subito di valere) oppure elimina e ricrea il webhook.
Tipi di evento
| Evento | Quando viene inviato |
|---|---|
shipment.created | Spedizione creata via API |
tracking.updated | Lo stato della spedizione è avanzato (in transito, in consegna) |
tracking.delivered | Spedizione consegnata al destinatario |
tracking.exception | Anomalia: giacenza, mancata consegna o reso |
Giacenza e reso non hanno un evento dedicato: arrivano come tracking.exception e si distinguono dal campo status (in_giacenza, non_consegnato, reso_mittente).
Payload
Il tuo endpoint riceve una POST con questo corpo JSON:
{
"event": "tracking.delivered",
"timestamp": 1789200000,
"data": {
"tracking_number": "MVX0012345678",
"carrier": "Standard Nazionale",
"status": "consegnata",
"location": "Roma",
"events": []
}
}
Il campo status è quello autorevole. Valori possibili:
status | Significato | Evento associato |
|---|---|---|
in_lavorazione | Creata, in preparazione | shipment.created |
spedita | Presa in carico dal corriere | tracking.updated |
in_transito | In transito | tracking.updated |
in_consegna | In consegna | tracking.updated |
consegnata | Consegnata | tracking.delivered |
in_giacenza | In giacenza | tracking.exception |
non_consegnato | Mancata consegna | tracking.exception |
reso_mittente | In reso al mittente | tracking.exception |
annullata | Annullata (credito riaccreditato) | tracking.updated |
Il campo events è lo storico del tracking e può arrivare vuoto: fai riferimento a status.
Header e verifica della firma
Ogni richiesta include due header:
Webhook-Timestamp— timestamp Unix (secondi) dell'invioWebhook-Signature— firma nel formatot=<timestamp>,v1=<hex>
La firma è un HMAC-SHA256 calcolato su {timestamp}.{corpo_grezzo} con il tuo secret, in esadecimale minuscolo. Usa il secret esattamente come restituito (incluso il prefisso whsec_).
Verifica sempre la firma e scarta le richieste con timestamp più vecchio di 300 secondi (protezione da replay). Usa un confronto timing-safe, mai == / ===.
import crypto from "crypto";
function verificaWebhook(rawBody, headers, secret) {
const sig = headers["webhook-signature"] || "";
const parti = Object.fromEntries(sig.split(",").map((p) => p.split("=")));
const t = Number(parti.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // troppo vecchia
const atteso = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(atteso);
const b = Buffer.from(parti.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
function verificaWebhook(string $rawBody, array $headers, string $secret): bool {
$sig = $headers['Webhook-Signature'] ?? '';
parse_str(str_replace(',', '&', $sig), $parti);
$t = (int)($parti['t'] ?? 0);
if (!$t || abs(time() - $t) > 300) return false;
$atteso = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($atteso, $parti['v1'] ?? '');
}
Importante: la firma si calcola sul corpo grezzo della richiesta, prima di qualsiasi parsing JSON.
Risposte attese e retry
Il tuo endpoint deve rispondere 2xx entro 5 secondi. In caso contrario riproviamo la consegna.
| Esito | Comportamento |
|---|---|
2xx | Evento ricevuto: nessun altro tentativo |
| altro codice / timeout / errore di rete | Riproviamo: fino a 3 tentativi in tutto, attese di 1s e poi 3s |
Poiché in caso di problemi lo stesso evento può arrivare più di una volta, rendi il tuo handler idempotente (deduplica su tracking_number + event + timestamp). L'ordine di arrivo non è garantito.
Rispondi 2xx subito e processa l'evento in background: eviti timeout e retry inutili.