---
title: "Webhooks und Ereignisse"
description: "Ereignisse abonnieren, Signaturen prüfen, Wiederholungen sauber behandeln."
url: "https://maklerportal.apim.eu/leitfaeden/webhooks"
image: "https://maklerportal.apim.eu/_og/d/c_Ocean.takumi,title_Webhooks+und+Ereignisse,description_~RXJlaWduaXNzZSBhYm9ubmllcmVuLCBTaWduYXR1cmVuIHByw7xmZW4sIFdpZWRlcmhvbHVuZ2VuIHNhdWJlciBiZWhhbmRlbG4u,props_eyJ0aGVtZSI6eyJtb2RlIjoiZGFyayIsImNvbG9ycyI6eyJwcmltYXJ5IjoiI0Q0QTI0QyJ9fX0,p_Ii9sZWl0ZmFlZGVuL3dlYmhvb2tzIg,s_ov4oG5NBtbku0iaL.png"
---

## Webhooks und Ereignisse

Statt im Minutentakt zu fragen, ob sich etwas getan hat: Sie hinterlegen eine URL und bekommen Bescheid.

## [Endpunkt registrieren](#endpunkt-registrieren)

```bash
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.

## [Verfügbare Ereignisse](#verfügbare-ereignisse)

| 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           |

## [Aufbau einer Zustellung](#aufbau-einer-zustellung)

```json
{
  "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>                    |

## [Signatur prüfen](#signatur-prüfen)

Signiert wird `"<unixzeit>.<rohbody>"` per HMAC-SHA256 mit Ihrem `signatur_geheimnis`.

```python
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.

## [Wiederholungen und Idempotenz](#wiederholungen-und-idempotenz)

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.