Webhooks und Ereignisse
Statt im Minutentakt zu fragen, ob sich etwas getan hat: Sie hinterlegen eine URL und bekommen Bescheid.
Statt im Minutentakt zu fragen, ob sich etwas getan hat: Sie hinterlegen eine URL und bekommen Bescheid.
curl -X POST https://api.maklerportal.example/v1/webhooks \
-H "apikey: $MAKLERPORTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://crm.ihre-firma.de/hooks/maklerportal",
"ereignisse": ["expose.fertiggestellt", "anfrage.eingegangen", "termin.bestaetigt"],
"beschreibung": "CRM-Anbindung Produktion"
}'
Die Antwort enthält ein signatur_geheimnis. Es wird nur einmal ausgegeben und dient zur Prüfung eingehender Zustellungen.
| Ereignis | Ausgelöst durch |
|---|---|
objekt.angelegt | Ein neues Objekt wurde gespeichert |
objekt.aktualisiert | Stammdaten, Preis oder Medien haben sich geändert |
objekt.archiviert | Objekt wurde aus der Vermarktung genommen |
expose.fertiggestellt | Generierung abgeschlossen, PDF steht bereit |
expose.veroeffentlicht | Exposé ist auf mindestens einem Kanal live |
anfrage.eingegangen | Ein Interessent hat sich auf ein Objekt gemeldet |
anfrage.bewertet | Das Matching hat eine Anfrage eingestuft |
termin.bestaetigt | Ein Besichtigungstermin wurde zugesagt |
termin.abgesagt | Termin wurde von einer Seite abgesagt |
abrechnung.erstellt | Eine Provisionsabrechnung wurde erzeugt |
{
"id": "evt_0d41c8",
"typ": "expose.fertiggestellt",
"erzeugt_am": "2026-08-20T09:41:20Z",
"daten": {
"expose_id": "exp_4d9b2e",
"objekt_id": "obj_8f2c1a",
"status": "fertig",
"pdf_url": "https://api.maklerportal.example/v1/exposes/exp_4d9b2e/pdf"
}
}
Kopfzeilen jeder Zustellung:
| Header | Inhalt |
|---|---|
X-Maklerportal-Ereignis | Ereignistyp, z. B. expose.fertiggestellt |
X-Maklerportal-Zustellung | Eindeutige ID dieser Zustellung |
X-Maklerportal-Signatur | t=<unixzeit>,v1=<hex> |
Signiert wird "<unixzeit>.<rohbody>" per HMAC-SHA256 mit Ihrem signatur_geheimnis.
import hashlib, hmac, time
def signatur_gueltig(rohbody: bytes, header: str, geheimnis: str, toleranz: int = 300) -> bool:
teile = dict(p.split("=", 1) for p in header.split(","))
zeitstempel, signatur = teile["t"], teile["v1"]
if abs(time.time() - int(zeitstempel)) > toleranz:
return False # zu alt – schützt vor Wiedereinspielung
erwartet = hmac.new(
geheimnis.encode(),
f"{zeitstempel}.".encode() + rohbody,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(erwartet, signatur)
Prüfen Sie gegen den rohen Anfragekörper. Wer erst nach JSON parst und wieder serialisiert, verändert Whitespace und Schlüsselreihenfolge – die Signatur passt dann nicht mehr.
Wir erwarten innerhalb von fünf Sekunden einen 2xx-Status. Bleibt er aus, wiederholen wir mit wachsendem Abstand: nach 1 min, 5 min, 30 min, 2 h und 6 h. Danach gilt die Zustellung als gescheitert und der Endpunkt wird nach 24 Stunden ohne Erfolg automatisch pausiert.
Zustellungen können mehrfach ankommen. Führen Sie die verarbeiteten id-Werte mindestens sieben Tage lang mit und verwerfen Sie Wiederholungen. Antworten Sie sofort mit 202 und arbeiten Sie asynchron weiter – langsame Verarbeitung im Request-Handler ist der häufigste Grund für unnötige Wiederholungen.