Intégrations

Webhooks sortants

Webhooks

Un webhook envoie un message à un autre système chaque fois que quelque chose se passe dans votre studio, par exemple une nouvelle réservation. La personne qui gère votre site peut s'en servir pour mettre à jour un tableur, publier dans un canal Slack ou tenir un autre outil à jour. Vous n'en avez besoin que si quelqu'un connecte un autre système à la main. Si vous utilisez Zapier, il crée lui-même ses webhooks. FullClass envoie chaque message en JSON signé à une adresse web de votre choix, pour que votre système n'ait pas à interroger l'API sans arrêt.

Configurez-les dans Paramètres → Webhooks. Seul le propriétaire peut ouvrir cette page. Ajoutez un point de terminaison (l'adresse qui reçoit les messages) en lui donnant un nom, une adresse https:// accessible depuis Internet, et les événements à lui envoyer. Vous pouvez ajouter jusqu'à 10 points de terminaison, et chacun peut recevoir un ensemble d'événements différent.

Un réglage de ce formulaire reste désactivé tant que vous ne l'activez pas. Inclure le code de la carte cadeau demande une vraie décision. Quand quelqu'un achète une carte cadeau ou un carnet de cours, l'événement payment.succeeded indique toujours que la vente a eu lieu, qui a acheté, qui détient la carte et pour quel montant. Le code de la carte cadeau, c'est différent, car toute personne qui l'a peut utiliser la carte au paiement, comme l'acheteur. Ne l'activez que pour un point de terminaison à qui vous faites confiance pour le protéger, et seulement si votre système a vraiment besoin du code, par exemple pour l'envoyer vous-même par e-mail. Le désactiver arrête l'envoi tout de suite.

Événement Envoyé quand
booking.created Une réservation est confirmée. Une réservation non payée qui n'est jamais terminée n'en envoie pas. Une formation en envoie un par séance, et toutes les séances d'un même achat ont le même course_purchase_id.
booking.updated Une réservation est déplacée vers un autre cours ou modifiée. Le message indique ce qui a changé.
booking.canceled Une réservation est annulée, par vous ou par le client.
event.created Un cours est ajouté à votre planning. Une série récurrente en envoie un par date, et une formation un par séance.
event.updated Vous changez l'horaire, le lieu, l'intervenant, le prix ou la taille d'un cours. Le message indique ce qui a changé. Un changement d'horaire contient aussi l'ancien horaire.
event.canceled Vous annulez un cours entier. Le message indique combien de réservations ont été annulées, pour que vous le distinguiez de clients qui annulent un par un.
customer.created Un compte client existe pour la première fois, qu'il ait réservé, qu'il se soit inscrit lui-même ou que vous l'ayez ajouté.
payment.succeeded Un paiement est encaissé, y compris les paiements que vous enregistrez à la main. Une vente de carte cadeau ou de carnet de cours indique aussi la carte, son type et la personne qui la détient. Vous pouvez ainsi envoyer un e-mail de bienvenue ou de récompense sans rien chercher.
payment.refunded De l'argent est rendu à un client. Cela couvre une annulation avec remboursement, un remboursement que vous faites dans Stripe ou Square, et une rétrofacturation que vous perdez.
waitlist.offered Une place libre dans un cours ou une formation est proposée à la personne suivante sur sa liste d'attente.

Les messages de réservation et de paiement contiennent le nom et l'adresse e-mail du client. Vous pouvez ainsi écrire à quelqu'un ou l'ajouter à une liste sans le chercher d'abord. Si la fiche du client a été supprimée depuis, ces informations sont omises, mais le message est quand même envoyé.

Quand vous ajoutez un point de terminaison, FullClass affiche une seule fois sa clé de signature. Chaque message est signé avec elle, pour que votre système puisse vérifier que le message vient bien de FullClass. Copiez-la directement dans les réglages de votre point de terminaison. Si vous la perdez, utilisez Renouveler la clé sur la page du point de terminaison pour en obtenir une nouvelle. L'ancienne clé cesse de fonctionner dès que la nouvelle apparaît, alors mettez votre point de terminaison à jour tout de suite.

Vérifier la signature

Cette partie est destinée à la personne qui gère votre site. Elle montre comment son code vérifie qu'un message vient de FullClass. La clé de signature n'est pas un mot de passe, et FullClass ne l'envoie jamais. Ne la cherchez pas dans la requête et ne la comparez à rien. Chaque message porte plutôt un en-tête FullClass-Signature. Votre code le recalcule à partir de la clé et du corps du message :

FullClass-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t est le moment de l'envoi, en secondes Unix. v1 est un HMAC-SHA256, écrit en hexadécimal minuscule.
  • Le HMAC est calculé sur l'horodatage, un point, et le corps brut de la requête, comme dans <t>.<body>.
  • La clé est la clé entière exactement telle qu'elle est affichée, préfixe whsec_ compris, utilisée comme texte brut. Ne la décodez pas en base64.
  • Comparez votre résultat à v1 avec une comparaison à temps constant. Rejetez le message si t s'écarte de plus de 5 minutes de votre propre horloge, dans un sens ou dans l'autre.

En Node.js avec 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);
}

Montez cette route avant tout app.use(express.json()). Un analyseur JSON qui s'exécute en premier remplace les octets bruts par un objet analysé, et la signature ne peut alors jamais correspondre. Traitez un en-tête absent comme une vérification échouée. N'écrivez jamais la clé ni l'en-tête dans vos journaux.

En Python avec 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)
Vérifiez le corps brut, avant de l'analyser. Si votre framework analyse le JSON et que vous le retransformez en texte, les espaces et l'ordre des clés changent. La signature ne correspondra alors jamais. Lisez d'abord le corps en octets, vérifiez-le, puis analysez-le. Pour la même raison, ne sautez pas la vérification de l'horodatage. Elle empêche quelqu'un qui a intercepté un ancien message de vous le renvoyer.

Chaque message contient aussi FullClass-Event-Id, FullClass-Event-Type et FullClass-Delivery-Id. Une nouvelle tentative est signée de nouveau avec un nouvel horodatage, donc sa signature diffère de celle du premier essai. Utilisez FullClass-Event-Id pour repérer un événement que vous avez déjà traité. Les points de terminaison qu'une application comme Zapier ajoute pour vous sont signés eux aussi, mais leur clé n'est jamais affichée. Vous ne pouvez donc vérifier la signature que sur les points de terminaison que vous ajoutez vous-même.

La page de chaque point de terminaison montre ses envois récents. Vous voyez l'événement, le moment de l'envoi, ce que votre serveur a répondu et le nombre d'essais. Un envoi en échec est retenté tout seul, avec des intervalles de plus en plus longs. Vous pouvez aussi ouvrir le message d'un envoi, ou le renvoyer une fois votre point de terminaison réparé. Un point de terminaison qui échoue sans cesse est mis en pause tout seul, pour qu'un serveur hors service n'accumule pas de retard. Réparez le côté qui reçoit, puis modifiez le point de terminaison et réactivez-le.

Vous ne recevez pas les événements ? Vérifiez l'état du point de terminaison dans Paramètres → Webhooks. S'il apparaît en pause après des échecs répétés, votre serveur était injoignable ou renvoyait des erreurs. Une fois qu'il fonctionne de nouveau, réactivez le point de terminaison avec Modifier. Son compteur d'échecs repart alors de zéro. Les événements survenus pendant la pause ne sont pas envoyés et ne figurent pas dans sa liste d'envois, donc il n'y a rien à renvoyer pour eux. « Renvoyer » fonctionne toujours sur les envois en échec d'avant la pause.