Integrazioni

Webhook in uscita

Webhooks

Un webhook invia un messaggio a un altro sistema ogni volta che succede qualcosa nel tuo studio, per esempio una nuova prenotazione. Chi gestisce il tuo sito può usarlo per aggiornare un foglio di calcolo, scrivere in un canale Slack o tenere aggiornato un altro strumento. Ti serve solo se qualcuno sta collegando a mano un altro sistema. Se usi Zapier, crea da solo i suoi webhook. FullClass invia ogni messaggio come JSON firmato a un indirizzo web che scegli tu, così il tuo sistema non deve continuare a interrogare l'API.

Li imposti in Impostazioni → Webhooks. Solo il titolare può aprire questa pagina. Aggiungi un endpoint (l'indirizzo che riceve i messaggi) indicando un nome, un indirizzo https:// raggiungibile da Internet e gli eventi da inviargli. Puoi aggiungere fino a 10 endpoint, e ognuno può ricevere un insieme di eventi diverso.

Un'impostazione di quel modulo è disattivata finché non la attivi. Includi il codice della gift card richiede una decisione vera. Quando qualcuno compra una carta regalo o un carnet, l'evento payment.succeeded dice sempre che la vendita è avvenuta, chi ha comprato, chi possiede la carta e per quale importo. Il codice della carta regalo è diverso, perché chiunque lo abbia può spendere la carta al pagamento, proprio come l'acquirente. Attivalo solo per un endpoint di cui ti fidi per tenerlo al sicuro, e solo se il tuo sistema ha davvero bisogno del codice, per esempio per inoltrarlo tu via e-mail. Se lo disattivi di nuovo, l'invio si ferma subito.

Evento Inviato quando
booking.created Una prenotazione viene confermata. Una prenotazione non pagata che non viene mai completata non lo invia. Un corso ne invia uno per sessione, e tutte le sessioni dello stesso acquisto hanno lo stesso course_purchase_id.
booking.updated Una prenotazione viene spostata su un'altra lezione o modificata. Il messaggio elenca cosa è cambiato.
booking.canceled Una prenotazione viene cancellata, da te o dal cliente.
event.created Una lezione viene aggiunta al tuo calendario. Una serie ricorrente ne invia uno per data, e un corso uno per sessione.
event.updated Cambi l'orario, la sede, l'istruttore, il prezzo o la capienza di una lezione. Il messaggio elenca cosa è cambiato. Un cambio di orario include anche l'orario precedente.
event.canceled Cancelli un'intera lezione. Il messaggio dice quante prenotazioni sono state cancellate, così puoi distinguerlo dai clienti che cancellano uno alla volta.
customer.created Un account cliente esiste per la prima volta, che abbia prenotato, si sia registrato da solo o che l'abbia aggiunto tu.
payment.succeeded Viene incassato un pagamento, compresi quelli che registri a mano. La vendita di una carta regalo o di un carnet indica anche la carta, il suo tipo e la persona che la possiede. Così puoi inviare un'e-mail di benvenuto o di premio senza cercare nulla.
payment.refunded Dei soldi tornano a un cliente. Vale per una cancellazione con rimborso, un rimborso che fai in Stripe o Square, e una contestazione (chargeback) che perdi.
waitlist.offered Un posto libero in una lezione o in un corso viene offerto alla persona successiva nella sua lista d'attesa.

I messaggi di prenotazione e di pagamento includono il nome e l'indirizzo e-mail del cliente. Così puoi scrivere a qualcuno o aggiungerlo a una lista senza doverlo cercare prima. Se nel frattempo la scheda del cliente è stata eliminata, questi dati vengono omessi, ma il messaggio viene inviato lo stesso.

Quando aggiungi un endpoint, FullClass mostra una sola volta la sua chiave di firma. Ogni messaggio viene firmato con questa chiave, così il tuo sistema può controllare che arrivi davvero da FullClass. Copiala subito nelle impostazioni del tuo endpoint. Se la perdi, usa Rinnova la chiave nella pagina dell'endpoint per averne una nuova. La vecchia chiave smette di funzionare appena compare la nuova, quindi aggiorna subito il tuo endpoint.

Verificare la firma

Questa parte è per chi gestisce il tuo sito. Mostra come il suo codice controlla che un messaggio arrivi da FullClass. La chiave di firma non è una password, e FullClass non la invia mai. Non cercarla nella richiesta e non confrontarla con nulla. Ogni messaggio ha invece un'intestazione FullClass-Signature. Il tuo codice la ricalcola a partire dalla chiave e dal corpo del messaggio:

FullClass-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t è il momento in cui il messaggio è stato inviato, in secondi Unix. v1 è un HMAC-SHA256, scritto in esadecimale minuscolo.
  • L'HMAC è calcolato sul timestamp, un punto e il corpo grezzo della richiesta, come in <t>.<body>.
  • La chiave è l'intera chiave esattamente come viene mostrata, prefisso whsec_ incluso, usata come testo semplice. Non decodificarla da base64.
  • Confronta il tuo risultato con v1 usando un confronto a tempo costante. Rifiuta il messaggio se t si discosta di più di 5 minuti dal tuo orologio, in un senso o nell'altro.

In Node.js con Express:

const crypto = require("crypto");
const express = require("express");

const secret = process.env.FULLCLASS_WEBHOOK_SECRET; // "whsec_..."
const app = express();

// express.raw keeps the exact bytes FullClass signed.
app.post("/fullclass", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.get("FullClass-Signature") || "", req.body)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  // ... handle event ...
  res.sendStatus(200);
});

function verify(header, rawBody) {
  if (!Buffer.isBuffer(rawBody)) return false; // a JSON parser ran first
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=", 2))
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + ".")
    .update(rawBody)
    .digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

Monta questa route prima di qualsiasi app.use(express.json()). Un parser JSON che parte prima sostituisce i byte grezzi con un oggetto già analizzato, e allora la firma non può mai corrispondere. Considera un'intestazione mancante come un controllo non riuscito. Non scrivere mai la chiave né l'intestazione nei tuoi log.

In Python con Flask:

import hashlib, hmac, os, time
from flask import Flask, abort, request

SECRET = os.environ["FULLCLASS_WEBHOOK_SECRET"]  # "whsec_..."
app = Flask(__name__)

@app.post("/fullclass")
def fullclass():
    raw = request.get_data()  # the exact bytes FullClass signed
    if not verify(request.headers.get("FullClass-Signature", ""), raw):
        abort(400)
    event = request.get_json()
    # ... handle event ...
    return "", 200

def verify(header: str, raw_body: bytes) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or not v1:
        return False
    if abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(
        SECRET.encode(), t.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, v1)
Controlla il corpo grezzo, prima di analizzarlo. Se il tuo framework analizza il JSON e tu lo riconverti in testo, gli spazi e l'ordine delle chiavi cambiano. A quel punto la firma non corrisponderà mai. Leggi prima il corpo come byte, controllalo, poi analizzalo. Per lo stesso motivo, non saltare il controllo del timestamp. Impedisce a chi ha intercettato un vecchio messaggio di rimandartelo.

Ogni messaggio ha anche FullClass-Event-Id, FullClass-Event-Type e FullClass-Delivery-Id. Un nuovo tentativo viene firmato di nuovo con un nuovo timestamp, quindi la sua firma è diversa da quella del primo invio. Usa FullClass-Event-Id per riconoscere un evento che hai già gestito. Anche gli endpoint che un'app come Zapier aggiunge per te sono firmati, ma la loro chiave non viene mai mostrata. Quindi puoi controllare la firma solo sugli endpoint che aggiungi tu.

La pagina di ogni endpoint mostra i suoi invii recenti. Vedi l'evento, quando è stato inviato, cosa ha risposto il tuo server e quanti tentativi sono stati fatti. Un invio non riuscito viene ritentato da solo, con attese ogni volta più lunghe. Puoi anche aprire il messaggio di un invio, o inviarlo di nuovo quando il tuo endpoint è sistemato. Un endpoint che continua a non riuscire viene messo in pausa da solo, così un server fermo non accumula arretrati. Sistema chi riceve, poi modifica l'endpoint e riattivalo.

Non ricevi gli eventi? Controlla lo stato dell'endpoint in Impostazioni → Webhooks. Se risulta in pausa dopo errori ripetuti, il tuo server non era raggiungibile o restituiva errori. Quando torna a funzionare, riattiva l'endpoint con Modifica. Il conteggio degli errori riparte allora da zero. Gli eventi del periodo di pausa non vengono inviati e non sono nel suo elenco di invii, quindi per loro non c'è nulla da inviare di nuovo. «Invia di nuovo» funziona ancora sugli invii non riusciti di prima della pausa.