Introduzione
Bozza — i webhook v2 non sono ancora attivi. Questa pagina descrive il contratto che stiamo per aprire; può cambiare prima del rilascio. I riquadri TODO segnano i punti ancora da decidere. Per i webhook attuali vale la documentazione v1.
Scopo
Un webhook è una chiamata POST che Qapla' fa verso un URL vostro
quando succede qualcosa a una spedizione: cambia stato, il corriere registra un
nuovo evento. Invece di interrogare l'API per sapere se qualcosa è cambiato, ricevete il fatto
appena lo conosciamo.
I webhook v2 sono la generazione che accompagna l'API v2:
- stesso schema dell'API: il payload contiene la spedizione nella stessa
forma di
GET /v2/shipments, quindi chi integra ha un solo modello dati; - firmati: ogni richiesta porta un HMAC-SHA256 del corpo, verificabile con un secret che conoscete solo voi e noi;
- configurabili: scegliete quando ricevere (solo i cambi di stato, la prima volta in uno stato, ogni evento del corriere) e per quali stati;
- affidabili: successo = qualunque
2xx, retry con backoff per circa 22 ore, consegne fallite conservate e rimandabili.
Configurazione
Un webhook si configura dal Control Panel, in Impostazioni canale → Aggiornamenti → «Webhook v2». Dalla stessa pagina si mandano i test, si ruota il secret, si consultano e si rimandano le consegne e si riattiva un endpoint spento. L'endpoint appartiene al canale: chi ha più canali lo configura su ognuno, anche con lo stesso URL. Ogni canale può avere fino a 5 endpoint (contano anche quelli spenti), per esempio l'ERP che riceve solo i cambi di stato e il customer care che riceve ogni evento.
Oggi non esistono rotte pubbliche di gestione degli endpoint: creazione, modifica, test, rotazione del secret e replay si fanno dal Control Panel. Il vostro lato del contratto è ricevere: è quello che questa pagina descrive.
| Campo | Significato |
|---|---|
| url | Dove inviamo. Solo https://, al massimo 2048
caratteri, senza credenziali nell'URL (https://user:pass@… è
rifiutato). L'host deve risolvere solo a indirizzi pubblici: indirizzi privati,
loopback, link-local e simili sono rifiutati sia al salvataggio sia
a ogni invio, dopo la risoluzione DNS. |
| events | I tipi di evento sottoscritti. Oggi solo shipment.updated
(Tipi di evento). |
| trigger | status_change (default), first_occurrence o
every_event (Trigger). |
| statuses | Gli stati per cui ricevere. Vuoto = tutti (Filtro per stato). |
| include | Sezioni opzionali del payload. Vuoto di default; oggi accetta solo
consignee (Sezioni opzionali). |
| alertEmail | Obbligatorio. L'indirizzo da avvisare se l'endpoint smette di rispondere (Avvisi e spegnimento). Mettete quello di chi ha fatto l'integrazione, non quello dell'amministrazione. |
| status | active o disabled. Si spegne a mano o da solo dopo
3 giorni di errori; si riattiva dal Control Panel. |
Alla creazione il Control Panel mostra il secret di firma, nella forma
whsec_ seguito da 64 caratteri esadecimali. Viene mostrato una volta
sola: copiatelo subito nella configurazione del vostro ricevitore. Se lo perdete, si
genera un secret nuovo (Rotazione del secret).
Configurare un endpoint v2 spegne la v1 del canale. Da quel momento il canale non riceve più i webhook v1, per non ricevere tutto due volte. Il vostro ricevitore v2 deve essere pronto prima di creare l'endpoint. Spegnere l'endpoint v2 non riaccende la v1, cancellarlo sì: vedi Migrazione dalla v1.
Eventi
Tipi di evento
| type | Quando |
|---|---|
| shipment.updated | Un evento di tracking di una spedizione del canale è diventato una consegna
secondo il trigger e il
filtro per stato dell'endpoint. |
| webhook.test | Solo su richiesta, dal pulsante di test del Control Panel (Evento di test). Non si sottoscrive. |
Altri tipi (etichette, resi) si aggiungeranno nello stesso catalogo. Il tipo arriva sia nel
corpo (type) sia nell'header X-Qapla-Event-Type.
Trigger
Il trigger decide quando un evento della spedizione diventa una
consegna verso il vostro endpoint. Lo stato Qapla' è una coppia
stato + dettaglio (per esempio EXCEPTION con dettaglio
«giacenza»): è su questa coppia che ragionano i primi due trigger.
| trigger | Parte quando | Caso d'uso |
|---|---|---|
| status_change default |
cambia lo stato o il dettaglio Qapla' della spedizione. Tre eventi «in transito» in tre hub diversi producono una sola consegna | «mandami solo gli effettivi cambi di stato». È il comportamento della v1 |
| first_occurrence | la spedizione entra per la prima volta in una coppia stato + dettaglio. Se torna in una coppia già vista, non parte niente | «dimmi quando è in transito, ma una volta» |
| every_event | ogni cambio di stato e ogni nuovo evento del corriere, anche a stato invariato (luogo o data nuovi) | «voglio la history completa» |
Esempio di first_occurrence. Una spedizione va in
giacenza per destinatario assente, poi in giacenza per collo danneggiato, poi di nuovo per
destinatario assente. Le prime due partono (sono due coppie diverse, stesso stato
EXCEPTION); la terza no, quella coppia è già stata consegnata.
Il trigger in uso arriva nel payload come data.trigger.
Filtro per stato
statuses è una lista di nomi di stato Qapla'. Vuota = tutti
gli stati. Si applica dopo il trigger e solo sullo stato, mai sul dettaglio:
["EXCEPTION"] prende tutte le eccezioni, qualunque sia il dettaglio. Se vi servono
solo alcuni dettagli, filtrateli dal vostro lato con data.event.statusDetailId.
Valori ammessi: WAITING_TO_COMPUTE, PENDING,
INFO_RECEIVED, IN_TRANSIT, OUT_FOR_DELIVERY,
FAILED_ATTEMPT, EXCEPTION, DELAY,
PICKUP_POINT, DEPARTED, PROCESSING,
RETURNED, DELIVERED. Un nome sconosciuto è un errore di
configurazione.
«Solo le consegne» è status_change +
statuses: ["DELIVERED"].
Sezioni opzionali
include aggiunge al payload sezioni che di default non ci sono. Oggi accetta
un solo valore:
| consignee | Aggiunge data.shipment.consignee (nome, indirizzo, email,
telefono del destinatario). Di default non c'è: per agganciare
l'evento al vostro ordine bastano riferimento ordine e tracking number, e il
destinatario lo conoscete già. Attivatelo solo se vi serve davvero
(minimizzazione dei dati personali, GDPR art. 5.1.c). |
|---|
Lo schema resta uno: il campo consignee c'è o non c'è, non cambia forma.
include è una lista e non un sì/no perché potrà accogliere altre sezioni senza
rompere il contratto.
Evento di test
Dal Control Panel si può mandare subito un evento webhook.test all'endpoint. È
firmato col vostro secret vero, con gli stessi header di una consegna reale:
serve a verificare la firma prima di andare live. Il Control Panel mostra il codice HTTP che
avete risposto, la durata e l'eventuale errore.
- stesso envelope di
shipment.updated, con una spedizione d'esempio fissa (non una vostra spedizione);consigneec'è solo se l'endpoint hainclude: ["consignee"]; - l'
idha la formaevt_test_…ed è diverso a ogni test; - un tentativo solo: non si ritenta e non finisce fra le consegne. Un
test fallito non conta per gli avvisi e lo spegnimento; un test
riuscito (
2xx) invece riporta l'endpoint sano, azzerando il conteggio dei fallimenti.
Il ricevitore deve rispondere 2xx anche a webhook.test, e non
deve trattarlo come un aggiornamento reale.
Payload
Struttura
Un evento per richiesta, niente batch. Content-Type:
application/json, UTF-8.
| Campo | Significato |
|---|---|
| id(string) | Identità dell'evento, evt_…. Resta uguale nei retry e nei
replay: è la vostra chiave di deduplica (Ordine e
duplicati). Arriva anche nell'header X-Qapla-Event-Id. |
| type(string) | shipment.updated o webhook.test. |
| createdAt(string) | Quando è nato l'evento da noi, ISO 8601 con offset
(2026-10-03T09:41:07+02:00). |
| apiVersion(string) | "2". Un cambio incompatibile della risorsa sarà una nuova
apiVersion, per l'API e per il webhook insieme. |
| data.trigger(string) | Il trigger dell'endpoint che ha generato la consegna. |
| data.shipment(object) | La spedizione, nello stato corrente: è la risorsa
ShipmentSummary dell'API v2, la stessa di ogni elemento di
GET /v2/shipments. Lo schema completo è
in Swagger UI (sezione
Schemas → ShipmentSummary) e in
openapi.json
(#/components/schemas/ShipmentSummary). Senza consignee
salvo include: ["consignee"]. Per colli, righe d'ordine e history
completa: GET /v2/shipments/{id} con
data.shipment.id. |
| data.event(object) | Il fatto che ha generato la consegna, nella forma di un elemento di
history[] (TrackingHistoryEvent):
date, courierStatus (il testo del corriere),
place, statusCode (nome dello stato Qapla'), più
statusDetailId (id del dettaglio, 0 = nessuno).
status e statusDetail (descrizioni) oggi sono
null. |
Il payload si costruisce al momento dell'invio.
data.event è il fatto originale, data.shipment è la spedizione
com'è adesso. Un «in transito» ritentato due ore dopo può arrivare con
event.statusCode: IN_TRANSIT e shipment.status già
«consegnata». Per lo stesso motivo un retry o un replay non è identico byte per byte
all'invio precedente: è uguale l'id.
Nuovi campi possono comparire nella risorsa (e quindi nel webhook) senza cambio di
apiVersion: il ricevitore deve ignorare i campi che non conosce.
Rappresentazione dello stato (aperto nell'API v2). Oggi
data.shipment.status è l'intero dello stato
(4), mentre data.event.statusCode ne usa il
nome (OUT_FOR_DELIVERY), come in
history[].statusCode. data.event.status e
data.event.statusDetail sono null. Va deciso nell'API v2 prima di
congelare questo contratto: il webhook eredita la scelta.
Offset delle date (aperto nell'API v2). createdAt ha
l'offset (+02:00, ora di Roma), ma le date della spedizione
(statusDate, statusUpdatedAt) e data.event.date sono
YYYY-MM-DD HH:MM:SS senza offset. Il database lavora con un
time_zone fisso: va deciso come esporre l'offset, nell'API e quindi qui.
Ordine e duplicati
L'ordine di arrivo non è garantito. Le consegne sono indipendenti e possono viaggiare in parallelo: un evento vecchio in retry non blocca quelli nuovi. È una scelta: una consegna in serie farebbe aspettare ore una «consegnata» dietro un errore temporaneo di un evento precedente.
Regola di scarto: ignorate un evento se il suo
data.shipment.statusDate è più vecchio di quello che avete già salvato per
quella spedizione. Così un evento arrivato in ritardo non sovrascrive uno stato
più recente.
Lo stesso evento può arrivare più di una volta: un retry dopo un timeout
in cui avevate già elaborato la richiesta, un replay. Usate id (o
X-Qapla-Event-Id) come chiave di idempotenza: se l'avete già elaborato,
rispondete 2xx e non fate altro. Una consegna fallita è rimandabile per
30 giorni (Replay): conservate gli id
almeno per quel periodo.
Se più endpoint dello stesso canale sottoscrivono lo stesso evento, ognuno riceve la sua
consegna con lo stesso id.
Rispondete prima di fare il lavoro pesante: salvate l'evento, rispondete
2xx, elaboratelo dopo. Avete 10 secondi in tutto, e una
risposta oltre il timeout vale un fallimento anche se l'avete elaborata.
Sicurezza
Firma
Ogni richiesta porta l'header:
X-Qapla-Signature: t=1791015120,v1=961fde4b87a60528ed28100be4badc6dcf1d64ce84b82f2a3fe9b077f45c0d2a
tè il momento dell'invio, in secondi Unix;v1è l'HMAC-SHA256 in esadecimale minuscolo della stringa"<t>.<corpo grezzo>", con chiave il secret dell'endpoint così com'è, prefissowhsec_compreso;- durante una rotazione del secret arrivano due
v1, uno per ogni secret valido: la richiesta è autentica se almeno uno corrisponde.
Per verificare:
- leggete il corpo grezzo, i byte come sono arrivati, prima di qualsiasi parsing JSON. Ricodificare il JSON cambia spazi e escape e la firma non torna più;
- estraete
te tutti iv1dall'header; - rifiutate la richiesta se
tsi discosta dal vostro orologio di più di 5 minuti (protezione dal replay di una richiesta intercettata); - calcolate l'HMAC-SHA256 di
t + "." + corpocol vostro secret e confrontatelo con ogniv1a tempo costante (hash_equals,crypto.timingSafeEqual,hmac.compare_digest), mai con==; - firma non valida: rispondete
401o403e scartate. Non verrà ritentata (Retry).
Lo schema è lo stesso delle Courier Events API, che usano la firma nella direzione opposta.
Verifica della firma
Implementazioni complete, senza dipendenze esterne. Tutte e tre accettano la richiesta se
almeno un v1 corrisponde, quindi funzionano anche durante una rotazione.
<?php
/**
* True when at least one v1 in X-Qapla-Signature matches and t is within the tolerance.
* $rawBody: the request body exactly as received, before any json_decode.
*/
function qaplaSignatureIsValid(string $rawBody, string $header, string $secret, int $tolerance = 300, ?int $now = null): bool
{
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
$pair = explode('=', trim($part), 2);
if (count($pair) !== 2) {
continue;
}
if ($pair[0] === 't') {
$timestamp = $pair[1];
} elseif ($pair[0] === 'v1') {
$signatures[] = $pair[1];
}
}
if ($timestamp === null || !ctype_digit($timestamp) || abs(($now ?? time()) - (int) $timestamp) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) {
return true;
}
}
return false;
}
// Uso
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_QAPLA_SIGNATURE'] ?? '';
if (!qaplaSignatureIsValid($rawBody, $header, getenv('QAPLA_WEBHOOK_SECRET'))) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody);
const crypto = require('crypto');
// rawBody: Buffer with the request body exactly as received (express.raw, not express.json).
function qaplaSignatureIsValid(rawBody, header, secret, toleranceSec = 300, now = Math.floor(Date.now() / 1000)) {
let timestamp = null;
const signatures = [];
for (const part of String(header || '').split(',')) {
const i = part.indexOf('=');
if (i < 0) continue;
const key = part.slice(0, i).trim();
const value = part.slice(i + 1).trim();
if (key === 't') timestamp = value;
else if (key === 'v1') signatures.push(value);
}
if (!/^\d+$/.test(timestamp || '') || Math.abs(now - Number(timestamp)) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
return signatures.some((signature) => {
const received = Buffer.from(signature, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
// Uso con Express: il corpo deve restare un Buffer
app.post('/qapla/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!qaplaSignatureIsValid(req.body, req.get('X-Qapla-Signature'), process.env.QAPLA_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(204);
});
import hashlib
import hmac
import time
def qapla_signature_is_valid(raw_body: bytes, header: str, secret: str, tolerance: int = 300, now: float | None = None) -> bool:
"""raw_body: the request body exactly as received (e.g. Flask request.get_data())."""
timestamp, signatures = None, []
for part in (header or "").split(","):
key, sep, value = part.strip().partition("=")
if not sep:
continue
if key == "t":
timestamp = value
elif key == "v1":
signatures.append(value)
if timestamp is None or not timestamp.isdigit():
return False
if abs((time.time() if now is None else now) - int(timestamp)) > tolerance:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, signature) for signature in signatures)
# Uso con Flask
@app.post("/qapla/webhook")
def qapla_webhook():
if not qapla_signature_is_valid(request.get_data(), request.headers.get("X-Qapla-Signature", ""), os.environ["QAPLA_WEBHOOK_SECRET"]):
abort(401)
event = request.get_json()
return "", 204
Vettore di prova
Per controllare la vostra implementazione senza aspettare un invio reale:
| corpo | 90-signature-test-body.json (883 byte, una riga, senza a capo finale: usate il file così com'è) |
|---|---|
| secret | whsec_3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f |
| secret precedente | whsec_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1 |
| header | t=1791015120,v1=961fde4b87a60528ed28100be4badc6dcf1d64ce84b82f2a3fe9b077f45c0d2a,v1=6e5964a9d6a80fd60aca19bdbb62ddd531a4490e3ad7b37ba1bc9a2bcf4b3499 |
Con uno qualsiasi dei due secret la verifica deve riuscire, con un altro secret no.
t è vecchio: per la prova disattivate il controllo della tolleranza o passate
come «adesso» un istante entro 5 minuti da 1791015120 (gli esempi accettano un
parametro now).
Rotazione del secret
Dal Control Panel si genera un secret nuovo, mostrato una volta sola. Il vecchio
resta valido per 24 ore: in quella finestra ogni
richiesta porta due v1, prima quello del secret nuovo, poi quello del vecchio.
Dopo, solo il nuovo.
Per ruotare senza perdere consegne:
- generate il secret nuovo dal Control Panel;
- entro 24 ore mettetelo nella configurazione del ricevitore al posto del vecchio. Le richieste continuano a passare in entrambe le configurazioni, perché portano tutte e due le firme.
Ruotate subito se il secret è finito dove non doveva (log, repository, ticket).
IP di uscita
Il meccanismo di autenticazione è la firma, non l'indirizzo di
provenienza: verificate sempre X-Qapla-Signature, anche se filtrate per IP.
Per chi deve comunque configurare un firewall, gli indirizzi da cui partono le nostre chiamate sono pubblicati qui, in testo semplice, uno per riga: https://cdn.qapla.it/sys/219a40582012cf87b88a782bdfe3a569. La lista può cambiare: se la usate, rileggetela periodicamente invece di copiarla una volta.
Verificare che i webhook v2 escano dagli stessi indirizzi della lista (o aggiungere i loro) prima del primo invio.
Consegna
Richiesta
POST{il vostro url}
| Header | Valore |
|---|---|
| Content-Type | application/json |
| User-Agent | Qapla-Webhooks/2 |
| X-Qapla-Event-Id | uguale a id nel corpo (evt_…) |
| X-Qapla-Event-Type | uguale a type nel corpo |
| X-Qapla-Delivery-Attempt | numero del tentativo, da 1. Riparte da 1 dopo un
replay |
| X-Qapla-Signature | t=…,v1=… (Firma) |
- solo
https, con il certificato del server verificato: un certificato scaduto, autofirmato o per un altro host è un errore di connessione; - timeout 10 secondi per l'intera richiesta, connessione compresa;
- nessun redirect: una risposta
3xxnon viene seguita e vale come fallimento definitivo. Configurate l'URL finale; - l'host viene risolto a ogni invio e deve risolvere solo a indirizzi pubblici. Se il DNS non risponde, si ritenta; se risolve a un indirizzo non pubblico, la consegna fallisce senza retry.
Risposta
Successo = qualunque 2xx. Il corpo della risposta viene
ignorato: non serve rispondere {"result":"OK"} come nella v1, va benissimo un
204 vuoto.
| Esito | Cosa facciamo |
|---|---|
2xx |
consegnata |
| timeout, errore di connessione o TLS, DNS senza risposta | ritentiamo |
408, 429, 5xx |
ritentiamo. Potete indicare Retry-After
(Retry) |
3xx, altri 4xx (400, 401,
404, 410, …) |
non ritentiamo: la consegna fallisce subito e resta disponibile per il replay |
Un 4xx dice «questa richiesta non andrà mai bene». Se il problema è
temporaneo dal vostro lato (deploy in corso, database giù), rispondete 503,
non 400: altrimenti l'evento non torna da solo.
Retry
Un esito ritentabile rimette in coda la consegna con un ritardo crescente, con un jitter di ±20% per non far ripartire tutto nello stesso istante. Al massimo 8 tentativi, distribuiti su circa 22 ore:
| Dopo il tentativo | Attesa (nominale) | Tentativo successivo, dal primo |
|---|---|---|
| 1 | 1 minuto | 1 min |
| 2 | 5 minuti | 6 min |
| 3 | 15 minuti | 21 min |
| 4 | 1 ora | 1 h 21 min |
| 5 | 3 ore | 4 h 21 min |
| 6 | 6 ore | 10 h 21 min |
| 7 | 12 ore | 22 h 21 min (ottavo e ultimo tentativo) |
| 8 | — | la consegna passa fra le fallite (DLQ) |
Retry-After (secondi o data HTTP) può solo
allungare l'attesa, fino a un massimo di 12 ore: se
indica meno del backoff previsto vale il backoff. Non aggiunge tentativi.
Avvisi e spegnimento
Un endpoint che fallisce per giorni non viene ritentato per sempre:
- se sono passate 24 ore dal primo fallimento
senza nessuna consegna riuscita, mandiamo un primo avviso ad
alertEmail; - se ne sono passati 3 giorni, l'endpoint passa
a
disabled, con un secondo avviso; - le soglie dipendono dal tempo trascorso dal primo fallimento, non dal numero di tentativi né da quanti eventi arrivano nel frattempo;
- il conteggio si azzera al primo
2xx, che sia una consegna o un evento di test mandato dal Control Panel: dopo aver sistemato il ricevitore, un test riuscito riporta l'endpoint sano.
Contano come fallimenti gli esiti non 2xx e gli errori di connessione. Un
evento di test fallito non conta.
Quando l'endpoint si spegne (da solo o a mano):
- le consegne ancora in attesa di retry passano subito fra le fallite, e restano rimandabili col replay;
- da spento non si generano eventi: niente si accumula mentre è giù.
Si riattiva dal Control Panel (Impostazioni canale → Aggiornamenti →
«Webhook v2»). Riparte pulito, senza un'ondata di eventi
vecchi. Il buco si recupera con
GET /v2/shipments?updatedAfter=…, partendo
dall'ultimo aggiornamento che avete ricevuto.
Replay
Le consegne fallite (esauriti i retry, rifiutate con un 4xx, o interrotte dallo
spegnimento dell'endpoint) si possono rimandare dal Control Panel, una per una
o per intervallo di tempo, per 30 giorni dalla nascita
della consegna. Il Control Panel mostra per ogni consegna stato, tentativi, ultimo codice HTTP
ed errore.
Un replay rimette la consegna in coda come nuova: stesso id,
X-Qapla-Delivery-Attempt che riparte da 1, di nuovo fino a
8 tentativi, e payload ricostruito con la spedizione com'è al momento del
nuovo invio.
Lista delle consegne e replay sono in Impostazioni canale → Aggiornamenti → «Webhook v2».
Riferimento
Migrazione dalla v1
La v2 non sostituisce la v1 d'ufficio: si attiva per scelta, configurando un endpoint v2 sul canale, perché richiede modifiche al vostro codice. La v1 (documentazione) resta com'è, senza nuove funzioni: filtri e trigger esistono solo nella v2. Non c'è ancora una data di fine della v1; sarà annunciata con 12 mesi di preavviso.
| v1 | v2 | |
|---|---|---|
| Autenticità | apiKey del canale in chiaro nel body |
nessuna credenziale nel body; firma HMAC X-Qapla-Signature |
| Successo | il corpo della risposta deve essere {"result":"OK"} |
qualunque 2xx, il corpo è ignorato |
| Payload | formato v1 | envelope con ShipmentSummary dell'API v2 + l'evento |
| Dati del destinatario | solo con fullData attivo (opt-in): name,
address, email |
solo con include: ["consignee"]
(Sezioni opzionali) |
| Quando | a ogni cambio di stato | trigger a scelta + filtro statuses |
| Trasporto | http o https |
solo https, certificato verificato, nessun
redirect, timeout 10 s |
| Errori | pochi tentativi, nessun replay | 8 tentativi in ~22 h, consegne fallite rimandabili per 30 giorni, avviso e spegnimento |
Se oggi ricevete su http://, prima di passare alla v2
serve un certificato TLS valido sul vostro endpoint.
Un canale con un endpoint v2 smette di ricevere la v1. Niente doppio invio, ma anche niente periodo in cui ricevete entrambe: il ricevitore v2 deve essere in produzione prima di creare l'endpoint.
Procedura consigliata
- implementate il ricevitore v2: verifica della firma, deduplica su
id, regola di scarto sustatusDate, risposta2xxveloce; - provatelo col vettore di prova;
- mettetelo in produzione accanto al ricevitore v1, su un URL diverso;
- dal Control Panel create l'endpoint v2 e salvate il secret: da qui la v1 del canale si ferma;
- mandate un evento di test e controllate l'esito;
- recuperate eventuali aggiornamenti persi nel passaggio con
GET /v2/shipments?updatedAfter=….
Spento e cancellato non sono la stessa cosa per la v1.
- Un endpoint v2 spento (a mano o dopo 3 giorni di errori) tiene il canale fuori dalla v1: il canale non riceve né la v2 né la v1 finché non lo riattivate;
- un endpoint v2 cancellato no: se sul canale non ne restano altri, il canale torna a ricevere la v1, sempre che il webhook v1 sia ancora configurato.
Per sospendere la v2 senza ricevere di nuovo la v1, spegnete l'endpoint; per tornare alla v1, cancellatelo.
Esempi
| File | Contenuto |
|---|---|
| 01-shipment-updated.json | shipment.updated con trigger status_change, senza
consignee |
| 02-webhook-test.json | l'evento di test, con la spedizione d'esempio fissa |
| 90-signature-test-body.json | corpo grezzo del vettore di prova della firma |
Punti aperti
Prima della pubblicazione vanno chiusi i riquadri TODO di questa pagina:
- rappresentazione dello stato nell'API v2 (intero o nome);
- offset delle date nell'API v2;
- IP di uscita dei webhook v2.
Contatto: tech@qapla.it