Webhooks schicken dir Ereignisse aus PandaCards in Echtzeit zu – als HTTP-POST mit JSON-Payload an eine von dir hinterlegte URL. Kein Polling nötig.
| Event | Beschreibung |
|---|---|
| form.submission.created | Eine neue, nicht als Spam erkannte Formular-Einsendung (Lead) ist eingegangen. |
| booking.created | Eine PandaTime-Buchung wurde bestätigt. |
| booking.cancelled | Eine Buchung wurde storniert (per Kunden-Link oder im Dashboard). |
| booking.rescheduled | Start- oder Endzeit einer nicht stornierten Buchung wurde geändert. |
Webhooks sind ein Team-Plan-Feature und werden im Dashboard unter Entwicklung → Webhooks verwaltet.
Als Team-Manager legst du unter Entwicklung → Webhooks einen neuen Endpoint an: Ziel-URL plus die Events, die du abonnieren möchtest.
https sein und öffentlich erreichbar.
Interne/private Adressen (localhost, private IP-Bereiche, Link-Local etc.) werden abgelehnt – sowohl bei der Erstellung als auch erneut unmittelbar vor jeder Zustellung.
Beim Anlegen wird ein Signing-Secret erzeugt und einmalig im Klartext angezeigt (Format whsec_…). Es dient zur Signaturprüfung und wird danach nicht erneut angezeigt – sicher speichern.
whsec_5f3a9c2e1b8d47a6f0c9e2b4d6a8f1c3
| Event | Ausgelöst, wenn … |
|---|---|
| form.submission.created | … eine Einsendung eines PandaForms-Formulars eingeht und nicht als Spam erkannt wurde. Als Spam markierte Einsendungen lösen keinen Webhook aus. |
| booking.created | … eine Buchung über eine PandaTime-Buchungsseite den Status confirmed erreicht. |
| booking.cancelled | … eine Buchung storniert wird — sowohl über den Storno-Link des Kunden als auch, wenn du sie selbst im Dashboard stornierst. |
| booking.rescheduled | … sich Start- oder Endzeit einer nicht stornierten Buchung ändert (Verschiebung). Die Payload enthält zusätzlich die vorherige Start-/Endzeit. |
Ein Endpoint erhält nur die Events, die bei seiner Erstellung ausgewählt wurden. Andere Buchungs-Änderungen (z. B. wenn der Erinnerungs-Versand intern einen Zeitstempel setzt) lösen keinen Webhook aus.
Jede Zustellung ist ein JSON-Objekt mit event, created_at (ISO-8601) und dem eigentlichen data-Objekt.
{
"event": "form.submission.created",
"created_at": "2026-07-20T12:34:56+02:00",
"data": {
"id": 210,
"form": { "id": 5, "slug": "kontakt", "name": "Kontaktformular" },
"submission": {
"data": { "name": "Erika Beispiel", "email": "erika@example.de", "msg": "Bitte um Angebot." },
"country_code": "DE",
"consent_given": true,
"created_at": "2026-07-20T12:34:56+02:00"
}
}
}
{
"event": "booking.created",
"created_at": "2026-07-20T12:34:56+02:00",
"data": {
"id": 512,
"booking_page": { "id": 3, "slug": "beratung", "name": "Erstberatung" },
"event_type": "Erstgespräch",
"start_at": "2026-07-22T10:00:00+02:00",
"end_at": "2026-07-22T10:30:00+02:00",
"timezone": "Europe/Berlin",
"status": "confirmed",
"customer": { "name": "Erika Beispiel", "email": "erika@example.de", "phone": "+49 170 1234567" },
"answers": { "thema": "Angebot für Website" }
}
}
Gleiche Form wie booking.created, mit "status": "cancelled".
{
"event": "booking.cancelled",
"created_at": "2026-07-20T12:34:56+02:00",
"data": {
"id": 512,
"booking_page": { "id": 3, "slug": "beratung", "name": "Erstberatung" },
"event_type": "Erstgespräch",
"start_at": "2026-07-22T10:00:00+02:00",
"end_at": "2026-07-22T10:30:00+02:00",
"timezone": "Europe/Berlin",
"status": "cancelled",
"customer": { "name": "Erika Beispiel", "email": "erika@example.de", "phone": "+49 170 1234567" },
"answers": { "thema": "Angebot für Website" }
}
}
Gleiche Form wie booking.created, plus previous_start_at/previous_end_at mit der vorherigen Zeit.
{
"event": "booking.rescheduled",
"created_at": "2026-07-20T12:34:56+02:00",
"data": {
"id": 512,
"booking_page": { "id": 3, "slug": "beratung", "name": "Erstberatung" },
"event_type": "Erstgespräch",
"start_at": "2026-07-23T14:00:00+02:00",
"end_at": "2026-07-23T14:30:00+02:00",
"timezone": "Europe/Berlin",
"status": "confirmed",
"customer": { "name": "Erika Beispiel", "email": "erika@example.de", "phone": "+49 170 1234567" },
"answers": { "thema": "Angebot für Website" },
"previous_start_at": "2026-07-22T10:00:00+02:00",
"previous_end_at": "2026-07-22T10:30:00+02:00"
}
}
Jede Zustellung trägt zusätzlich folgende HTTP-Header:
| Header | Bedeutung |
|---|---|
X-PandaCards-Event | Event-Name, z. B. booking.created |
X-PandaCards-Delivery | Eindeutige ID dieses Zustellversuchs |
X-PandaCards-Timestamp | Unix-Zeitstempel (Sekunden) der Anfrage |
X-PandaCards-Signature | sha256= + Hex-HMAC-SHA256 – siehe Signaturprüfung |
Die Signatur ist HMAC-SHA256(timestamp + "." + rawBody, secret), hex-kodiert und mit sha256= vorangestellt. Prüfe drei Dinge:
timingSafeEqual / hmac.compare_digest), nie mit ==.const crypto = require("crypto");
function verify(rawBody, headers, secret) {
const ts = headers["x-pandacards-timestamp"];
const sig = headers["x-pandacards-signature"];
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // Replay-Schutz (5 Min)
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
import hmac, hashlib, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
ts = headers["X-PandaCards-Timestamp"]
sig = headers["X-PandaCards-Signature"]
expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
if abs(time.time() - int(ts)) > 300: # Replay-Schutz (5 Min)
return False
return hmac.compare_digest(sig, expected)
Ein HTTP-Status 2xx gilt als erfolgreiche Zustellung. Antwortet dein Endpoint mit einem anderen Status oder ist er nicht erreichbar, wird die Zustellung mit Backoff wiederholt:
| Versuch | Verzögerung ab vorherigem Versuch |
|---|---|
| 1 | sofort |
| 2 | + 1 Minute |
| 3 | + 5 Minuten |
| 4 | + 30 Minuten |
| 5 | + 2 Stunden |
Nach dem 5. erfolglosen Versuch wird die Zustellung als failed markiert und nicht weiter wiederholt.
failed), wird automatisch deaktiviert. Du kannst ihn danach im Dashboard wieder aktivieren, sobald das Ziel wieder erreichbar ist.
https sein; interne/private/Link-Local-Adressen werden abgelehnt – auch erneut direkt vor jeder Zustellung geprüft.X-PandaCards-Signature und X-PandaCards-Timestamp – siehe Signaturprüfung.