Webhooks
Ereignistypen, Payload, Signaturprüfung, Wiederholungen, Pausieren, Test-Ereignisse und Transportsicherheit.
letr.dev sendet ein Ereignis an Ihre Endpunkte, sobald eine E-Mail versendet, zugestellt, zurückgestellt, abgewiesen, als Spam gemeldet oder aufgegeben wurde, und wenn Ihr Konto pausiert oder reaktiviert wird. Ein Konto hat bis zu 5 Endpunkte, jeder mit eigener Auswahl an Ereignissen und eigenem Signing-Secret. Endpunkte erstellen Sie im Dashboard; die API kann sie auflisten, ändern, löschen und testen.
Unter /v1/webhooks listen Keys mit read_only und full_access die Endpunkte auf, nie deren Secrets. Nur full_access darf url, events, enabled oder paused ändern, einen Endpunkt löschen oder ein Test-Ereignis senden. Endpunkte erstellen, Secrets rotieren und Zertifikate hinterlegen bleibt im Dashboard. Ein Key mit sending_access erhält 403 insufficient_permission, und ein Endpunkt eines anderen Kontos ergibt 404 not_found.
curl -X PATCH https://api.letr.dev/v1/webhooks/$ID \
-H "Authorization: Bearer $LETR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
Felder, die Sie weglassen, bleiben, wie sie sind. Wird ein Endpunkt gelöscht, verfallen die Zustellungen, die für ihn noch ausstehen. Die API-Referenz nennt alle Felder.
Ereignistypen
| Typ | Bedeutung |
|---|---|
email.sent |
Der Mailserver von letr.dev hat die E-Mail zur Zustellung angenommen. |
email.delivered |
Der Server des Empfängers hat sie angenommen. |
email.deferred |
Die Zustellung ist vorerst gescheitert und wird wiederholt. |
email.bounced |
Der Server des Empfängers hat sie abgewiesen. |
email.complained |
Der Empfänger hat sie als Spam gemeldet. |
email.failed |
letr.dev hat sie aufgegeben. |
account.paused |
Ihr Konto wurde pausiert und kann nicht versenden. |
account.reactivated |
Ihr Konto wurde reaktiviert und kann wieder versenden. |
webhook.test |
Nur von der Test-Aktion. |
Payload
{
"id": "evt_4xud2qy0r8lm1kc7tz5vb9wh",
"type": "email.delivered",
"createdAt": "2026-11-02T08:15:30Z",
"data": {
"emailId": "em_gk3lx0aqw2rz9vb7nc1ep4ty",
"recipient": "[email protected]",
"tags": { "type": "invoice" },
"detail": "250 2.0.0 OK"
}
}
detail enthält die SMTP-Antwort oder die Bounce-Klassifizierung, oder null. Die id des Ereignisses ist auf jedem Endpunkt und bei jeder Wiederholung dieselbe: Damit erkennen Sie Duplikate. Ereignisse können auch in anderer Reihenfolge eintreffen: Ordnen Sie sie nach createdAt und setzen Sie einen Status nie zurück.
account.paused und account.reactivated enthalten statt einer E-Mail das Konto:
{
"id": "evt_8rk1vz3qm7lp0xc2wb6hd2ma",
"type": "account.paused",
"createdAt": "2026-11-02T09:40:12Z",
"data": { "accountId": "acc_3fp9kd2wq7ys1lb4mx8tz5qe", "reason": "bounce_rate", "detail": null }
}
reason ist bei einer Pause bounce_rate, complaint_rate, payment_failed oder operator und bei account.reactivated immer resumed. Bei operator enthält detail die Begründung der Person bei letr.dev, sonst ist es null. Bei jeder Pause erhält die Kontoinhaberin oder der Kontoinhaber zusätzlich eine E-Mail.
Signatur prüfen
letr.dev signiert jede Anfrage nach Standard Webhooks, deshalb prüfen die offiziellen Bibliotheken von Standard Webhooks sie. Jede Anfrage trägt:
webhook-id: die ID des Ereignisses.webhook-timestamp: den Zeitpunkt des Versands in Unix-Sekunden.webhook-signature:v1,<Signatur>, der HMAC-SHA256 von<webhook-id>.<webhook-timestamp>.<Rohdaten>in Base64.letr-endpoint-id: den Endpunkt, für den die Anfrage bestimmt ist.
Der Schlüssel ist das Signing-Secret des Endpunkts, das mit whsec_ beginnt: Dekodieren Sie den Teil nach whsec_ aus Base64. Akzeptieren Sie die Anfrage, wenn eine Signatur passt, und lehnen Sie sie ab, wenn der Zeitstempel mehr als 5 Minuten abweicht. Prüfen Sie die Rohdaten, vor jedem JSON-Parsing.
// Node.js: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
const webhook = new Webhook(process.env.LETR_WEBHOOK_SECRET);
// rawBody: the request body as received, a string or a Buffer
const event = webhook.verify(rawBody, request.headers);
verify wirft einen WebhookVerificationError, wenn die Signatur nicht passt oder der Zeitstempel zu alt oder zu neu ist, und gibt sonst das geparste Ereignis zurück.
# Python, without a library
import base64, hashlib, hmac, time
def verify_letr_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
msg_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for entry in headers["webhook-signature"].split(" "):
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature, expected):
return True
return False
Secret rotieren
Ein Secret rotieren Sie im Dashboard mit einer Übergangsfrist von sofort, 1 Stunde, 24 Stunden oder 7 Tagen. Bis die Frist endet, trägt webhook-signature zwei Signaturen, durch ein Leerzeichen getrennt, eine mit dem neuen und eine mit dem alten Secret: v1,<neu> v1,<alt>. Beide Beispiele oben akzeptieren jede davon, so stellen Sie das neue Secret in Ihrem Tempo um.
Wiederholungen
letr.dev wartet 10 Sekunden auf eine Antwort. Jede Antwort ausser 2xx, oder keine Antwort in dieser Zeit, wird mit exponentiellem Backoff wiederholt, ab 30 Sekunden und auf höchstens 2 Stunden zwischen den Versuchen wachsend, während 24 Stunden. Danach gilt die Zustellung als gescheitert. Antworten Sie schnell und erledigen Sie die Arbeit nach der Bestätigung.
Fehlerhafte Endpunkte
failingSince eines Endpunkts ist der Zeitpunkt des ersten gescheiterten Versuchs seit dem letzten Erfolg, und null, solange Zustellungen gelingen. Nach 24 Stunden mit Fehlern erhält die Kontoinhaberin oder der Kontoinhaber eine E-Mail, die den Endpunkt, den letzten Fehler und das erneute Senden nennt; sie kommt einmal pro Endpunkt, bis wieder eine Zustellung gelingt.
Sobald der Endpunkt funktioniert, senden Sie im Dashboard erneut: letr.dev reiht jede Zustellung an diesen Endpunkt aus den letzten 30 Tagen wieder ein, die nach 24 Stunden Wiederholungen gescheitert ist. Einen deaktivierten Endpunkt müssen Sie zuerst aktivieren (409 webhook_disabled).
Endpunkt pausieren
Pausieren Sie einen Endpunkt im Dashboard oder mit "paused": true über PATCH /v1/webhooks/{id} (full_access), solange Ihr Dienst für eine Wartung oder ein Deployment nicht erreichbar ist. letr.dev erfasst weiterhin für jedes abonnierte Ereignis eine Zustellung, sendet sie aber nicht; eine Zustellung, die beim Pausieren bereits gesendet wird, wird noch abgeschlossen. Der Endpunkt zeigt paused, pausedAt und heldDeliveries, die Zahl der wartenden Zustellungen. Test-Ereignisse und erneut gesendete Zustellungen an einen pausierten Endpunkt werden ebenfalls gehalten, und die E-Mail zu fehlerhaften Endpunkten geht während der Pause nicht hinaus.
Mit "paused": false setzen Sie den Endpunkt fort. letr.dev reiht die gehaltenen Zustellungen ein, die ältesten zuerst, wie gewohnt signiert und mit neuen Versuchen, und die 24 Stunden Wiederholungen zählen ab dem Fortsetzen. Ordnen Sie sie wie jede Zustellung auf Ihrer Seite nach createdAt. Einen pausierten Endpunkt erneut zu pausieren oder einen laufenden fortzusetzen, ändert nichts. Einen deaktivierten Endpunkt können Sie weder pausieren noch fortsetzen (409 webhook_disabled): Deaktiviert heisst, dass letr.dev für neue Ereignisse keine Zustellungen erfasst; ein Test-Ereignis geht trotzdem hinaus; ist der Endpunkt pausiert, wird es gehalten. Deaktivieren Sie einen pausierten Endpunkt, bleiben seine Zustellungen gehalten; aktivieren Sie ihn, bevor Sie ihn fortsetzen.
Wie jede Zustellung folgen gehaltene Zustellungen der Aufbewahrung von Webhook-Zustellungen von 32 Tagen: Eine länger gehaltene Zustellung wird gelöscht, ohne gesendet zu werden.
Test-Ereignisse
POST /v1/webhooks/{id}/test (full_access) oder das Dashboard sendet ein webhook.test-Ereignis an diesen Endpunkt, auch wenn er deaktiviert ist; ein pausierter Endpunkt hält es bis zum Fortsetzen. data.emailId und data.recipient sind leere Strings. Das Ergebnis erscheint beim Endpunkt als letzter Erfolg oder Fehler.
Transport
letr.dev sendet nur über https mit TLS 1.2 oder neuer, prüft immer den Hostnamen, folgt keinen Weiterleitungen und verweigert beim Verbinden private und Loopback-Adressen.
Server-CA festlegen
Im Dashboard können Sie einem Endpunkt die CA-Zertifikate (PEM) geben, die sein Serverzertifikat ausgestellt haben. letr.dev vertraut dann für diesen Endpunkt nur diesen statt den öffentlichen CAs; der Hostname wird weiterhin geprüft.
Mutual TLS
Pro Endpunkt kann letr.dev ein Client-Zertifikat vorweisen:
| Modus | So funktioniert er |
|---|---|
none |
Kein Client-Zertifikat. |
letr_ca |
letr.dev erstellt einen Schlüssel und ein Zertifikat, signiert von der letr.dev-Webhook-Client-CA, 1 Jahr gültig und 30 Tage vor Ablauf erneuert. Vertrauen Sie der CA aus /meta/webhook-client-ca.pem. |
customer_ca |
letr.dev erstellt einen Schlüssel und einen CSR; Ihre CA signiert ihn, und Sie laden das Zertifikat im Dashboard hoch. |
Der private Schlüssel verlässt letr.dev nie, ist verschlüsselt gespeichert und wird nie herausgegeben.
IP-Allowlist
GET /meta/webhook-ips listet die Adressen, von denen Webhooks kommen; IPv6 verwendet letr.dev nur, wenn eine IPv6-Adresse aufgeführt ist. Änderungen kündigt letr.dev den Kontoinhabern mindestens 14 Tage im Voraus per E-Mail an, und sie erscheinen in der Liste, bevor sie verwendet werden.
Eine Allowlist sagt Ihnen, dass eine Anfrage aus dem Netz von letr.dev kommt, nicht, dass sie echt ist oder Ihrem Konto gilt. Prüfen Sie die Signatur in jedem Fall; die Allowlist ist eine zusätzliche Schicht.