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.

Nur eingehend

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:

  1. Nicht-POST ablehnen mit 405.
  2. Rate-Limiting nach Client-IP (webhookIngestion: Token-Bucket, 50/Sek., Burst-Kapazität 100). Über dem Limit wird 429 mit einem Retry-After-Header zurückgeliefert.
  3. Signatur verifizieren über den Provider-Adapter. Fail-closed — siehe Erforderliche Env-Secrets.
  4. Rohes Payload revisionssicher speichern (Best-Effort; lässt die Anfrage nie scheitern).
  5. Payload parsen in ein normalisiertes channel.received-Ereignis. Ein Parse-Fehler liefert 400; ein Payload ohne verwertbare Nachricht (z. B. ein Meta-Statusupdate) wird mit 200 { "success": true, "ignored": true } quittiert.
  6. Ereignis dispatchen. Der Dispatcher (apps/api/convex/webhooks/dispatcher.ts) leitet channel.received an die interne Mutation processInboundChannel weiter, die den Kontakt auflöst oder anlegt, den Konversations-Thread findet oder wieder öffnet und eine eingehende Zeile in unifiedMessages einfü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.

Basis-URL

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.

EigenschaftWert
MethodePOST
Content-Typeapplication/x-www-form-urlencoded (Twilio-Standard)
Signatur-HeaderX-Twilio-Signature
SignaturverfahrenHMAC-SHA1, Base64-kodiert
SecretAuth Token des SMS-Kanals, sonst TWILIO_AUTH_TOKEN
ErfolgsantwortLeeres 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:

FormularfeldZugeordnet zu
FromAbsenderkennung (erforderlich)
BodyNachrichtentext (erforderlich)
MessageSidexternalMessageId
MediaUrl0erste Medien-URL (optional)
FromCity, FromState, FromCountryals 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>
Kein Replay-Schutz

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

EigenschaftWert
MethodePOST
Content-Typeapplication/json
Signatur-HeaderX-Hub-Signature-256
SignaturverfahrenHMAC-SHA256 über den rohen Body, hex-kodiert, mit Präfix sha256=
SecretApp Secret des WhatsApp-Kanals, sonst META_APP_SECRET
Erfolgsantwort200 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]:

QuellfeldZugeordnet zu
messages[0].fromAbsenderkennung (erforderlich)
messages[0].text.bodyNachrichtentext
messages[0].idexternalMessageId
messages[0].image.url oder document.urlMedien-URL (optional)
contacts[0].profile.nameals 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_TOKEN gesetzt, liefert er 503.
  • Ist hub.mode gleich subscribe und stimmt hub.verify_token mit dem konfigurierten Token überein (Vergleich in konstanter Zeit), gibt er hub.challenge mit 200 zurü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.

EigenschaftWert
MethodePOST
Content-Typeapplication/json
Auth-HeaderX-Webhook-Secret, oder Authorization: Bearer <secret>
Auth-VerfahrenZeichenkettenvergleich in konstanter Zeit
SecretSecret Key des Generic-Webhook-Kanals, sonst GENERIC_WEBHOOK_SECRET
Erfolgsantwort200 { "success": true, "kind": "channel.received" }

Der JSON-Envelope ist nachsichtig — Felder greifen auf mehrere Aliase zurück:

Envelope-FeldAliase / FallbackZugeordnet zu
fromsender, sonst wörtlich "webhook"Absenderkennung
textmessage, content.textNachrichtentext
htmlcontent.htmlHTML-Body (optional)
subjectcontent.subjectBetreff (optional)
idmessageIdexternalMessageId (optional)
metadataObjekt 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.

EndpunktKanal-ZugangsdatenEnv-FallbackBeides fehlt →Falsche/fehlende Signatur →
POST /webhooks/smsSMS Auth TokenTWILIO_AUTH_TOKEN503401
POST /webhooks/whatsappWhatsApp App SecretMETA_APP_SECRET503401
GET /webhooks/whatsappWhatsApp Verify TokenMETA_VERIFY_TOKEN503403
POST /webhooks/channelGeneric Secret KeyGENERIC_WEBHOOK_SECRET503401

Übliche Statuscodes in der gesamten Kanal-Pipeline:

StatusBedeutung
200Angenommen (oder ignored: true bei einem Payload ohne Nachricht)
400Body konnte nicht gelesen oder geparst werden
401Fehlende oder ungültige Signatur / fehlendes Shared Secret
403Meta-Verifizierungs-Challenge fehlgeschlagen (nur GET)
405Andere Methode als POST (Kanal-Endpunkte)
429Rate-limitiert; beachten Sie Retry-After
503Endpunkt 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.

Ausgehende Webhooks sind separat

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.

Verwandte Themen