- Home
- Entwickler
- Webhooks
Webhooks
Ereignisse der eigenen Teamdaten signiert an eine eigene URL
zugestellt — ohne zu pollen. Verwaltung über die Oberfläche
(Team → Schnittstellen → Webhooks) oder über die API
(Scope webhooks:manage, siehe Referenz).
Ereignisse
Ein Endpunkt abonniert eine Auswahl dieser Typen beim Anlegen:
| Typ | Auslöser |
|---|---|
training.completed | Trainingssitzung beendet |
evaluation.completed | Bewertung gespeichert |
assignment.created | Zuweisung angelegt |
assignment.completed | Zuweisung abgeschlossen |
member.joined | Mitgliedschaft begonnen |
member.removed | Mitgliedschaft beendet |
crm.sync.failed | CRM-Abgleich fehlgeschlagen |
ping | nur über „Testereignis senden“ — kein abonnierbarer Typ |
assignment.created/assignment.completed
feuern heute nur, wenn die Zuweisung über diese API selbst angelegt oder geändert
wird — eine Zuweisung, die ein Team-Admin über die Oberfläche anlegt, löst noch kein
Webhook-Ereignis aus.
Körper
Jedes Ereignis trägt dieselbe Hülle; data ist je Typ verschieden und
enthält Kennungen und eine Zusammenfassung. Punktzahl und user_id stehen
nur drin, wenn member_detail_enabled zum Zeitpunkt des Ereignisses an war
— mehr holt der Empfänger bei Bedarf über die API.
{
"id": "evt_7f3c1a2b",
"type": "evaluation.completed",
"created_at": "2026-10-14T08:42:10Z",
"api_version": "2026-10",
"tenant_id": "6a9c3e3e-9b8e-4b7b-9e9d-2b7b1a2c3d4e",
"data": {
"team_id": "3c2b1a0f-5678-4a3b-8c9d-0e1f2a3b4c5d",
"training_id": "7f3cfe1a-1111-4b22-9c33-abcdef012345",
"user_id": "1b9e2b3c-4d5e-6f70-8192-a3b4c5d6e7f8",
"score": 74
}
}Signatur prüfen
Jede Zustellung trägt den Kopf Sip-Signature: t=<Unix-Zeit>,v1=<hex>
und zur Duplikaterkennung Sip-Event-Id. v1 ist
HMAC-SHA256(geheimnis, "<t>.<roher Körper>") — das Geheimnis
wird nur bei POST /webhook-endpoints einmal angezeigt. Verwirf
Zeitstempel, die älter als fünf Minuten sind, und vergleiche zeitkonstant
(hmac.compare_digest bzw. das Äquivalent deiner Sprache) — nie mit
==.
import hashlib
import hmac
import time
def pruefen(geheimnis: str, kopf: str, koerper: bytes, *, max_alter_s: int = 300) -> bool:
teile = dict(t.split("=", 1) for t in kopf.split(","))
zeitstempel, empfangen = int(teile["t"]), teile["v1"]
if abs(time.time() - zeitstempel) > max_alter_s:
return False
nachricht = f"{zeitstempel}.".encode() + koerper
erwartet = hmac.new(geheimnis.encode(), nachricht, hashlib.sha256).hexdigest()
return hmac.compare_digest(erwartet, empfangen)
const crypto = require("crypto");
function pruefen(geheimnis, kopf, koerper, maxAlterS = 300) {
const teile = Object.fromEntries(kopf.split(",").map((t) => t.split("=")));
const zeitstempel = parseInt(teile.t, 10);
if (Math.abs(Date.now() / 1000 - zeitstempel) > maxAlterS) return false;
const nachricht = Buffer.concat([Buffer.from(`${zeitstempel}.`), koerper]);
const erwartet = Buffer.from(
crypto.createHmac("sha256", geheimnis).update(nachricht).digest("hex"));
const empfangen = Buffer.from(teile.v1 || "");
// timingSafeEqual wirft bei unterschiedlicher Laenge (RangeError) statt false zu liefern —
// eine falsch lange Signatur muss VORHER abgefangen werden.
if (empfangen.length !== erwartet.length) return false;
return crypto.timingSafeEqual(erwartet, empfangen);
}
<?php
function pruefen(string $geheimnis, string $kopf, string $koerper, int $maxAlterS = 300): bool {
parse_str(str_replace(",", "&", $kopf), $teile);
$zeitstempel = (int) $teile["t"];
if (abs(time() - $zeitstempel) > $maxAlterS) {
return false;
}
$erwartet = hash_hmac("sha256", "{$zeitstempel}." . $koerper, $geheimnis);
return hash_equals($erwartet, $teile["v1"]);
}
Zustellung und Wiederholung
Mindestens einmal zugestellt, Reihenfolge nicht garantiert; kein Folgen von
Weiterleitungen, Antwortkörper nach 1000 Zeichen abgeschnitten, 10 Sekunden
Zeitlimit. Jeder Fehlschlag (alles außer Status 2xx) wird wiederholt nach 1 min,
5 min, 30 min, 2 h, 6 h, 12 h, 24 h (sieben Versuche, rund
45 Stunden), danach gave_up. Blieb ein Endpunkt drei Tage lang ohne
eine einzige erfolgreiche Zustellung, pausiert ihn sip.coach
(disabled_failures) und mailt jeden Team-Administrator; ein Testereignis
startet ihn nicht automatisch wieder.
Das Ziel muss HTTPS sein und öffentlich erreichbar — eine private oder link-lokale Adresse wird beim Anlegen und vor jedem Versand abgelehnt.
Endpunkte verwalten
Anlegen, ändern, löschen, ein Testereignis senden und eine fehlgeschlagene
Zustellung sofort erneut versuchen — über die Oberfläche oder per API mit dem Scope
webhooks:manage. Jede dieser Aktionen protokolliert sip.coach, gleich auf
welchem Weg sie ausgelöst wurde.
# Testereignis anstoßen (Verwaltungsschlüssel mit Scope webhooks:manage):
curl -X POST https://app.sip.coach/api/public/v1/webhook-endpoints/<id>/test \
-H "Authorization: Bearer sipk_live_ab12cd34_…"
Jede Operation samt Parametern steht in der Referenz.