Mit Webhooks lässt du dein eigenes System automatisch auf Ereignisse in DigitalMembers reagieren — ohne ständig nachzufragen. Sobald sich etwas ändert, schicken wir dir eine signierte Nachricht an eine von dir hinterlegte Adresse. Du prüfst sie, bestätigst den Empfang und lädst bei Bedarf die Details über die API nach.
So funktioniert es
Tritt in deiner Organisation ein Ereignis ein, senden wir eine POST-Anfrage mit einem kompakten Datensatz an deine hinterlegte Adresse. Du prüfst die Signatur, antwortest mit einem Status im 2xx-Bereich und verarbeitest die Nachricht. Antwortest du nicht mit 2xx, wiederholen wir die Zustellung automatisch mit wachsenden Abständen — du musst nichts von Hand nachtriggern.
Webhook einrichten
Webhooks verwaltest du unter Organisation → Einstellungen → Webhooks. Pro Webhook hinterlegst du eine HTTPS-Adresse und wählst die Ereignisse aus, die du abonnieren möchtest.
Die Absicherung — Signatur und optional OAuth2 — konfigurierst du einmalig pro Organisation im Bereich „Authentifizierung". Sie gilt für alle deine Webhooks. Aktiviere mindestens eine Absicherung.
Verfügbare Ereignisse
Aktuell stehen diese Ereignisse zur Verfügung:
member.changed— etwas rund um ein Mitglied hat sich geändert. Das Ereignis wird ausgelöst, wenn ein Mitglied erstellt, geändert oder gelöscht wird, aber auch, wenn sich ein Feld oder eine Feldzuweisung ändert. Welcher Objekttyp betroffen ist, steht im Felddata.model(members.member,members.fieldodermembers.fieldassignment); die konkrete Aktion im Felddata.operation(created,updated,deleted).registration.changed— eine Anmeldung hat sich geändert. Das Ereignis wird ausgelöst, wenn über ein öffentliches Formular eine neue Anmeldung eingeht, wenn ein Mitglied eine Profiländerung einreicht oder anpasst, oder wenn eine Anmeldung angenommen oder abgelehnt wird. Im Felddata.modelstehtregistration.registration, die konkrete Aktion im Felddata.operation(created,updated). Überdata.idslädst du die betroffene Anmeldung bei Bedarf über die API nach.
Aufbau der Nachricht
Jede Zustellung hat denselben Aufbau: einen Umschlag mit Metadaten und ein data-Objekt, das die betroffenen Objekte beschreibt.
{
"id": "1efdff58-1111-1111-1111-111111111111",
"event": "member.changed",
"version": "v2",
"created_at": "2026-07-05T10:15:00+00:00",
"org_id": "1efdff58-2222-2222-2222-222222222222",
"data": {
"ids": ["1efdff58-0000-0000-0000-000000000000"],
"model": "members.member",
"service": "members",
"operation": "updated"
}
}
id— eindeutige Kennung der Nachricht. Bleibt bei Wiederholungen gleich; verarbeite jedeidnur einmal.event— der Ereignistyp.version— Version des Nachrichten-Formats. Bei einer grundlegenden Änderung erhöhen wir sie.created_at— Zeitpunkt des Ereignisses.org_id— die Organisation, zu der das Ereignis gehört.data.ids— die Kennungen der betroffenen Objekte als Liste. Damit lädst du die Details über die API nach.data.model/data.service— Objekttyp und zuständiger Bereich.data.modelsagt dir, worauf sichdata.idsbeziehen:members.member(Mitglieder),members.field(Felder),members.fieldassignment(Feldzuweisungen) oderregistration.registration(Anmeldungen). Lade je nach Typ das passende Objekt über die API nach.data.operation— die Aktion:created,updatedoderdeleted. Beideletedist das Objekt über die API nicht mehr abrufbar.
Warum die Nachricht keine Personendaten enthält
Die Nachricht sagt dir bewusst nur, was sich geändert hat und wo — niemals die Inhalte selbst. Namen, Adressen oder andere Personendaten sind nie enthalten. Das hat mehrere Gründe:
- Die Absicherung ist optional. Ein Webhook kann auch ohne OAuth2 betrieben werden. Damit selbst dann nichts Sensibles nach aussen gelangt — etwa wenn eine Adresse versehentlich falsch konfiguriert ist — enthält die Nachricht grundsätzlich keine Personendaten.
- Du behältst die Kontrolle über deine Daten. Die eigentlichen Details holst du gezielt über die API ab, mit deinem eigenen Zugang. Ein kompromittierter Empfänger verrät so keine Mitgliederdaten.
- Die Reihenfolge ist nicht garantiert und eine Nachricht kann in seltenen Fällen doppelt eintreffen. Behandle die Nachricht darum als Auslöser, nicht als endgültige Wahrheit: lade bei Bedarf den aktuellen Stand über die API nach, statt dich allein auf den Inhalt der Nachricht zu verlassen.
Signatur prüfen
Bei aktivierter Signatur trägt jede Zustellung zwei Kopfzeilen. Damit weist du nach, dass die Nachricht von DigitalMembers stammt und unterwegs nicht verändert wurde — auch wenn dein Endpoint sonst keine eigene Authentifizierung hat.
X-Webhook-Signature: 5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd X-Webhook-Timestamp: 1720087200
- Die Signatur ist ein HMAC-SHA256 über die Zeichenkette
{timestamp}{body}mit deinem Signaturschlüssel (in den Webhook-Einstellungen über „Signaturschlüssel kopieren" abrufbar). - Berechne den HMAC über
{X-Webhook-Timestamp}{empfangener Body}und vergleiche ihn zeitkonstant mitX-Webhook-Signature. - Verwirf Nachrichten, deren Zeitstempel älter als 5 Minuten ist (Schutz vor Wiederholungsangriffen).
- Prüfe immer gegen den rohen, unveränderten Body — verarbeite den Inhalt erst nach erfolgreicher Prüfung.
import hashlib
import hmac
import time
def signature_is_valid(secret, signature, timestamp, body, tolerance_seconds=300):
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Dein Signaturschlüssel ist wie ein Passwort. Bewahre ihn sicher auf, gib ihn nicht weiter und lege ihn nie in öffentlich zugänglichem Code ab. Rotiere ihn sofort, wenn der Verdacht besteht, dass er nach aussen gelangt ist — das geht jederzeit in den Webhook-Einstellungen. Für Schäden aus einem geleakten oder unsorgfältig aufbewahrten Signaturschlüssel übernimmt DigitalMembers keine Verantwortung; die sichere Aufbewahrung liegt allein in deiner Verantwortung.
OAuth2 (optional)
Zusätzlich oder alternativ zur Signatur kann jede Zustellung ein OAuth2-Token tragen. Hinterlege dazu Token-Adresse, Client-ID und Client-Secret deines eigenen Identity-Providers. Vor jeder Zustellung holt DigitalMembers per Client-Credentials-Grant ein Token bei deiner Token-Adresse ab und sendet es als Authorization: Bearer <token> mit. Da das Token von deinem eigenen Provider ausgestellt wird, prüfst du es an deinem Endpoint gegen genau diesen Provider.
Beispiel: Webhook-Empfänger mit FastAPI
Ein vollständiger Empfänger liest den rohen Body, prüft die Signatur und verarbeitet die Nachricht erst nach bestandener Prüfung.
import hashlib
import hmac
import json
import time
from fastapi import FastAPI, Header, HTTPException, Request
WEBHOOK_SECRET = "whsec_dein_signaturschluessel"
SIGNATURE_TOLERANCE_SECONDS = 300 # 5 Minuten
app = FastAPI()
def signature_is_valid(signature, timestamp, body):
if not signature or not timestamp or not timestamp.isdigit():
return False
if abs(time.time() - int(timestamp)) > SIGNATURE_TOLERANCE_SECONDS:
return False
expected = hmac.new(WEBHOOK_SECRET.encode(), timestamp.encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/webhooks/digitalmembers")
async def receive_webhook(request: Request,
x_webhook_signature: str | None = Header(default=None),
x_webhook_timestamp: str | None = Header(default=None)):
body = await request.body() # rohe Bytes lesen, bevor JSON geparst wird
if not signature_is_valid(x_webhook_signature, x_webhook_timestamp, body):
raise HTTPException(status_code=401, detail="invalid signature")
event = json.loads(body)
if event["event"] == "member.changed":
model = event["data"]["model"] # members.member | members.field | members.fieldassignment
object_ids = event["data"]["ids"] # Liste der betroffenen Kennungen
operation = event["data"]["operation"] # created | updated | deleted
# Details bei Bedarf über die API nachladen (mit deinem eigenen Zugang)
handle_member_change(model, object_ids, operation)
elif event["event"] == "registration.changed":
registration_ids = event["data"]["ids"] # betroffene Anmeldungen
operation = event["data"]["operation"] # created | updated
handle_registration_change(registration_ids, operation)
return {"received": True}
Hast du zusätzlich OAuth2 aktiviert, prüfst du den mitgesendeten Bearer-Token gegen deinen eigenen Identity-Provider und lehnst ungültige Token mit 401 ab.
import jwt # pip install "pyjwt[crypto]"
from jwt import PyJWKClient
# JWKS deines eigenen Identity-Providers (derselbe, dessen Token-Adresse du hinterlegt hast)
JWKS = PyJWKClient("https://dein-idp.example.com/.well-known/jwks.json")
def bearer_is_valid(authorization):
if not authorization or not authorization.lower().startswith("bearer "):
return False
token = authorization.split(" ", 1)[1]
try:
signing_key = JWKS.get_signing_key_from_jwt(token).key
jwt.decode(token, signing_key, algorithms=["RS256"]) # wirft bei ungültig oder abgelaufen
return True
except jwt.InvalidTokenError:
return False
Zustellung und Wiederholungen
Wir stellen jede Nachricht zuverlässig zu und wiederholen sie bei Problemen automatisch — du musst nichts von Hand nachtriggern.
- Erwartete Antwort: Dein Endpoint muss innerhalb von 10 Sekunden mit einem Status im 2xx-Bereich antworten. Jede andere Antwort — ein Fehlerstatus, eine Zeitüberschreitung oder eine nicht erreichbare Adresse — gilt als Fehlversuch und löst eine Wiederholung aus.
- Wiederholungen: Nach dem ersten Versuch wiederholen wir mit wachsenden Abständen: nach 5 Minuten, 15 Minuten, 30 Minuten, 2 Stunden, 6 Stunden und 24 Stunden. Das sind insgesamt 7 Zustellversuche. Antwortet dein Endpoint bei einem davon mit 2xx, gilt die Nachricht als zugestellt und wir hören auf. Kommt auch der letzte Versuch nicht durch, markieren wir die Nachricht als endgültig fehlgeschlagen.
- Mindestens einmal: Die Zustellung erfolgt „at least once" — eine Nachricht kann in seltenen Fällen doppelt eintreffen. Dedupliziere darum über die
idund verarbeite jede nur einmal (Idempotenz). - Reihenfolge: Die Zustellreihenfolge pro Webhook ist meist stabil, aber nicht garantiert. Verlasse dich nicht darauf; lade bei Bedarf den aktuellen Stand über die API nach.
- Von Hand erneut zustellen: Im Zustellprotokoll im Webhook-Detail siehst du zu jeder Nachricht den Antwort-Status und die Zahl der Versuche und kannst einzelne Zustellungen jederzeit erneut auslösen.
- Automatische Deaktivierung: Schlägt ein Webhook über längere Zeit dauerhaft fehl — rund 20 aufeinanderfolgende Fehlversuche über mindestens eine Woche ohne einen einzigen Erfolg — deaktivieren wir ihn automatisch, damit sich keine Nachrichten an eine tote Adresse stauen. Du siehst das am Webhook-Status und kannst ihn in den Einstellungen wieder aktivieren; dabei wird der Fehlerzähler zurückgesetzt.
Schutz vor Endlosschleifen: Zusätzlich gilt eine grosszügige Obergrenze an Zustellungen pro Stunde und Organisation. Löst dein System auf ein eingehendes Ereignis hin wieder eine Änderung bei uns aus, die ihrerseits ein neues Ereignis erzeugt, kann eine Schleife entstehen; wird die Obergrenze überschritten, stellen wir weitere Ereignisse in dieser Stunde nicht mehr zu — sie werden auch später nicht nachgeliefert. Gleiche den Datenstand in diesem Fall über die REST-API ab. Vermeide darum Rückschreibungen, die dasselbe Ereignis erneut auslösen.
Anforderungen an die Adresse
- Nur
https://mit einem öffentlich erreichbaren Hostnamen. - Antworte schnell mit 2xx und verarbeite aufwändige Logik im Hintergrund, damit du das Zeitlimit nicht reisst.
- Behandle deinen Signaturschlüssel wie ein Passwort und rotiere ihn bei Verdacht.
Testen
Du musst nicht auf ein echtes Ereignis warten: Sende in den Webhook-Einstellungen ein Test-Event an deinen Endpoint. Im Zustellprotokoll siehst du zu jeder Nachricht den Antwort-Status und kannst einzelne Zustellungen erneut auslösen. So prüfst du deine Signatur- und OAuth-Verifikation von Anfang bis Ende, bevor du live gehst.
Zum Ausprobieren ohne eigenes System kannst du einen Webhook auch direkt auf unseren Test-Empfänger zeigen lassen:
https://signatera.digitalmembers.ch/v2/webhooks/test/{deine-org-id}/
Der Test-Empfänger nimmt jede Zustellung an, prüft die Signatur mit deinem Signaturschlüssel und liefert das Ergebnis im Antwort-Body zurück (signature_present, signature_valid, timestamp_valid).