Webhooks für eingehende Kanäle
Provider-Webhook-Referenz für eingehende SMS-, WhatsApp- und Generic-Channel-Nachrichten sowie die MTA-Mailbox- und Credential-Callbacks.
Dies sind die Server-zu-Server-Webhook-Endpunkte, die Kundennachrichten außerhalb von E-Mail nach Owlat einspeisen. Jeder von ihnen nimmt ein eingehendes Payload eines externen Anbieters (Twilio, Meta oder Ihres eigenen Systems) entgegen, verifiziert es kryptografisch und wandelt es in eine Nachricht des vereinheitlichten Posteingangs um. Sie gehören nicht zur authentifizierten /api/v1/*-Oberfläche und werden nicht aus einem Browser aufgerufen — Sie konfigurieren sie im Dashboard des Anbieters, und dieser postet direkt an Ihr Deployment.
Diese Endpunkte nehmen eingehende Nachrichten entgegen. Der ausgehende Versand über SMS/WhatsApp/Generic ist durchgängig verdrahtet (echte Twilio-/Meta-/Webhook-Adapter) und wird über den Antwortpfad des KI-Agenten, die Antworten-Schaltfläche pro Nachricht in der Thread-Ansicht des Team-Posteingangs oder einen manuellen Versand aus der vereinheitlichten Zeitleiste eines Kontakts erreicht. Unter Kommunikationskanäle sehen Sie, was jeder Kanal heute leisten kann. Diese Seite behandelt die Empfangsseite.
Überblick: Wie eingehende Kanäle den vereinheitlichten Posteingang speisen
Alle drei Kanal-Webhooks nutzen dieselbe Pipeline (apps/api/convex/webhooks/pipeline.ts). Die Hülle ist für jeden Anbieter identisch; nur Signaturprüfung und Payload-Parsing unterscheiden sich, und diese liegen in anbieterspezifischen Adaptern unter apps/api/convex/webhooks/adapters/.
Die Pipeline führt diese Schritte der Reihe nach aus:
- Nicht-
POSTablehnen mit405. - Rate-Limiting nach Client-IP (
webhookIngestion: Token-Bucket, 50/Sek., Burst-Kapazität 100). Über dem Limit wird429mit einemRetry-After-Header zurückgeliefert. - Signatur verifizieren über den Provider-Adapter. Fail-closed — siehe Erforderliche Env-Secrets.
- Rohes Payload revisionssicher speichern (Best-Effort; lässt die Anfrage nie scheitern).
- Payload parsen in ein normalisiertes
channel.received-Ereignis. Ein Parse-Fehler liefert400; ein Payload ohne verwertbare Nachricht (z. B. ein Meta-Statusupdate) wird mit200 { "success": true, "ignored": true }quittiert. - Ereignis dispatchen. Der Dispatcher (
apps/api/convex/webhooks/dispatcher.ts) leitetchannel.receivedan die interne MutationprocessInboundChannelweiter, die den Kontakt auflöst oder anlegt, den Konversations-Thread findet oder wieder öffnet und eine eingehende Zeile inunifiedMessageseinfügt.
Kontakte werden pro Kanal aufgelöst, nicht per E-Mail: Eine eingehende SMS- oder WhatsApp-Nachricht legt über contactIdentities einen Kontakt an, der auf die Telefonnummer oder das Handle des Absenders geschlüsselt ist; diese Kontakte haben keine email. Jeder Kanal bildet seinen eigenen Identitäts-Schlüsselraum — ein john@example.com im generic-Kanal ist ein anderer Kontakt als einer im email-Kanal. Die kanalübergreifende Vereinheitlichung ist eine explizite Merge-Operation und erfolgt nicht automatisch.
Alle nachfolgenden Pfade sind relativ zur HTTP-URL Ihres Deployments, z. B. https://<your-deployment>.convex.site/webhooks/sms. Tragen Sie diese vollständige URL in den Webhook-Einstellungen des Anbieters ein.
POST /webhooks/sms (Twilio)
Eingehende SMS aus dem Programmable-Messaging-Webhook von Twilio.
| Eigenschaft | Wert |
|---|---|
| Methode | POST |
| Content-Type | application/x-www-form-urlencoded (Twilio-Standard) |
| Signatur-Header | X-Twilio-Signature |
| Signaturverfahren | HMAC-SHA1, Base64-kodiert |
| Secret | Auth Token des SMS-Kanals, sonst TWILIO_AUTH_TOKEN |
| Erfolgsantwort | Leeres TwiML <Response></Response> (text/xml) |
Der Adapter rekonstruiert Twilios kanonische Validierungszeichenkette — die vollständige Request-URL, gefolgt von allen Formularparametern in alphabetischer Reihenfolge aneinandergehängt (Schlüssel unmittelbar gefolgt vom Wert, ohne Trennzeichen) — und vergleicht dann den HMAC-SHA1 in konstanter Zeit mit X-Twilio-Signature. Das entspricht Twilios Spezifikation zur Request-Validierung.
Geparste Felder:
| Formularfeld | Zugeordnet zu |
|---|---|
From | Absenderkennung (erforderlich) |
Body | Nachrichtentext (erforderlich) |
MessageSid | externalMessageId |
MediaUrl0 | erste Medien-URL (optional) |
FromCity, FromState, FromCountry | als Metadaten gespeichert |
Fehlt From oder Body, schlägt das Parsen fehl und der Endpunkt liefert 400.
POST /webhooks/sms
X-Twilio-Signature: <base64 HMAC-SHA1>
Content-Type: application/x-www-form-urlencoded
From=%2B15551234567&Body=Hello&MessageSid=SM123&FromCity=Berlin
Bei Erfolg erhält Twilio den TwiML-Envelope mit leerer Response (von diesem Endpunkt wird keine automatische Antwort gesendet):
<?xml version="1.0" encoding="UTF-8"?><Response></Response>
Twilio nimmt keinen Zeitstempel in seine Signatur auf, daher kann diese Schicht Replays nicht erkennen. Der schlimmste Fall ist eine doppelte eingehende Nachricht, kein gefälschter Zustandsübergang — für einen eingehenden Kanal akzeptabel, verwenden Sie dieses Verfahren aber nicht für irgendetwas, das Abrechnung oder Authentifizierung verändert.
POST und GET /webhooks/whatsapp (Meta-Verifizierungs-Challenge)
WhatsApp-Business-Nachrichten kommen über Metas Cloud API. Derselbe Pfad bedient zwei Methoden: POST für eingehende Nachrichten und GET für die einmalige Verifizierungs-Challenge des Abonnements.
POST — eingehende Nachricht
| Eigenschaft | Wert |
|---|---|
| Methode | POST |
| Content-Type | application/json |
| Signatur-Header | X-Hub-Signature-256 |
| Signaturverfahren | HMAC-SHA256 über den rohen Body, hex-kodiert, mit Präfix sha256= |
| Secret | App Secret des WhatsApp-Kanals, sonst META_APP_SECRET |
| Erfolgsantwort | 200 OK (Klartext-Body OK) |
Der Adapter entfernt das Präfix sha256= aus dem Header und vergleicht den hex-kodierten HMAC-SHA256 des rohen Bodys in konstanter Zeit, gemäß Metas Webhook-Dokumentation.
Er liest die erste Nachricht aus dem verschachtelten Envelope entry[0].changes[0].value.messages[0]:
| Quellfeld | Zugeordnet zu |
|---|---|
messages[0].from | Absenderkennung (erforderlich) |
messages[0].text.body | Nachrichtentext |
messages[0].id | externalMessageId |
messages[0].image.url oder document.url | Medien-URL (optional) |
contacts[0].profile.name | als Metadaten gespeichert (profileName) |
Meta postet außerdem Zustell-/Lesestatus-Updates, die keine messages enthalten. Diese werden mit 200 { "success": true, "ignored": true } quittiert und erzeugen keine Posteingangsnachricht.
GET — Verifizierungs-Challenge
Wenn Sie ein Webhook-Abonnement aktivieren, sendet Meta eine GET-Anfrage mit den Query-Parametern hub.mode, hub.verify_token und hub.challenge. Der Handler läuft außerhalb der Reihe, vor der Inbound-Pipeline:
- Ist weder der Verify Token des WhatsApp-Kanals noch
META_VERIFY_TOKENgesetzt, liefert er503. - Ist
hub.modegleichsubscribeund stimmthub.verify_tokenmit dem konfigurierten Token überein (Vergleich in konstanter Zeit), gibt erhub.challengemit200zurück. - Andernfalls liefert er
403 Verification failed.
GET /webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=<your-token>&hub.challenge=12345
→ 200
12345
Setzen Sie den Verify Token des WhatsApp-Kanals (Einstellungen → Kanäle) — oder META_VERIFY_TOKEN — auf eine beliebige Zeichenkette Ihrer Wahl und tragen Sie denselben Wert in Metas Webhook-Konfiguration als Verify-Token ein.
POST /webhooks/channel (generischer Shared-Secret-Envelope)
Ein anbieterunabhängiger Endpunkt, um Nachrichten aus jedem System einzuspeisen, das JSON mit einem Shared Secret posten kann. Dies ist der Kanal mit dem geringsten Vertrauensniveau — er nutzt ein statisches Shared Secret statt eines HMAC pro Anfrage, daher ist das IP-Rate-Limit der Pipeline Ihre wichtigste Missbrauchskontrolle.
| Eigenschaft | Wert |
|---|---|
| Methode | POST |
| Content-Type | application/json |
| Auth-Header | X-Webhook-Secret, oder Authorization: Bearer <secret> |
| Auth-Verfahren | Zeichenkettenvergleich in konstanter Zeit |
| Secret | Secret Key des Generic-Webhook-Kanals, sonst GENERIC_WEBHOOK_SECRET |
| Erfolgsantwort | 200 { "success": true, "kind": "channel.received" } |
Der JSON-Envelope ist nachsichtig — Felder greifen auf mehrere Aliase zurück:
| Envelope-Feld | Aliase / Fallback | Zugeordnet zu |
|---|---|---|
from | sender, sonst wörtlich "webhook" | Absenderkennung |
text | message, content.text | Nachrichtentext |
html | content.html | HTML-Body (optional) |
subject | content.subject | Betreff (optional) |
id | messageId | externalMessageId (optional) |
metadata | — | Objekt aus String-Werten (optional) |
POST /webhooks/channel
X-Webhook-Secret: <your-secret>
Content-Type: application/json
{
"from": "user-42",
"text": "Hi, is anyone there?",
"id": "ext-9001",
"metadata": { "source": "intercom" }
}
Erforderliche Secrets und Fail-closed-Verhalten
Jeder Adapter löst sein Secret aus den Zugangsdaten auf, die in der eigenen Konfigurationszeile des Kanals hinterlegt sind (Einstellungen → Kanäle), und weicht auf die Umgebungsvariable des Deployments aus, wenn der Kanal keine solche Zugangsdaten besitzt. Ist keines von beiden gesetzt, lehnt der Endpunkt mit 503 ab — er akzeptiert niemals „vorläufig“ eine unsignierte Anfrage.
Die Rangfolge ist relevant, wenn beides existiert: Der im Kanalformular eingetragene Wert gewinnt, sodass eine Secret-Rotation dort wirksam wird, ohne das Deployment anzufassen.
| Endpunkt | Kanal-Zugangsdaten | Env-Fallback | Beides fehlt → | Falsche/fehlende Signatur → |
|---|---|---|---|---|
POST /webhooks/sms | SMS Auth Token | TWILIO_AUTH_TOKEN | 503 | 401 |
POST /webhooks/whatsapp | WhatsApp App Secret | META_APP_SECRET | 503 | 401 |
GET /webhooks/whatsapp | WhatsApp Verify Token | META_VERIFY_TOKEN | 503 | 403 |
POST /webhooks/channel | Generic Secret Key | GENERIC_WEBHOOK_SECRET | 503 | 401 |
Übliche Statuscodes in der gesamten Kanal-Pipeline:
| Status | Bedeutung |
|---|---|
200 | Angenommen (oder ignored: true bei einem Payload ohne Nachricht) |
400 | Body konnte nicht gelesen oder geparst werden |
401 | Fehlende oder ungültige Signatur / fehlendes Shared Secret |
403 | Meta-Verifizierungs-Challenge fehlgeschlagen (nur GET) |
405 | Andere Methode als POST (Kanal-Endpunkte) |
429 | Rate-limitiert; beachten Sie Retry-After |
503 | Endpunkt nicht konfiguriert — erforderliches Secret ist nicht gesetzt |
Setzen Sie diese mit convex env set (oder über die Umgebungskonfiguration Ihres Deployments). Die vollständige Übersicht finden Sie unter Umgebungsvariablen.
Eingehende MTA-Routen
Zwei weitere Webhooks liegen im selben HTTP-Router, sind aber Callbacks des mitgelieferten Mail Transfer Agents und der IMAP-Dienste und keine Kundenkanäle. Sie authentifizieren sich mit einer HMAC-SHA256-Signatur, die auf MTA_WEBHOOK_SECRET geschlüsselt ist und als X-MTA-Signature über die Zeichenkette <timestamp>.<body> gesendet wird, wobei der Zeitstempel in X-MTA-Timestamp steht. Beide lehnen ab, wenn MTA_WEBHOOK_SECRET nicht gesetzt ist (503), und weisen einen veralteten oder fehlenden Zeitstempel zurück (401).
POST /webhooks/mta-mailbox
Eingehende Zustellung persönlicher E-Mail (Postbox) vom MTA — zu unterscheiden vom Bounce-/Beschwerde-Callback unter POST /webhooks/mta. Er akzeptiert das Ereignis inbound.mailbox.received, verlangt, dass der Zeitstempel innerhalb von 300 Sekunden zur aktuellen Zeit liegt, und leitet die geparste Nachricht in die Postbox-Zustellpipeline weiter. Siehe Postbox-Architektur.
POST /webhooks/mta-verify-credential
Verifizierung von App-Passwörtern für IMAP und die SMTP-Einlieferung. Der MTA-/IMAP-Server postet { address, password, scope: "imap" | "smtp" }; dadurch kann er Clients authentifizieren, ohne den Convex-Admin-Key vorzuhalten. Das Zeitstempelfenster ist hier enger — 60 Sekunden. Bei einer Übereinstimmung liefert er { ok: true, mailboxId, appPasswordId, userId, organizationId }, andernfalls { ok: false }. Siehe Postbox-Architektur.
Auf dieser Seite geht es um Webhooks, die Owlat empfängt. Für die Webhooks, die Owlat an Ihre Endpunkte sendet (Zustellereignisse, Kontakt- und Themenänderungen), siehe Webhooks und den Contract der Webhook-Payloads.