Discord nennt zwei Dinge „Webhooks", und sie verhalten sich völlig unterschiedlich. Das eine ist eine Fire-and-forget-URL zum Posten von Nachrichten. Das andere ist ein Interaction-Endpoint, der signierte POSTs empfängt, sobald ein Nutzer einen Slash-Command ausführt. Das erste läuft auf localhost. Das zweite nie. In diesem Leitfaden geht es um das zweite — denn das braucht einen Tunnel.
Die zwei Discord-Webhook-Varianten
Ausgehende Webhooks sind URLs, an die du POSTest. Du schickst eine JSON-Nachricht, sie erscheint in einem Channel. Kein Tunnel nötig, da der Traffic von deinem Rechner nach außen fließt.
Interaction-Endpoints laufen in die Gegenrichtung. Du registrierst eine URL bei Discord; Discord POSTet daran, sobald ein Nutzer /deinbefehl ausführt. Der Endpoint muss HTTPS sein, innerhalb von drei Sekunden antworten und bei jeder Anfrage eine Ed25519-Signatur prüfen. Localhost kann nichts davon allein.
Warum Interaction-Endpoints strenger sind als die meisten
Discord will nicht nur HTTPS — es verlangt, dass du vor der Annahme der URL nachweist, dass du den Endpoint kontrollierst. Der Prüfablauf passiert bei der Registrierung: Discord schickt eine PING-Interaction, dein Server muss mit einem signierten PONG antworten. Schlägt die Ed25519-Prüfung auch nur einmal fehl, weigert sich Discord, die URL zu speichern.
Den Ed25519-Teil bekommst du beim ersten Versuch nicht hin. Fast niemand schafft das.
Ed25519-Prüfung, der Teil, der beißt
Discord nutzt Ed25519 (nicht HMAC SHA-256) und liefert zwei Header bei jeder Interaction:
X-Signature-Ed25519— die Signatur in HexX-Signature-Timestamp— der Timestamp in Sekunden
Du signierst timestamp + rawBody mit dem Public Key, den Discord dir im Developer-Portal zeigt. Die meisten Sprachen haben dafür eine Bibliothek. In Node:
import nacl from 'tweetnacl';
const valid = nacl.sign.detached.verify(
Buffer.from(timestamp + rawBody),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex'),
);
Ursache Nummer eins für fehlgeschlagene Prüfungen ist — wieder — Middleware, die den Body parst, bevor dieser Code läuft. Du brauchst den rohen String, den Discord geschickt hat, nicht eine neu serialisierte Version des geparsten Objekts.
Schritt für Schritt: Interaction-Endpoint auf localhost
- Starte den HTTP-Server deines Bots lokal (beliebiger Port).
- Führe
npx portpreview 3000aus, um eine öffentliche HTTPS-URL zu erhalten. - Öffne im Discord-Developer-Portal deine Anwendung und füge die Tunnel-URL samt Interactions-Pfad in Interactions Endpoint URL ein.
- Klicke Save. Discord feuert sofort ein
PING. Funktioniert deine Ed25519-Prüfung, wird die URL gespeichert. Sonst zeigt das Feld einen Fehler. - Ist sie gespeichert, führe einen Slash-Command in einem Server aus, in dem dein Bot ist. Die Anfrage landet in deinem Handler.
Schlägt das Speichern wiederholt fehl, inspiziere die erfasste Anfrage — Header, Body und die Antwort deines Servers. In neun von zehn Fällen wurde der Body still von einer JSON-Middleware vor der Prüfung verändert.
Drei-Sekunden-Frist
Discord verlangt eine Antwort in unter drei Sekunden, sonst gilt die Interaction als fehlgeschlagen. Für alles Längere schicke eine erste Typ-5-Antwort (deferred) und PATCHe die ursprüngliche Interaction später mit der echten Antwort. Lokales Testen macht offensichtlich, welche Handler zu langsam sind — die Request-Erfassung zeigt dir die exakte Latenz.
Ausgehende Webhooks sind weiterhin nützlich zum Testen
Willst du nur Nachrichten aus lokalem Code an einen Channel senden, funktioniert die ausgehende Webhook-URL ohne Tunnel. JSON POSTen, Nachricht erscheint. Wir nutzen das für Log-Alerts aus lokalen Dev-Läufen.
Wann du diesem Setup entwächst
Für einen einzelnen Bot mit einer Handvoll Befehle reicht lokales Testen mit Tunnel völlig. Pflegst du einen öffentlichen Bot für Millionen Nutzer, willst du irgendwann ein Staging-Deployment mit stabiler URL, damit Discord nicht jedes Mal neu prüfen muss, wenn die Tunnel-Session rotiert.
Discords Drei-Sekunden-Timeout ist dieselbe Problemfamilie wie GitHubs Zehn-Sekunden-Fenster. Lies unsere Sicht auf Webhook-Retries und Idempotenz für die ausführliche Version. Tritt der PortPreview-Warteliste bei für Tunnel + Erfassung in einer CLI.