Integrationen

Ausgehende Webhooks

Webhooks

Ein Webhook schickt jedes Mal eine Nachricht an ein anderes System, wenn in Ihrem Studio etwas passiert, zum Beispiel eine neue Buchung. Ihre Web-Betreuung kann damit eine Tabelle aktualisieren, in einem Slack-Kanal posten oder ein anderes Tool aktuell halten. Sie brauchen das nur, wenn jemand ein anderes System von Hand anbindet. Wenn Sie Zapier nutzen, legt Zapier seine eigenen Webhooks an. FullClass schickt jede Nachricht als signiertes JSON an eine Webadresse Ihrer Wahl. So muss Ihr System nicht ständig die API abfragen.

Richten Sie sie unter Einstellungen → Webhooks ein. Nur der Inhaber kann diese Seite öffnen. Fügen Sie einen Endpunkt hinzu (die Adresse, die die Nachrichten empfängt). Geben Sie ihm einen Namen, eine aus dem Internet erreichbare https://-Adresse und die Ereignisse, die er bekommen soll. Sie können bis zu 10 Endpunkte anlegen. Jeder kann andere Ereignisse bekommen.

Eine Einstellung in diesem Formular ist aus, bis Sie sie einschalten. Code des Geschenkgutscheins mitsenden braucht eine echte Entscheidung. Kauft jemand einen Geschenkgutschein oder ein Kursguthaben, meldet das Ereignis payment.succeeded immer, dass der Verkauf passiert ist, wer gekauft hat, wer den Gutschein besitzt und über welchen Betrag. Der Code des Geschenkgutscheins ist etwas anderes. Jeder, der ihn hat, kann den Gutschein an der Kasse einlösen, genau wie der Käufer. Schalten Sie die Einstellung nur für einen Endpunkt ein, dem Sie den Code sicher anvertrauen. Und nur, wenn Ihr System den Code wirklich braucht, zum Beispiel um ihn selbst per E-Mail weiterzuschicken. Schalten Sie sie wieder aus, endet das sofort.

Ereignis Wird gesendet, wenn
booking.created Eine Buchung bestätigt wird. Eine unbezahlte Buchung, die nie abgeschlossen wird, sendet keines. Eine Kursreihe sendet eines pro Termin. Jeder Termin aus demselben Kauf hat dieselbe course_purchase_id.
booking.updated Eine Buchung in einen anderen Kurs verschoben oder bearbeitet wird. Die Nachricht listet, was sich geändert hat.
booking.canceled Eine Buchung storniert wird, von Ihnen oder vom Kunden.
event.created Ein Kurs in Ihren Zeitplan kommt. Eine wiederkehrende Serie sendet eines pro Datum, eine Kursreihe eines pro Termin.
event.updated Sie Uhrzeit, Standort, Kursleitung, Preis oder Größe eines Kurses ändern. Die Nachricht listet, was sich geändert hat. Bei einer Zeitänderung ist auch die alte Uhrzeit dabei.
event.canceled Sie einen ganzen Kurs absagen. Die Nachricht sagt, wie viele Buchungen storniert wurden. So können Sie es von einzelnen Stornierungen durch Kunden unterscheiden.
customer.created Ein Kundenkonto zum ersten Mal entsteht, ob der Kunde gebucht hat, sich selbst registriert hat oder von Ihnen angelegt wurde.
payment.succeeded Eine Zahlung eingenommen wird, auch Zahlungen, die Sie von Hand erfassen. Beim Verkauf eines Geschenkgutscheins oder Kursguthabens werden auch der Gutschein, seine Art und die Person genannt, die ihn besitzt. So können Sie eine Willkommens- oder Prämien-E-Mail schicken, ohne etwas nachzuschlagen.
payment.refunded Geld an einen Kunden zurückgeht. Das umfasst eine Stornierung mit Rückerstattung, eine Rückerstattung in Stripe oder Square und eine verlorene Zahlungsanfechtung.
waitlist.offered Ein freier Platz in einem Kurs oder einer Kursreihe der nächsten Person auf der Warteliste angeboten wird.

Buchungs- und Zahlungsnachrichten enthalten Name und E-Mail-Adresse des Kunden. So können Sie jemandem schreiben oder ihn zu einer Liste hinzufügen, ohne ihn vorher nachzuschlagen. Wurde der Datensatz des Kunden inzwischen gelöscht, fehlen diese Angaben. Die Nachricht wird aber trotzdem gesendet.

Wenn Sie einen Endpunkt hinzufügen, zeigt FullClass sein Signaturgeheimnis einmal an. Jede Nachricht wird damit signiert. So kann Ihr System prüfen, ob die Nachricht wirklich von FullClass kommt. Kopieren Sie es direkt in die Einstellungen Ihres Endpunkts. Wenn Sie es verlieren, holen Sie sich mit Secret erneuern auf der Seite des Endpunkts ein neues. Das alte Geheimnis funktioniert nicht mehr, sobald das neue erscheint. Aktualisieren Sie Ihren Endpunkt also sofort.

Die Signatur prüfen

Dieser Teil ist für Ihre Web-Betreuung. Er zeigt, wie deren Code prüft, ob eine Nachricht von FullClass kommt. Das Signaturgeheimnis ist kein Passwort, und FullClass schickt es nie mit. Suchen Sie es nicht in der Anfrage und vergleichen Sie es mit nichts. Stattdessen hat jede Nachricht einen Header FullClass-Signature. Ihr Code berechnet ihn aus dem Geheimnis und dem Nachrichtentext neu:

FullClass-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t ist der Sendezeitpunkt der Nachricht in Unix-Sekunden. v1 ist ein HMAC-SHA256, geschrieben als Hexadezimalzahl in Kleinbuchstaben.
  • Der HMAC wird über den Zeitstempel, einen Punkt und den unveränderten Request-Body berechnet, also <t>.<body>.
  • Der Schlüssel ist das ganze Geheimnis genau wie angezeigt, mit dem Präfix whsec_, als reiner Text. Dekodieren Sie es nicht mit Base64.
  • Vergleichen Sie Ihr Ergebnis mit v1 über einen zeitkonstanten Vergleich. Weisen Sie die Nachricht ab, wenn t mehr als 5 Minuten von Ihrer eigenen Uhr abweicht, in beide Richtungen.

In Node.js mit 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);
}

Binden Sie diese Route vor jedem app.use(express.json()) ein. Ein JSON-Body-Parser, der zuerst läuft, ersetzt die Rohbytes durch ein geparstes Objekt. Dann kann die Signatur nie stimmen. Behandeln Sie einen fehlenden Header als fehlgeschlagene Prüfung. Schreiben Sie das Geheimnis oder den Header nie in Ihre Logs.

In Python mit 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)
Prüfen Sie den Roh-Body, bevor Sie ihn parsen. Parst Ihr Framework das JSON und Sie wandeln es zurück in Text, ändern sich Leerzeichen und Reihenfolge der Schlüssel. Dann stimmt die Signatur nie. Lesen Sie den Body zuerst als Bytes, prüfen Sie ihn und parsen Sie ihn dann. Lassen Sie aus demselben Grund die Prüfung des Zeitstempels nicht weg. Sie verhindert, dass jemand eine abgefangene alte Nachricht erneut an Sie schickt.

Jede Nachricht hat außerdem FullClass-Event-Id, FullClass-Event-Type und FullClass-Delivery-Id. Ein neuer Versuch wird mit einem neuen Zeitstempel erneut signiert. Seine Signatur unterscheidet sich also vom ersten Versuch. Nutzen Sie FullClass-Event-Id, um ein schon verarbeitetes Ereignis zu erkennen. Endpunkte, die eine App wie Zapier für Sie anlegt, werden auch signiert, aber ihr Geheimnis wird nie angezeigt. Die Signatur können Sie also nur bei Endpunkten prüfen, die Sie selbst anlegen.

Die Seite jedes Endpunkts zeigt seine letzten Zustellungen. Sie sehen das Ereignis, wann es gesendet wurde, was Ihr Server geantwortet hat und wie viele Versuche es gab. Eine fehlgeschlagene Zustellung wird von selbst wiederholt, mit jedes Mal längeren Pausen. Sie können auch die Nachricht einer Zustellung öffnen oder sie erneut senden, sobald Ihr Endpunkt repariert ist. Ein Endpunkt, der immer wieder fehlschlägt, wird von selbst pausiert. So staut sich für einen toten Server nichts auf. Reparieren Sie die Empfängerseite, bearbeiten Sie dann den Endpunkt und schalten Sie ihn wieder ein.

Kommen keine Ereignisse an? Prüfen Sie den Status des Endpunkts unter Einstellungen → Webhooks. Ist er nach wiederholten Fehlern pausiert, war Ihr Server nicht erreichbar oder hat Fehler zurückgegeben. Funktioniert er wieder, schalten Sie den Endpunkt mit Bearbeiten wieder ein. Sein Fehlerzähler beginnt dann wieder bei null. Ereignisse aus der Zeit der Pause werden nicht gesendet und stehen nicht in seiner Zustellungsliste. Es gibt für sie also nichts erneut zu senden. „Erneut senden“ funktioniert weiter für die fehlgeschlagenen Zustellungen von vor der Pause.