Webhooks sind der Teil der API-Oberfläche eines URL-Shorteners, den jeder ausliefert und fast niemand gut ausliefert. Die schwierigen Teile sind nicht die Kodierung — der Payload ist ein JSON-Objekt — sondern die operativen Details: Signaturverifizierung, Retry-Richtlinie, Idempotenz, Zustellgarantien und was passiert, wenn der Subscriber-Endpunkt zwei Tage lang nicht erreichbar ist.
Dieser Beitrag dokumentiert jeden Webhook-Event, den Elido aussendet, jede Form des Payloads, die Retry-Kurve und das Signaturschema. Der URL-Shortener-API + SDKs Quickstart deckt die eingehende API-Oberfläche ab; dies ist die ausgehende Seite.
Die 12 Event-Typen#
Elido sendet 12 Webhook-Event-Typen aus, gruppiert in drei Familien:
Click- und Traffic-Events: click, bio.click, qr.scan, conversion. Diese werden bei jeder Weiterleitung oder bei jedem Scan nach einer kurzen Warteschlangenverzögerung ausgelöst (unten beschrieben).
Lifecycle-Events: link.created, link.updated, link.deleted, bio.published. Diese werden von der API-Schicht ausgelöst, wenn sich der zugrunde liegende Datensatz ändert.
Aggregations- und Ops-Events: daily.summary, campaign.ended, alert.threshold_exceeded, quota.warning. Diese werden planmäßig oder bei Schwellenwertüberschreitung ausgelöst.
Ein Subscriber registriert einen Webhook unter POST /v1/webhooks mit einer Ziel-URL und einem Array von Event-Typen, die er zugestellt haben möchte. Die vollständige Subscription-Anfrage:
POST /v1/webhooks
Content-Type: application/json
Authorization: Bearer <api-key>
{
"url": "https://example.com/webhooks/elido",
"events": ["click", "conversion", "link.created"],
"secret": "whsec_<32-byte-base64>",
"active": true
}
Das secret ist der HMAC-Schlüssel, mit dem ausgehende Anfragen signiert werden. Er ist für Elido opak; wir protokollieren oder zeigen ihn nach der Antwort auf den Erstellungsaufruf nie an.
Der click-Event-Payload#
Nach Volumen ist dies der Event, der für Sie wichtig ist. Jede Weiterleitung über einen beliebigen Kurzlink erzeugt einen click-Event, nachdem die Weiterleitung dem Client zugestellt wurde. Die Struktur:
{
"id": "evt_2g8kFqJxYwPaZcvAm3HsTr",
"type": "click",
"created_at": "2026-05-22T14:32:18.847Z",
"data": {
"link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"short_url": "https://elido.me/abc123",
"destination_url": "https://shop.example.com/spring",
"click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
"ip_prefix": "203.0.113.0/24",
"country": "DE",
"city_geoname_id": 2950159,
"user_agent_family": "Chrome 124",
"device_type": "mobile",
"os_family": "iOS 17.5",
"referrer": "https://www.google.com",
"utm_source": "newsletter",
"utm_medium": "email",
"utm_campaign": "spring-2026",
"utm_term": null,
"utm_content": null
},
"workspace_id": "ws_12"
}
Einige Details, die es wert sind, hervorgehoben zu werden:
ip_prefix, nichtip. Wir behalten das /24 (IPv4) oder /48 (IPv6)-Netzwerkpräfix, nicht die vollständige Adresse. Der DSGVO für URL-Shortener-Beitrag erklärt warum; der praktische Effekt ist, dass Ihr Subscriber genug geografische Präzision für Analytics erhält, ohne die Haftung für personenbezogene Daten vollständiger IPs.city_geoname_id, nichtcity_name. Die GeoNames-ID ist über Locales hinweg stabil; der Stadtname variiert. Wenn Sie einen lokalisierten Namen benötigen, suchen Sie die ID einmalig in GeoNames.org's Dump nach und cachen Sie das Ergebnis.user_agent_family, nicht der vollständige UA-String. Wir entfernen den vollständigen UA bei der Erfassung (er ist hochentropische Fingerabdruckdaten); die Family ist der Browser+Hauptversion, der übrig bleibt.
Die Verzögerung zwischen der Weiterleitung, die den Client bedient, und dem Auslösen des Webhooks beträgt typischerweise 200ms bis 2s. Click-Events fließen zuerst durch Redpanda, werden für Analytics aggregiert und dann sendet ein Fan-out-Worker die Webhooks aus. Dies ist dieselbe Pipeline, die das Dashboard-Analytics antreibt — der Fire-and-Forget-Click-Ingestion-Beitrag deckt die Warteschlangenmechanik ab.
Der conversion-Event-Payload#
Conversion-Events werden ausgelöst, wenn ein Click einer nachgelagerten Conversion zugeordnet wird — einem Kauf, einer Anmeldung, einem Lead-Formular oder allem anderen, was Sie in die Conversion-Forwarding-Pipeline einbinden.
{
"id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
"type": "conversion",
"created_at": "2026-05-22T14:38:42.193Z",
"data": {
"click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
"link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"conversion_id": "cnv_2g8mPzKlYxRcBnQvFt5HwS",
"value": 49.50,
"currency": "EUR",
"event_name": "purchase",
"product_id": "sku_42",
"metadata": {
"order_id": "ord_12345",
"is_new_customer": true
},
"attribution_window_minutes": 6,
"forwarded_to": ["meta_capi", "ga4_mp"]
},
"workspace_id": "ws_12"
}
Die click_id verweist zurück auf den ursprünglichen Click-Event; Sie können beide serverseitig verbinden, um den Click-to-Conversion-Pfad zu rekonstruieren. Die attribution_window_minutes ist die verstrichene Zeit zwischen dem Click und dem Auslösen der Conversion, was für das Attributionsmodelling nützlich ist.
Das forwarded_to-Array zeigt Ihnen, welche Plattform-Pixel Elido diese Conversion bereits weitergeleitet hat. Wenn Ihr Subscriber Conversions in Ihr eigenes Data Warehouse einbindet, können Sie dies verwenden, um Doppelzählungen gegen Ihre nachgelagerten Analytics zu vermeiden.
Der link.created-Event-Payload#
Lifecycle-Events haben eine schlankere Struktur — nur die Ressource und der Akteur:
{
"id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
"type": "link.created",
"created_at": "2026-05-22T14:38:42.193Z",
"data": {
"link": {
"id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"slug": "abc123",
"short_url": "https://elido.me/abc123",
"destination_url": "https://shop.example.com/spring",
"domain": "elido.me",
"tags": ["spring-2026", "newsletter"],
"created_at": "2026-05-22T14:38:42.193Z",
"created_by": "usr_42"
}
},
"workspace_id": "ws_12"
}
link.updated enthält neben dem neuen Zustand einen previous-Snapshot; link.deleted enthält den letzten Zustand des Links zum Zeitpunkt der Löschung. Das vollständige Schema befindet sich im operativen Leitfaden /docs/guides/conversion-forwarding.
Signaturverifizierung#
Jede Webhook-Anfrage enthält drei HTTP-Header:
Elido-Signature: t=1716392538,v1=4f3b2e1d9c8a7b6f5e4d3c2b1a0f9e8d7c6b5a49382716054 ...
Elido-Webhook-Id: evt_2g8kFqJxYwPaZcvAm3HsTr
Elido-Delivery-Attempt: 1
Das Signaturschema folgt dem Stripe-Modell: HMAC-SHA256 über {timestamp}.{body} unter Verwendung des Webhook-Secrets. Das v1=-Präfix ist die Version des Signaturalgorithmus; neue Algorithmusversionen werden hinzugefügt, bevor sie zum Standard gemacht werden, sodass Subscriber mehrere Versionen gleichzeitig verifizieren können.
Verifizierung in Go:
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
)
func verify(sigHeader, body, secret string) bool {
parts := strings.Split(sigHeader, ",")
var t int64
var v1 string
for _, p := range parts {
kv := strings.SplitN(p, "=", 2)
if len(kv) != 2 { continue }
switch kv[0] {
case "t":
fmt.Sscanf(kv[1], "%d", &t)
case "v1":
v1 = kv[1]
}
}
if time.Since(time.Unix(t, 0)) > 5*time.Minute {
return false // reject stale requests
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.%s", t, body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(v1))
}
Die 5-Minuten-Veraltungsprüfung ist der Teil, den die meisten Subscriber vergessen. Ohne sie gelingt ein Replay-Angriff — ein Angreifer, der eine gültige Anfrage abgefangen hat und sie später wiedergibt — weil die Signatur noch gültig ist. Mit der Zeitstempelprüfung wird die Anfrage nur innerhalb eines 5-Minuten-Fensters ab dem Zeitpunkt akzeptiert, zu dem Elido sie ausgesendet hat.
Die Signaturspezifikation ist im OWASP-Cheat-Sheet zur Webhook-Sicherheit dokumentiert; wir haben das Muster nicht erfunden, sondern nur implementiert.
Retry-Richtlinie#
Dies ist der Teil, bei dem die meisten Webhook-Implementierungen nachlässig werden.
Ein Webhook wird auf dem Happy Path einmal ausgelöst: Der Subscriber gibt 2xx zurück, der Dispatcher verzeichnet Erfolg, das Event ist abgeschlossen. Die schwierigeren Fälle sind Nicht-2xx-Antworten, Netzwerkfehler und Subscriber, die langsam antworten.
Der Elido-Retry-Zeitplan:
| Versuch | Verzögerung nach vorherigem | Kumulativ | Status |
|---|---|---|---|
| 1 | — | 0 | initial |
| 2 | 1s | 1s | erster Retry |
| 3 | 30s | 31s | |
| 4 | 5m | 5m 31s | |
| 5 | 1h | 1h 5m 31s | |
| 6 | 6h | 7h 5m 31s | |
| 7 | 24h | 31h 5m 31s | final |
Nach Versuch 7 (~31 Stunden nach dem ersten Versuch) gibt der Dispatcher auf und sendet ein internes webhook.failed-Event aus. Der Subscriber-Endpunkt wird nach drei aufeinanderfolgenden Fehlern bei beliebigen Events als degradiert markiert; degradierte Subscriptions erhalten für 24 Stunden ein reduziertes Retry-Budget. Nach 50 aufeinanderfolgenden Fehlern wird die Subscription pausiert und der Workspace-Eigentümer benachrichtigt.
Das Retry-Verhalten respektiert Retry-After-Header des Subscribers. Wenn Ihr Endpunkt Elido ratenlimitiert (gibt 429 mit Retry-After: 120 zurück), wartet der nächste Versuch 120 Sekunden statt der standardmäßigen 30s des Zeitplans.
Keine Antwort innerhalb von 10 Sekunden wird als Timeout behandelt und zählt als fehlgeschlagener Versuch. Das 10-Sekunden-Budget ist bewusst großzügig — es deckt die Cold-Start-Latenz bei serverlosen Subscribern ab — aber wenn Ihr Endpunkt regelmäßig mehr als 5 Sekunden benötigt, beheben Sie das zuerst; es wird Sie bei der Retry-Menge kosten.
Idempotenz#
Subscriber können dasselbe Event mehr als einmal empfangen.
Dies ist kein Fehler; es ist die Konsequenz der Funktionsweise verteilter Nachrichtenzustellung. Wenn ein Subscriber einen 504 zurückgibt, weil sein Backend langsam war, aber das Event schließlich verarbeitet hat, wird der Dispatcher einen Retry durchführen; der Subscriber empfängt es zweimal und verarbeitet es möglicherweise zweimal. Dasselbe Event kann auch zweimal ausgelöst werden, wenn der Dispatcher mitten in der Zustellung abstürzt und das Event neu in die Warteschlange eingereiht wird.
Die Gegenmaßnahme: Jedes Event hat eine eindeutige id (das evt_…-Präfix). Subscriber sollten die IDs speichern, die sie bereits verarbeitet haben (eine kleine Key-Value-Tabelle funktioniert; TTL von 14 Tagen deckt das Retry-Fenster mit Marge ab) und Events überspringen, deren ID sie bereits gesehen haben.
CREATE TABLE webhook_processed_events (
event_id TEXT PRIMARY KEY,
received_at TIMESTAMPTZ DEFAULT now()
);
-- in your handler:
INSERT INTO webhook_processed_events (event_id) VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
-- if the RETURNING is empty, you've already processed this event
Das ON CONFLICT DO NOTHING ist die günstige Idempotenzprüfung. Wenn das Insert eine Zeile zurückgibt, haben Sie das Event zum ersten Mal gesehen; gibt es nichts zurück, haben Sie es bereits verarbeitet.
Für Hochdurchsatz-Subscriber (>1k Events/Sek.) funktioniert ein dediziertes Redis SETNX mit TTL auf dieselbe Weise zu geringeren Kosten als eine Postgres-Zeile.
Zustellreihenfolge#
Es gibt keine globale Reihenfolgegarantie. Events von derselben link_id werden in der Einreichungsreihenfolge gesendet, aber Events von verschiedenen Links können verzahnt ankommen. Ein click-Event zum Zeitpunkt T+0 und ein conversion-Event zum Zeitpunkt T+10ms können je nach Worker-Pool-Zustand in beliebiger Reihenfolge bei Ihrem Subscriber ankommen.
Die created_at-Zeitstempel sind autoritativ für die Reihenfolge. Wenn Ihr Subscriber eine strikte Reihenfolge benötigt, sortieren Sie nach created_at serverseitig vor der Verarbeitung.
Für den Click → Conversion-Pfad speziell: Das Conversion-Event verweist immer auf die click_id des Click-Events, sodass Sie sie serverseitig verbinden können, auch wenn sie außer der Reihe ankommen.
Webhooks vs. Polling — der Kompromiss#
Der Webhooks vs. Polling für Click-Tracking-Beitrag deckt dies im Detail ab. Die kurze Antwort: Webhooks sind das richtige Muster, wenn (a) Sie geringe Latenz bei der Event-Ankunft benötigen (<5 Sekunden) und (b) Ihr Subscriber über das öffentliche Internet mit TLS erreichbar ist. Polling ist das richtige Muster, wenn (a) Sie kein Echtzeit benötigen, (b) Sie das Data Warehouse kontrollieren und nur einen täglichen/stündlichen Batch wollen oder (c) Ihr Subscriber sich in einem Netzwerk befindet, das keinen eingehenden Traffic akzeptiert.
Für die meisten Teams sind Webhooks die Antwort. Die Retry-Kurve behandelt vorübergehende Fehler elegant; das Signaturschema behandelt die Sicherheit; das Idempotenz-Modell behandelt Zustellduplizierung. Die Arbeit liegt auf der Subscriber-Seite — dem Aufbau eines robusten Handlers — und diese Arbeit ist gering im Vergleich zum Aufbau einer polling-basierten Ingestion-Pipeline.
Operatives Tooling#
Die Webhook-Seite des Dashboards zeigt drei Dinge pro Subscription:
- Zustellhistorie: jedes gesendete Event, der HTTP-Status, den der Subscriber zurückgegeben hat, die Latenz und der nächste Retry-Zeitstempel (falls vorhanden).
- Replay: ein Button pro Event zum Wiederholen. Nützlich zum Testen von Änderungen an Ihrem Handler.
- Test-Endpunkt: ein Button pro Subscription zum Senden eines synthetischen Test-Events, ohne einen echten Click auszulösen. Das Test-Event hat
type: "test"und einen festen Payload.
Die Replay- und Test-Endpunkte sind auch als API-Endpunkte verfügbar (POST /v1/webhooks/{id}/events/{evt_id}/replay und POST /v1/webhooks/{id}/test).
Für Hochdurchsatz-Debugging deckt der Observability-Leitfaden ab, wie Sie die Webhook-Zustellung in Ihre eigenen Metriken einbinden — jede Zustellung wird als Prometheus-Counter und Histogramm exportiert.
Externe Referenzen#
- OWASP-Webhook-Sicherheits-Cheat-Sheet — die Begründung des Signaturschemas.
- Stripe's Webhook-Dokumentation — die Referenzimplementierung für HMAC-signierte Webhooks.
- RFC 7234 — HTTP/1.1 Caching — deckt
Retry-After-Semantik ab.
Weiterführende Lektüre#
- Smart Links erklärt — der Features-Cluster-Cornerstone.
- Webhooks vs. Polling für Click-Tracking — wann Sie was wählen.
- URL-Shortener-API + SDKs Quickstart — die eingehende API-Oberfläche.
- Fire-and-Forget-Click-Ingestion mit Redpanda — die Warteschlange hinter dem Dispatcher.
- Serverseitiges Conversion-Tracking — was das
conversion-Event auslöst. - Produktoberflächen:
/features/webhooks,/solutions/developers. - Operativer Leitfaden:
/docs/guides/conversion-forwarding,/docs/guides/observability.