Webhook e sincronizzazione
Guida tecnica per il gestionale: sottoscrivi, gestisci ogni scenario di notifica, riallinea con il feed, controlla job e audit.
Sottoscrizione
curl -X POST "$TAKO_API_URL/api/v1/webhooks/subscriptions" \
-H "Authorization: Bearer $TAKO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://gestionale.example/webhooks/tako",
"supplierId": "supplier-id",
"notificationTypes": [
"PRODUCT_UPDATE",
"AVAILABILITY_UPDATE",
"BOOKING_UPDATE"
],
"headers": {
"Authorization": "Bearer receiver-secret"
}
}'| Campo | Valore |
|---|---|
url | HTTPS pubblico. Niente credenziali nell'URL, niente host privati. |
supplierId | Limita al fornitore. Omesso: tutti i fornitori del tenant (solo con chiave tenant). |
notificationTypes | PRODUCT_UPDATE, AVAILABILITY_UPDATE, BOOKING_UPDATE. Default: solo BOOKING_UPDATE. |
headers | Header inviati a ogni POST, cifrati a riposo e mai restituiti. Usali per autenticare Tako. |
Scope bookings:write per creare, modificare e cancellare; bookings:read per elencare. PATCH /webhooks/subscriptions/{id} accetta gli stessi campi più status: ACTIVE riattiva una sottoscrizione disabilitata e azzera il contatore errori. Una sola sottoscrizione per URL e ambito.
Envelope e contratto HTTP
{
"id": "evt_9f2c…:sub_a1b2…:prd_7788",
"subscriptionId": "sub_a1b2…",
"notificationType": "PRODUCT_UPDATE",
"utcCreatedAt": "2026-09-15T10:12:00.000Z",
"data": { "productId": "prd_7788", "optionId": "opt_1122" }
}| Campo | Significato |
|---|---|
id | Deterministico: evento, sottoscrizione ed entità. Un retry ripete lo stesso id, deduplica su questo. |
notificationType | Uno dei tre tipi sottoscritti. |
utcCreatedAt | Istante dell'evento in Tako, non della consegna. |
data | Solo identificatori. PRODUCT_UPDATE: productId, optionId se noto. AVAILABILITY_UPDATE: productId, optionId, localDateStart, localDateEnd. BOOKING_UPDATE: id, uuid. |
consecutiveFailures e dopo 20 di fila la sottoscrizione passa a DISABLED con disabledReason. Un retry può ripetere consegne già riuscite dello stesso evento: l'id resta identico.Scenari: cosa ricevi e cosa chiamare
Ogni notifica dice solo che qualcosa è cambiato. La verità è sempre la risposta della chiamata nella terza colonna. Gli endpoint sono relativi a /api/v1.
| Cosa è cambiato in Tako | Notifica e data | Chiamata | Cosa aggiornare |
|---|---|---|---|
| Prodotto modificato (nome, descrizioni, durata, fuso, cutoff) | PRODUCT_UPDATE { productId } | GET /products/{productId} | Anagrafica del prodotto. |
| Prodotto eliminato, oppure status INACTIVE | PRODUCT_UPDATE { productId } | GET /products/{productId} | 404 o status INACTIVE: ferma la vendita del prodotto e delle sue opzioni. Le prenotazioni esistenti restano valide. |
| Opzione creata, modificata o eliminata | PRODUCT_UPDATE { productId } | GET /products/{productId} | Riconcilia options[] per id: quelle assenti sono eliminate. Ogni opzione porta units[], resourceRequirements[], availabilityPeriods[], pricingPeriods[]. |
| Unità creata, modificata o eliminata (Adulto, Bambino…) | PRODUCT_UPDATE { productId } | GET /products/{productId} | Riconcilia units[] di ogni opzione per id. Un'unità ritirata sparisce dall'elenco ma resta nelle prenotazioni storiche. |
| Listino sostituito (PUT pricing-periods) | PRODUCT_UPDATE { productId, optionId } | GET /options/{optionId}/pricing-periods | Sostituisci tutti i periodi dell'opzione: gli id sono rigenerati a ogni PUT. Le prenotazioni già create conservano pricingSnapshot. |
| Orari ricorrenti (availability-periods) creati o rimossi | PRODUCT_UPDATE + AVAILABILITY_UPDATE per opzione | GET /products/{productId} poi GET /availability | Prima il catalogo (nuovi startTimes), poi le partenze nell'intervallo indicato. |
| Risorsa: quantity o capacityPerResource cambiati | AVAILABILITY_UPDATE { productId, optionId, localDateStart, localDateEnd } per ogni opzione che usa la risorsa | GET /availability?optionId=&from=&to= | Sostituisci vacancies e available delle partenze nell'intervallo. Una barca condivisa da due opzioni produce due notifiche. |
| Risorsa agganciata o sganciata da un'opzione (resource-requirements) | PRODUCT_UPDATE + AVAILABILITY_UPDATE | GET /products/{productId} poi GET /availability | Cambiano resourceRequirements[] e la capacità vendibile. |
| Blocco manuale o inventario per data (availability-blocks, inventory) | AVAILABILITY_UPDATE per opzione | GET /availability?optionId=&from=&to= | Solo le partenze nell'intervallo indicato. |
| Risorsa eliminata | Nessuna notifica dedicata | Il delete riesce solo quando nessuna opzione la usa: hai già ricevuto gli AVAILABILITY_UPDATE degli sganci. | |
| Risorsa rinominata o metadata | Nessuna notifica | Non è visibile in vendita. | |
| Prenotazione creata, confermata, modificata, cancellata, scaduta, rifiutata | BOOKING_UPDATE { id, uuid } | GET /bookings/{id} | status (ON_HOLD, PENDING, CONFIRMED, CANCELLED, REJECTED, EXPIRED), startAt, unitItems[], customer, pricingSnapshot. Le prenotazioni di altri canali arrivano allo stesso modo. |
La chiamata a GET /availability vuole from e to in UTC: localDateStart e localDateEnd sono date nel fuso del prodotto (product.timezone, altrimenti quello del fornitore). Converti con il fuso o allarga la finestra di un giorno per lato.
Ricevitore di riferimento
import express from "express";
const app = express();
app.use(express.json());
const seen = new Set(); // in produzione: tabella con chiave primaria = id della notifica
const api = (path) =>
fetch(process.env.TAKO_API_URL + path, {
headers: { Authorization: "Bearer " + process.env.TAKO_API_KEY },
}).then((r) => (r.status === 404 ? null : r.json()));
app.post("/webhooks/tako", (req, res) => {
if (req.get("authorization") !== "Bearer " + process.env.RECEIVER_SECRET) {
return res.sendStatus(401);
}
const n = req.body;
if (seen.has(n.id)) return res.sendStatus(200); // retry di Tako: già elaborata
seen.add(n.id);
res.sendStatus(202); // rispondi entro 5 s, poi elabora
setImmediate(() => handle(n).catch((e) => { seen.delete(n.id); console.error(e); }));
});
async function handle({ notificationType, data }) {
switch (notificationType) {
case "PRODUCT_UPDATE": {
const product = await api("/api/v1/products/" + data.productId);
if (!product || product.status === "INACTIVE") return catalog.deactivate(data.productId);
// options[] con units[], resourceRequirements[], availabilityPeriods[], pricingPeriods[]
return catalog.replace(product);
}
case "AVAILABILITY_UPDATE": {
// localDateStart/End sono date nel fuso del prodotto: allarga di un giorno per lato
const from = shiftDays(data.localDateStart, -1) + "T00:00:00Z";
const to = shiftDays(data.localDateEnd, 1) + "T23:59:59Z";
const slots = await api(
"/api/v1/availability?optionId=" + data.optionId + "&from=" + from + "&to=" + to,
);
return calendar.replace(data.optionId, from, to, slots);
}
case "BOOKING_UPDATE": {
const booking = await api("/api/v1/bookings/" + data.id);
return bookings.upsert(booking); // status, unitItems, pricingSnapshot, customer
}
}
}Quattro regole: verifica l'header che hai dichiarato in headers; deduplica sull'id; rispondi prima di elaborare; tratta ogni notifica come "rileggi", mai come dato. Se la rilettura fallisce, rimuovi l'id dai visti: al prossimo evento sulla stessa entità o alla riconciliazione ti riallinei.
Riconciliazione
I webhook non sono mai l'unico meccanismo: una consegna può andare persa per un tuo downtime superiore ai retry o per una sottoscrizione disabilitata. Pianifica tre letture periodiche.
| Cosa | Chiamata | Quando |
|---|---|---|
| Prenotazioni cambiate | GET /bookings?updatedSince=&limit=100 | Ogni pochi minuti, con l'ultimo updatedAt visto (sezione seguente). |
| Catalogo completo | GET /products?supplierId=&limit=&offset= e un GET /products/{id} per prodotto | Ogni notte. I prodotti non più elencati sono eliminati. |
| Disponibilità sull'orizzonte di vendita | GET /availability?optionId=&from=&to= | Ogni notte per ogni opzione attiva, a intervalli di un mese. |
| Stato delle sottoscrizioni | GET /webhooks/subscriptions | Con il catalogo: se status è DISABLED leggi disabledReason, correggi l'endpoint e riattiva con PATCH status ACTIVE, poi esegui una riconciliazione completa. |
Feed incrementale
curl "$TAKO_API_URL/api/v1/bookings?updatedSince=2030-06-01T00:00:00Z&limit=100" \
-H "Authorization: Bearer $TAKO_API_KEY"La risposta contiene items e nextCursor; continua inviando lo stesso updatedSince con il cursore fino a null. Il cursore è opaco e l’ordine è updatedAt/id. Salva l’updatedAt dell’ultimo elemento e riparti da lì al giro successivo.
Job e audit
Le scritture che richiedono propagazione generano eventi elaborati dal worker. Una risposta HTTP riuscita conferma l’operazione locale, non la ricezione da parte del canale.
| Endpoint | Uso e limite |
|---|---|
| GET /sync-jobs?status=FAILED | Ultimi 200 job, con attempts. Filtri status, provider e supplierId. |
| POST /sync-jobs/{id}/retry | Solo FAILED con evento di origine disponibile; consegna asincrona. |
| GET /audit-logs?entityType=Booking&entityId=… | Ultimi 200 eventi, dal più recente; non è un feed di sincronizzazione. |
| POST /webhook-events/{id}/retry | Rielabora eventi Viator in stato RECEIVED/FAILED. |
Stati job: PENDING, RUNNING, RETRYING, SUCCEEDED, FAILED. Risolvi la causa prima del retry manuale; chiamarlo più volte crea più eventi.