v0.9.1

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"
  }
}'
CampoValore
urlHTTPS pubblico. Niente credenziali nell'URL, niente host privati.
supplierIdLimita al fornitore. Omesso: tutti i fornitori del tenant (solo con chiave tenant).
notificationTypesPRODUCT_UPDATE, AVAILABILITY_UPDATE, BOOKING_UPDATE. Default: solo BOOKING_UPDATE.
headersHeader 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

POST <url>
{
  "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" }
}
CampoSignificato
idDeterministico: evento, sottoscrizione ed entità. Un retry ripete lo stesso id, deduplica su questo.
notificationTypeUno dei tre tipi sottoscritti.
utcCreatedAtIstante dell'evento in Tako, non della consegna.
dataSolo identificatori. PRODUCT_UPDATE: productId, optionId se noto. AVAILABILITY_UPDATE: productId, optionId, localDateStart, localDateEnd. BOOKING_UPDATE: id, uuid.
Rispondi 2xx entro 5 secondi, senza elaborare in linea. 429, 5xx, timeout ed errori di rete vengono ritentati dalla coda con backoff; ogni altro codice è permanente, incrementa 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 TakoNotifica e dataChiamataCosa aggiornare
Prodotto modificato (nome, descrizioni, durata, fuso, cutoff)PRODUCT_UPDATE { productId }GET /products/{productId}Anagrafica del prodotto.
Prodotto eliminato, oppure status INACTIVEPRODUCT_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 eliminataPRODUCT_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-periodsSostituisci tutti i periodi dell'opzione: gli id sono rigenerati a ogni PUT. Le prenotazioni già create conservano pricingSnapshot.
Orari ricorrenti (availability-periods) creati o rimossiPRODUCT_UPDATE + AVAILABILITY_UPDATE per opzioneGET /products/{productId} poi GET /availabilityPrima il catalogo (nuovi startTimes), poi le partenze nell'intervallo indicato.
Risorsa: quantity o capacityPerResource cambiatiAVAILABILITY_UPDATE { productId, optionId, localDateStart, localDateEnd } per ogni opzione che usa la risorsaGET /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_UPDATEGET /products/{productId} poi GET /availabilityCambiano resourceRequirements[] e la capacità vendibile.
Blocco manuale o inventario per data (availability-blocks, inventory)AVAILABILITY_UPDATE per opzioneGET /availability?optionId=&from=&to=Solo le partenze nell'intervallo indicato.
Risorsa eliminataNessuna notifica dedicataIl delete riesce solo quando nessuna opzione la usa: hai già ricevuto gli AVAILABILITY_UPDATE degli sganci.
Risorsa rinominata o metadataNessuna notificaNon è visibile in vendita.
Prenotazione creata, confermata, modificata, cancellata, scaduta, rifiutataBOOKING_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

Node.js + Express
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.

CosaChiamataQuando
Prenotazioni cambiateGET /bookings?updatedSince=&limit=100Ogni pochi minuti, con l'ultimo updatedAt visto (sezione seguente).
Catalogo completoGET /products?supplierId=&limit=&offset= e un GET /products/{id} per prodottoOgni notte. I prodotti non più elencati sono eliminati.
Disponibilità sull'orizzonte di venditaGET /availability?optionId=&from=&to=Ogni notte per ogni opzione attiva, a intervalli di un mese.
Stato delle sottoscrizioniGET /webhooks/subscriptionsCon 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.

EndpointUso e limite
GET /sync-jobs?status=FAILEDUltimi 200 job, con attempts. Filtri status, provider e supplierId.
POST /sync-jobs/{id}/retrySolo 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}/retryRielabora 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.