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
v1usando un confronto a tempo costante. Rifiuta il messaggio setsi 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)
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.