MMoovExpressDocs

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_…"
}
MetodoEndpointDescrizione
POST/api/v1/webhooksCrea un webhook: { "url": "https://…", "eventi"?: [...] }. Restituisce il secret (solo qui).
GET/api/v1/webhooksElenca 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

EventoQuando viene inviato
shipment.createdSpedizione creata via API
tracking.updatedLo stato della spedizione è avanzato (in transito, in consegna)
tracking.deliveredSpedizione consegnata al destinatario
tracking.exceptionAnomalia: 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:

statusSignificatoEvento associato
in_lavorazioneCreata, in preparazioneshipment.created
speditaPresa in carico dal corrieretracking.updated
in_transitoIn transitotracking.updated
in_consegnaIn consegnatracking.updated
consegnataConsegnatatracking.delivered
in_giacenzaIn giacenzatracking.exception
non_consegnatoMancata consegnatracking.exception
reso_mittenteIn reso al mittentetracking.exception
annullataAnnullata (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:

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.

EsitoComportamento
2xxEvento ricevuto: nessun altro tentativo
altro codice / timeout / errore di reteRiproviamo: 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.