Webhooks
Mit Webhooks benachrichtigt Tourfold Ihre Systeme nahezu in Echtzeit, sobald etwas passiert —
eine Marke wird angelegt, eine Tour aktualisiert, ein Einsatz gelöscht. Anstatt die API
abzufragen, registrieren Sie einen HTTPS-Endpoint, an den Tourfold bei jedem Ereignis eine
signierte JSON-Payload per POST sendet.
Tourfold-Webhooks folgen der offenen Spezifikation
Standard Webhooks. Sie können sie daher in den meisten
Sprachen mit den offiziellen standardwebhooks-Bibliotheken verifizieren und verarbeiten — ein
Tourfold-spezifisches SDK ist nicht nötig.
Auf einen Blick
| Transport | HTTPS POST, Content-Type: application/json |
| Zustellung | At-least-once — dasselbe Ereignis kann mehrfach eintreffen |
| Reihenfolge | Nicht garantiert — ordnen Sie Ereignisse selbst über timestamp |
| Signierung | Standard Webhooks v1 HMAC-SHA256 (Header siehe unten) |
| Idempotenz-Schlüssel | Der Header webhook-id (über Wiederholungen hinweg stabil) |
| Erfolg | Jede HTTP-2xx-Antwort bestätigt den Empfang |
| Antwortfrist | Innerhalb von 5 Sekunden mit 2xx antworten; längere Arbeit asynchron einreihen |
| Ziel | Eine öffentliche HTTPS-URL; Weiterleitungen werden nicht verfolgt |
| Wiederholungen | Standardmäßig aktiv — 1 Erstversuch plus bis zu 6 Wiederholungen (insgesamt 7 Versuche; siehe Wiederholungen) |
1. Endpoint registrieren
Sie benötigen ein API-Bearer-Token und einen über öffentliches HTTPS erreichbaren Empfänger. Fragen Sie zuerst den aktuellen Ereigniskatalog Ihres Arbeitsbereichs ab, statt eine Liste aus diesem Leitfaden fest zu hinterlegen:
curl -fsS "https://api.tourfold.com/api/v2/webhooks/event-types" \
-H "Authorization: Bearer YOUR_TOKEN" \
| jq -r '.items[].type'
Legen Sie anschließend mit
createWebhookEndpoint
einen Endpoint an und wählen Sie exakte Ereignisse oder Abonnement-Muster:
curl -X POST "https://api.tourfold.com/api/v2/webhook-endpoints" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Alpine maintenance integration",
"endpoint_url": "https://maintenance.example.invalid/tourfold/events",
"signature_scheme": "HMAC_SHA256",
"enabled": true,
"retry": true,
"subscriptions": ["folder.*", "*.created"]
}'
Bei erfolgreicher Erstellung erhalten Sie HTTP 201, einen Location-Header für die neue
Ressource und den Endpoint:
{
"id": "00000000-0000-4000-8000-000000008001",
"display_name": "Alpine maintenance integration",
"endpoint_url": "https://maintenance.example.invalid/tourfold/events",
"signature_scheme": "HMAC_SHA256",
"enabled": true,
"retry": true,
"subscriptions": ["folder.*", "*.created"],
"invalid_subscriptions": [],
"created_at": "2026-08-20T08:40:00Z",
"updated_at": "2026-08-20T08:40:00Z",
"lock_version": 0,
"secret": "whsec_example_not_a_real_secret"
}
Das Signatur-Secret (Format whsec_…) wird nur einmal zurückgegeben: bei der Erstellung
und beim Rotieren. Speichern Sie es sofort in Ihrem Secret Manager. Beim
späteren Auflisten oder Abrufen des Endpoints wird es nicht erneut angezeigt.
Empfänger testen
Nachdem das Secret im Empfänger hinterlegt ist, lösen Sie einen signierten Verbindungstest aus:
curl -X POST "https://api.tourfold.com/api/v2/webhooks/send-test" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"endpoint_id":"00000000-0000-4000-8000-000000008001"}'
{
"last_test_tried_at": "2026-08-20T09:50:00Z",
"test_was_successful": true
}
Der Aufruf sendet test.webhook sofort und synchron. Er umgeht die Abonnements, funktioniert
auch bei deaktiviertem Endpoint und wird nie wiederholt. Ein fehlgeschlagener Test wird dennoch
unter den Zustellfehlern erfasst. Ein Test aktualisiert den
Endpoint-Zustand und erhöht dessen lock_version. Rufen Sie den Endpoint daher vor einer
versionsgeschützten Änderung erneut ab.
Anforderungen an das Ziel
In Produktion muss endpoint_url:
- eine absolute
https://-URL ohne eingebettete Zugangsdaten oder Fragment sein; - ausschließlich auf öffentliche IP-Adressen auflösen — Loopback-, private, Link-Local- und reservierte Ziele werden abgelehnt; und
- zum Zustellzeitpunkt weiterhin öffentlich auflösbar sein. Tourfold löst den Namen unmittelbar vor jedem Versuch erneut auf, um DNS-Rebinding zu verhindern.
Tourfold verfolgt keine Weiterleitungen. Für den Verbindungsaufbau stehen etwa 3 Sekunden und für die Antwort 5 Sekunden zur Verfügung. Lokale HTTP-Endpoints ohne TLS funktionieren nur, wenn eine Tourfold-Entwicklungsumgebung die unsichere lokale Zustellung ausdrücklich aktiviert.
Lebenszyklus des Endpoints verwalten
Ändern Sie einen Endpoint mit JSON Merge Patch. Lesen Sie ihn zuvor ab und senden Sie seine
lock_version mit, damit Sie keine gleichzeitige Änderung überschreiben:
curl -X PATCH \
"https://api.tourfold.com/api/v2/webhook-endpoints/00000000-0000-4000-8000-000000008001" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{
"subscriptions": ["folder.*", "*.created"],
"lock_version": 0
}'
- Nicht angegebene Felder bleiben unverändert. Ein angegebenes
subscriptions-Array ersetzt die gesamte Menge;[]leert sie. Ein ausdrücklichesnullist ungültig. - Jeder Endpoint akzeptiert bis zu 100 Abonnement-Muster.
- Eine veraltete
lock_versionliefert HTTP409. Rufen Sie den Endpoint erneut ab, führen Sie Ihre beabsichtigte Änderung zusammen und wiederholen Sie den Aufruf mit dem aktuellen Wert. - Pausieren und aktivieren Sie Zustellungen, indem Sie
enabledauffalseodertruesetzen. Ein Test ist auch während der Pause möglich. - Das Löschen eines Endpoints liefert HTTP
204und entfernt seine Konfiguration dauerhaft.
Verwalten Sie Endpoints über die Operationen unter Webhook Endpoints
(auflisten, Abonnements ändern, Secret rotieren, löschen), und prüfen Sie die Erreichbarkeit
jederzeit mit sendTestWebhook.
2. Ereignisnamen
Ein Ereignisname besteht aus einem Ressourcenpfad, gefolgt von einem Verb:
brand.created
folder.permissions.updated
custom_object.invoice.created
area.created
Zwei Regeln erklären jeden Namen, der Ihnen begegnen wird:
- Ein Punkt bedeutet Zugehörigkeit.
folder.permissionssind die Berechtigungen eines Ordners — etwas anderes als der Ordner selbst, mit eigenen Ereignissen.folder.updated(der Ordner wurde bearbeitet) undfolder.permissions.updated(seine Zugriffsliste hat sich geändert) sind daher unterschiedliche Ereignisse für unterschiedliche Empfänger. - Ein Unterstrich verbindet Wörter innerhalb eines Namensabschnitts.
stored_addressist ein einzelnes Substantiv, keinstored, das eineaddressenthält.
Das Verb ist immer der letzte Abschnitt. created, updated und deleted sind die Grundmenge;
Ressourcen können zusätzliche Zustandsübergänge anbieten, wenn diese tatsächlich in ihrem
gespeicherten Statusmodell vorkommen. updated bedeutet „eine Eigenschaft wurde bearbeitet“ — es
gibt nie an, welche Eigenschaft.
Ressourcen eines Plugins verwenden <plugin_key>.<resource>.<verb>. Das Präfix ist Teil von
resource_path und kein separates Feld; dadurch bleibt die Zugehörigkeit in Abonnements,
zugestellten Payloads und Logs sichtbar. Plugin-Ereignistypen sind laufzeitabhängige Fähigkeiten
eines Mandanten: Der Ereignistyp-Katalog liefert sie nur, wenn das jeweilige Plugin für Ihren
Mandanten aktiviert ist. Im statischen OpenAPI-Vertrag werden sie bewusst nicht aufgezählt. Die
verbindliche Liste der aktivierbaren Typen erhalten Sie über GET /api/v2/webhooks/event-types.
Zwei Konventionen, die Sie kennen sollten:
deletedbedeutet, dass der Datensatz endgültig weg ist. Das Verschieben in den Papierkorb und das Wiederherstellen sind Bearbeitungen und kommen deshalb alsupdatedan.- Benutzerdefinierte Objekte werden über ihren Slug benannt —
custom_object.invoice.created. Siehe Benutzerdefinierte Objekte dazu, was beim Umbenennen eines Slugs passiert.
Die aktuelle Liste für Ihren Workspace liefert
listWebhookEventTypes
— je Ereignis den type sowie resource_path, verb und bei benutzerdefinierten Objekten
definition_slug. Der Webhooks-Abschnitt der OpenAPI-Referenz
zeigt die mandantenunabhängige generische Menge mit vollständigen Payload-Schemata. Nur zur Laufzeit
verfügbare Plugin-Ereignistypen werden durch die Katalogantwort und das bereitstellende Plugin
dokumentiert.
3. Mit Mustern abonnieren
Ein Abonnement ist entweder ein exakter Ereignisname oder ein Muster. Über Muster abonnieren Sie breit, ohne jeden Namen einzeln aufzuführen:
| Muster | Trifft zu auf |
|---|---|
brand.created | genau dieses Ereignis |
folder.* | jedes Ordner-Verb und jeden Ordner-Aspekt, einschließlich folder.permissions.updated |
<plugin_key>.* | jede Ressource und jedes Verb eines aktivierten Plugins |
*.created | created bei jeder Ressource |
* | alles |
custom_object.invoice.* | jedes Ereignis des benutzerdefinierten Objekts invoice |
custom_object.*.created | created bei jedem benutzerdefinierten Objekt |
Die eine Asymmetrie, die Sie sich merken sollten: ein Wildcard-Verb weitet den Pfad, ein benanntes
Verb legt ihn fest. folder.* umfasst folder.permissions.updated, weil Sie den Ordner samt allem
darunter abonniert haben. folder.updated umfasst es nicht, denn dieser Name bezeichnet ein
einzelnes Ereignis, und Berechtigungen sind eine andere Ressource. Wenn Sie
Berechtigungsänderungen möchten, abonnieren Sie sie ausdrücklich.
Präfixe greifen immer auf ganze Abschnitte, folder.* trifft also nie eine Ressource namens
folder_archive.
Ebenso trifft <plugin_key>.* nie auf ein generisches Ereignis wie vehicle.created. *.created
und * sind dagegen bewusst global und umfassen generische Ereignisse sowie Ereignisse aktivierter
Plugins; der zugestellte type enthält weiterhin das konkrete Plugin-Präfix.
Überlappende Muster sind unproblematisch. Abonniert ein Endpoint sowohl folder.* als auch
folder.updated, erzeugt eine Ordnerbearbeitung dennoch genau eine Zustellung an ihn — die
Entdopplung erfolgt pro Endpoint, nicht pro zutreffendem Muster.
Ein Muster, das auf nichts zutreffen kann, wird beim Speichern abgelehnt (HTTP 422), statt
angenommen zu werden und dann nie etwas zuzustellen. Ein Abonnement, das ins Leere läuft, wäre der
verwirrendste Fehler, den diese API erzeugen könnte — deshalb wird er vorab verhindert.
4. Payload und Header
Jede Zustellung hat denselben Envelope. Der ereignisspezifische Teil liegt unter data.object:
{
"type": "brand.created",
"id": "1178a3d4-76c1-402d-bc51-0a411424eab2",
"event_id": "f820f8c7-3566-4c4d-a1a0-8ec2b288feab",
"timestamp": "2024-01-15T10:30:00Z",
"payload_version": 1,
"tenant_id": "34f5c98e-f430-457b-a812-92637d0c6fd0",
"actor": { "type": "USER", "id": "6b1e...c4a2" },
"request": { "id": "d7e56ac2-af7a-4e00-af77-9ffc5e02f3cb", "correlation_id": "02bde914-c402-4a49-95cd-e8a4944b85d3" },
"data": {
"object": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Alpine Facility Services GmbH",
"email": "contact@alpine-facility-services.example.invalid",
"phone_number": "+43123456789"
}
}
}
| Feld | Bedeutung |
|---|---|
type | Der Ereignisname. |
id | Die Id dieser Zustellung — derselbe Wert wie im Header webhook-id. |
event_id | Die Id des Ereignisses. Ein Ereignis an drei Endpoints ergibt drei ids und eine event_id. |
timestamp | Wann das Ereignis eingetreten ist (RFC 3339) — nicht, wann dieser Versuch signiert wurde. |
payload_version | Version des Envelope. Siehe Versionierung. |
actor | Wer es ausgelöst hat: {"type":"USER","id":…} oder {"type":"SYSTEM"}. Fehlt, wenn nicht erfasst. |
request | Korrelations-Ids für Support und Tracing. Fehlt, wenn nicht zutreffend. |
data.object | Die Ressource, um die es in diesem Ereignis geht. |
data.previous_attributes | Bei einer Bearbeitung die alten Werte der geänderten Schlüssel. Fehlt sonst. |
previous_attributes
Nur bei Bearbeitungen vorhanden, und nur mit dem, was sich geändert hat:
"data": {
"object": { "id": "…", "name": "2024 reports", "parent_id": null },
"previous_attributes": { "name": "2024", "parent_id": "9f8c…" }
}
Lesen Sie es als „was diese Schlüssel vorher waren“. Ein Schlüssel mit dem Wert null bedeutet, dass
er tatsächlich leer war — ein in die oberste Ebene verschobener Ordner meldet
"parent_id": null. Das ist etwas anderes als ein fehlender Schlüssel (dieser Schlüssel hat sich
nicht geändert).
Was data.object garantiert
Genau drei Dinge:
- Die Identität der Ressource ist immer enthalten.
idbei einer Ressource, die eine hat, oder ein Verweis auf ihr übergeordnetes Objekt, wenn sie keine eigene hat —comment.reactionhat keine eigene Id und führt deshalbcomment_id. - Innerhalb einer
payload_versionwerden Schlüssel nur ergänzt — nie entfernt, umbenannt oder im Typ geändert. - Die Schlüsselmenge ist offen. Behandeln Sie unbekannte Schlüssel als normal und ignorieren Sie diejenigen, die Sie nicht verwenden.
Ausdrücklich nicht garantiert ist, dass data.object dem entspricht, was ein GET für dieselbe
Ressource zurückgibt. Es sieht oft ähnlich aus, und sich darauf zu verlassen, geht früher oder später
schief: Zu einem deleted-Ereignis gibt es kein GET, dem es entsprechen könnte, und es können
Plugin-spezifische Felder auftreten, die kein GET ausliefert. Lesen Sie die Felder, die Sie
brauchen, und ignorieren Sie den Rest.
Header
| Header | Beschreibung |
|---|---|
webhook-id | Eindeutige Nachrichten-Id (UUID). Über Wiederholungen hinweg stabil — nutzen Sie sie als Idempotenz-Schlüssel. |
webhook-timestamp | Unix-Zeitstempel (Sekunden) des Signaturzeitpunkts. |
webhook-signature | Die mit v1, präfigierte Signatur — siehe unten. |
request-id | Korrelations-Id für Support und Tracing. |
5. Signatur verifizieren
Verifizieren Sie immer, bevor Sie eine Payload parsen oder ihr vertrauen. Dieser vollständige FastAPI-Empfänger verwendet die offizielle Standard-Webhooks-Bibliothek und verifiziert die exakten Request-Bytes:
import json
import os
from fastapi import FastAPI, HTTPException, Request, Response
from standardwebhooks import Webhook, WebhookVerificationError
app = FastAPI()
verifier = Webhook(os.environ["TOURFOLD_WEBHOOK_SECRET"])
@app.post("/tourfold/webhooks")
async def receive_tourfold_webhook(request: Request):
raw_body = await request.body()
headers = {
"webhook-id": request.headers.get("webhook-id", ""),
"webhook-timestamp": request.headers.get("webhook-timestamp", ""),
"webhook-signature": request.headers.get("webhook-signature", ""),
}
try:
verifier.verify(raw_body, headers)
except WebhookVerificationError as exc:
raise HTTPException(status_code=400, detail="Invalid webhook signature") from exc
event = json.loads(raw_body)
# In Produktion webhook-id atomar entdoppeln und die Verarbeitung
# dauerhaft einreihen, bevor die Zustellung bestätigt wird.
print(event["type"])
return Response(status_code=204)
python -m pip install fastapi standardwebhooks uvicorn
TOURFOLD_WEBHOOK_SECRET='whsec_...' \
uvicorn receiver:app --host 0.0.0.0 --port 8000
print dient nur der Veranschaulichung. Speichern Sie in Produktion die webhook-id und
reihen Sie die dauerhafte Verarbeitung ein, bevor Sie mit 2xx antworten. Langsamere Arbeit
erledigen Sie danach asynchron. Protokollieren Sie weder Signatur-Secrets noch vollständige
Payloads, die Kundendaten enthalten können.
Bei manueller Verifizierung: Die Signatur ist v1, gefolgt vom base64-kodierten HMAC-SHA256 der
Zeichenkette {webhook-id}.{webhook-timestamp}.{raw_body}. Der Schlüssel ist Ihr Secret ohne das
Präfix whsec_, dessen Rest base64-dekodiert wird; raw_body sind die exakt empfangenen Bytes
(nicht erneut serialisieren):
key = base64_decode(Secret ohne das Präfix "whsec_")
signed_content = webhook_id + "." + webhook_timestamp + "." + raw_body
expected = "v1," + base64(hmac_sha256(key, signed_content))
Setzen Sie außerdem Replay-Schutz um: Lehnen Sie Zustellungen ab, deren webhook-timestamp
außerhalb eines Toleranzfensters liegt (5 Minuten sind üblich).
6. Wiederholungen & Fehler
Die Zustellung erfolgt at-least-once. Sie gilt als erfolgreich, wenn Ihr Endpoint ein beliebiges
HTTP 2xx zurückgibt; alles andere (auch Timeout oder Verbindungsfehler) ist ein Fehlschlag.
Endpoints wiederholen standardmäßig (retry: true am Endpoint). Nach dem Erstversuch wird eine
fehlgeschlagene Zustellung bis zu 6-mal mit steigendem Backoff wiederholt, also höchstens
7 Versuche insgesamt:
| Wiederholung | Wartezeit nach dem vorherigen Versuch |
|---|---|
| 1 | 30 Sekunden |
| 2 | 2 Minuten |
| 3 | 10 Minuten |
| 4 | 30 Minuten |
| 5 | 1 Stunde |
| 6 | 2 Stunden |
Da jede Wiederholung dieselbe webhook-id trägt, entdoppeln Sie darüber: verarbeitete Ids
festhalten und Wiederholungen ignorieren. Zusammen mit der Zeitstempel-Toleranz schützt Sie das vor
doppelten Zustellungen und Replays.
Die Reihenfolge ist nicht garantiert. Durch Wiederholungen und parallele Zustellung kann ein
späteres Ereignis ein früheres überholen — verlassen Sie sich nie auf die Eingangsreihenfolge,
sondern gleichen Sie über timestamp und Ihren eigenen Zustand ab. Ein 429 wird berücksichtigt:
Tourfold wartet dann mindestens 5 Minuten, oder länger, wenn Ihr Retry-After größer ist. Ein mit
retry: false erstellter Endpoint wird nicht wiederholt — er schlägt beim ersten Fehlversuch
endgültig fehl.
Nach dem letzten Versuch ist eine Zustellung endgültig fehlgeschlagen, und es gibt keine
automatische erneute Zustellung — die Wiederherstellung liegt bei Ihnen. Sehen Sie sich
aufgezeichnete Fehlschläge mit
listDeliveryFailures
an (eine Zeile je Nachricht, die mindestens einmal fehlgeschlagen ist; bei einem weiteren
fehlgeschlagenen Versuch wird dieselbe Zeile aktualisiert). Das ist kein Zustellungsprotokoll:
Erfolgreiche Versuche werden nicht aufgezeichnet, und eine später erfolgreiche Wiederholung ergänzt
keinen Erfolgseintrag. Das Fehlen eines Eintrags beweist daher keinen Erfolg; ein Eintrag allein
beweist keinen endgültigen Fehlschlag. Mit retry_state: EXHAUSTED erkennen Sie Nachrichten, deren
Versuchsbudget ausgeschöpft wurde, und können sie bei Bedarf aus Ihrem eigenen Zustand nachholen.
Fehlereinträge werden etwa 30 Tage aufbewahrt und danach gelöscht.
7. Secret rotieren
Rotieren Sie ein kompromittiertes oder in die Jahre gekommenes Secret mit
rotateWebhookEndpointSecret.
Das neue whsec_…-Secret wird einmalig zurückgegeben. Die Rotation gilt sofort: Sobald die
Operation erfolgreich war, ist das alte Secret ungültig; nachfolgende Zustellungen tragen genau
eine Signatur mit dem neuen Secret. Koordinieren Sie die Aktualisierung des Empfängers als Cutover.
Zustellungen, die eintreffen, bevor der Empfänger das neue Secret kennt, schlagen fehl und folgen
der Retry-Konfiguration des Endpoints.
8. Versionierung und Kompatibilität
Der Envelope ist über die Ganzzahl payload_version versioniert. Ereignisnamen tragen kein
Versionssuffix.
Innerhalb einer payload_version nehmen wir ausschließlich rückwärtskompatible Änderungen vor:
- Neue Felder können jederzeit zu
data.objectoder zum Envelope hinzukommen — ignorieren Sie unbekannte Felder, damit Ihre Integration weiterläuft, wenn sie auftreten. - Eine brechende Änderung erscheint als neue
payload_version; die bestehende Version behält ihren Vertrag.
Konfigurieren Sie Ihren Parser so, dass er unbekannte Eigenschaften toleriert.
:::note Änderung gegenüber dem früheren Schema
Ereignisnamen trugen früher ein Suffix .vN (brand.created.v1), und Breite entstand über separate
„Umbrella“-Ereignisse (resource.created.v1). Beides ist entfallen: Die Versionierung liegt jetzt in
payload_version des Envelope, und Breite entsteht über Abonnement-Muster.
Erzwungen hat die Änderung das Versionssuffix je Name: Es lässt sich nicht mit Präfix-Mustern vereinbaren, weil jede Versionserhöhung ein bereits gespeichertes Muster stillschweigend nicht mehr treffen würde. :::
9. Benutzerdefinierte Objekte
Ereignisse benutzerdefinierter Objekte werden über den Slug der Definition benannt:
custom_object.invoice.created.
Abonnements werden dagegen über die Id der Definition gespeichert. Dieser Unterschied ist gewollt und hat zwei Folgen:
-
Das Umbenennen einer Definition macht Ihr Abonnement nicht ungültig. Es greift weiter. Allerdings ändert sich der zugestellte
type, weil der Name den aktuellen Slug wiedergibt — verankern Sie einen Slug also nicht fest in einer Routing-Logik, die Sie nicht anpassen können.Routen Sie stattdessen über
data.object.definition_id. Jedes Ereignis eines benutzerdefinierten Objekts führt dieses Feld, und es ändert sich über die gesamte Lebensdauer der Definition nicht. Betrachten Sietypeals den lesbaren Namen, der dem aktuellen Slug folgt, unddefinition_idals den stabilen Schlüssel:"data": { "object": { "id": "…", "definition_id": "6b1e0f22-…", "definition_slug": "invoice" } } -
Löschen und Neuanlegen einer Definition mit demselben Slug belebt kein altes Abonnement. Die neu angelegte Definition ist ein anderes Objekt mit einer neuen Id.
Sie können ein Abonnement mit dem Slug oder mit der Id der Definition schreiben; beides führt zum
selben gespeicherten Abonnement. Wird eine Definition gelöscht, werden ihre Abonnements in Id-Form
zurückgemeldet und am Endpoint unter invalid_subscriptions aufgeführt, sodass Sie sie sehen und
entfernen können.
10. Checkliste für den Produktivbetrieb
Bevor Sie einen Endpoint für Produktivverkehr aktivieren:
- Verifizieren Sie die Signatur über die unveränderten Request-Bytes, bevor Sie JSON parsen oder irgendeinem Feld vertrauen.
- Erzwingen Sie ein Zeitstempel-Toleranzfenster und halten Sie die Uhren der Empfänger synchron.
- Legen Sie einen Unique Constraint auf
webhook-id; bestätigen Sie ein Duplikat, ohne es erneut anzuwenden. - Speichern Sie akzeptierte Zustellungen oder reihen Sie sie dauerhaft ein, bevor Sie
2xxzurückgeben, und antworten Sie innerhalb von 5 Sekunden. - Behandeln Sie die Payload als offenes Schema und ignorieren Sie unbekannte Felder.
- Rechnen Sie mit mehrfach und in anderer Reihenfolge eintreffenden Ereignissen. Nutzen Sie
timestampund gleichen Sie bei relevanter Reihenfolge mit dem aktuellen Zustand der REST API ab. - Überwachen Sie Zustellfehler, insbesondere
retry_state: EXHAUSTED, und definieren Sie einen Nachholprozess. Der Fehler-Endpoint ist kein vollständiges Zustellungsprotokoll. - Bewahren Sie das Signatur-Secret in einem Secret Manager auf und proben Sie die sofort wirksame Rotation.
- Pausieren Sie den Endpoint bei Wartungsarbeiten mit
enabled: false, wenn er Verkehr nicht sicher annehmen kann.
11. Fehlerbehebung
| Symptom | Prüfen Sie Folgendes |
|---|---|
Erstellung oder Änderung des Endpoints liefert 422 | Verwenden Sie eine absolute öffentliche HTTPS-URL ohne Zugangsdaten oder Fragment. Stellen Sie sicher, dass jede DNS-Antwort öffentlich ist und der Host aufgelöst wird. |
Änderung der Abonnements liefert 422 | Fragen Sie den aktuellen Ereigniskatalog ab, prüfen Sie die Schreibweise der Muster und verwenden Sie höchstens 100 Muster in der Ersatzmenge. |
| Signaturprüfung schlägt fehl | Verwenden Sie das einmalig ausgegebene Secret dieses Endpoints, verifizieren Sie die unveränderten Bytes und alle drei webhook-*-Header, prüfen Sie die Uhrabweichung und beachten Sie, dass eine Rotation das alte Secret sofort ungültig macht. |
Ein Test liefert test_was_successful: false | Prüfen Sie öffentliche Erreichbarkeit, ein vertrauenswürdiges TLS-Zertifikat und eine 2xx-Antwort innerhalb von 5 Sekunden. Weiterleitungen werden nicht verfolgt. Prüfen Sie den Fehlereintrag für test.webhook. |
Ein PATCH liefert nach einem Test 409 | Der Test hat den Endpoint-Zustand aktualisiert und lock_version erhöht. Rufen Sie den Endpoint erneut ab, führen Sie Ihre Änderung zusammen und wiederholen Sie sie mit der aktuellen Version. |
| Dasselbe Ereignis wird zweimal verarbeitet | At-least-once-Zustellung verhält sich wie vorgesehen. Entdoppeln Sie über webhook-id, die bei Wiederholungen stabil bleibt. |
| Ereignisse scheinen in falscher Reihenfolge einzutreffen | Zustellungen laufen parallel, und Wiederholungen können neuere Versuche überholen. Nutzen Sie nicht die Eingangsreihenfolge, sondern timestamp und den aktuellen Ressourcenzustand. |
invalid_subscriptions ist nicht leer | Eine referenzierte Definition eines benutzerdefinierten Objekts wurde gelöscht. Senden Sie per PATCH die vollständige gewünschte subscriptions-Menge ohne die ungültigen Einträge. |
Ein Fehler zeigt retry_state: EXHAUSTED | Die automatischen Versuche sind beendet. Reparieren Sie den Empfänger und gleichen Sie anschließend über die REST API ab oder holen Sie Daten nach; es gibt keinen Endpoint zur erneuten Zustellung. |
| Es ist kein Fehler aufgeführt | Das beweist keine Zustellung. Nur fehlgeschlagene Nachrichten werden erfasst, Einträge verfallen nach etwa 30 Tagen und erfolgreiche Versuche bilden kein durchsuchbares Protokoll. |