# n8n WhatsApp KI-Agent: Produktions-Setup Schritt für Schritt

> So baust du einen n8n WhatsApp KI-Agenten für die Produktion: signierter Webhook, sofortiges 200, Duplikatschutz, AI Agent mit Memory, Retries und Übergabe an Menschen.

Source: https://whasp.ai/de/blog/n8n-whatsapp-ki-agent-produktion · 2026-10-20

## Das Wichtigste

- Ein n8n WhatsApp KI-Agent braucht nur einen Webhook-Node, einen Code-Node, der die Signatur prüft, einen AI-Agent-Node mit Memory und einen HTTP-Request-Node, der die Antwort sendet.
- Schalte beim Webhook-Node die Option Raw Body ein, denn die Signatur deckt genau die Bytes ab, die whasp gesendet hat, und ein neu serialisierter JSON-Body kann die Prüfung nicht bestehen.
- Antworte direkt nach der Prüfung mit 200, entferne Duplikate mit einem Remove-Duplicates-Node auf Basis der Event-ID und sende Antworten mit einem Idempotency-Key, damit ein Retry nie doppelt sendet.
- Nimm in der Produktion Postgres Chat Memory oder ein anderes datenbankgestütztes Memory, denn Simple Memory liegt im Arbeitsspeicher des n8n-Prozesses und vergisst einen Chat nach einer Stunde Stille.
- Die Übergabe an einen Menschen liegt in n8n, nicht in whasp: Führe pro Chat ein Flag in einer Data table und prüfe es vor jedem KI-Aufruf.

Um einen n8n WhatsApp KI-Agenten zu bauen, richtest du einen Webhook-Node auf die eingehenden Events des Agenten, prüfst die Signatur in einem Code-Node, antwortest sofort mit 200, verwirfst doppelte Events, lässt einen AI-Agent-Node mit Chatmodell und Memory laufen (Schlüssel ist die Telefonnummer des Absenders) und sendest die Antwort mit einem HTTP-Request-Node. Diese Anleitung nutzt whasp für die WhatsApp-Seite und lief auf n8n 2.43.8.

## Warum nicht den WhatsApp-Node von n8n nutzen?

Der eingebaute [WhatsApp-Business-Cloud-Node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.whatsapp/) von n8n sendet Nachrichten, Templates und Medien über Metas offizielle WhatsApp Business Platform. Seit dem 15. Januar 2026 schließen die Bedingungen dieser Plattform allgemeine KI-Assistenten aus, wie [WhatsApp KI-Chatbot-Verbot 2026](https://whasp.ai/de/blog/whatsapp-ki-chatbot-verbot-2026) erklärt, und [die anderen Optionen](https://whasp.ai/de/blog/ki-agent-whatsapp-optionen-vergleich) bringen jeweils eigene Abwägungen mit. Diese Anleitung nutzt eine Nummer, die whasp betreibt. n8n braucht dafür keinen besonderen Node: whasp sendet Events an eine URL und nimmt Antworten über eine einfache HTTPS-API entgegen, der Webhook- und der HTTP-Request-Node genügen also.

Die Kette hat acht Schritte: Webhook, Verify signature (Code), Respond to Webhook, Text message? (If), Remove Duplicates, Is STOP? (If), AI Agent mit seinem Modell und Memory, Send reply (HTTP Request). Wir haben am 19. Oktober 2026 alles außer dem Postgres-Node auf n8n 2.43.8 ausgeführt, mit gemocktem Sprachmodell und gemockter whasp API.

Dein n8n muss über öffentliches HTTPS erreichbar sein (hinter einem Reverse Proxy setzt du `N8N_WEBHOOK_URL`). Dazu brauchst du eine whasp-Nummer, einen API-Key und ein Modell-Credential.

## Wie empfängst du whasp-Events in n8n?

Füge einen Webhook-Node hinzu und stelle ein:

- HTTP Method: POST. Path: `whasp`.
- Respond: Using 'Respond to Webhook' Node.
- Options, Raw Body: an.

Der [Webhook-Node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/) hat eine Test-URL und eine Produktions-URL. Die Test-URL (`/webhook-test/whasp`) funktioniert, solange du auf Listen for Test Event klickst. Die Produktions-URL (`/webhook/whasp`) funktioniert erst, nachdem du den Workflow veröffentlicht hast. Registriere die Produktions-URL bei whasp. Die Antwort enthält das Signing Secret (`whsec_...`), das du als `WHASP_WEBHOOK_SECRET` speicherst:

```bash
curl -X PUT https://api.whasp.ai/v1/webhook \
  -H "Authorization: Bearer $WHASP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://n8n.example.com/webhook/whasp"}'
```

Raw Body ist wichtig. whasp signiert genau die Bytes, die es sendet, nach dem Schema von Standard Webhooks. Mit eingeschalteter Option behält der Node diese Bytes in `binary.data` neben dem geparsten `body`. Wenn du das geparste JSON neu serialisierst, können sich Leerraum oder Schlüsselreihenfolge ändern, und die Prüfung schlägt fehl.

Rufe `POST /v1/webhook/test` auf, um ein `webhook.test`-Event zu senden und zu beobachten, wie es ankommt.

## Wie prüfst du die Signatur in einem Code-Node?

Füge einen Code-Node mit dem Namen `Verify signature` hinzu, Mode Run Once for All Items, Sprache JavaScript:

```js
const crypto = require('crypto');

if (!$env.WHASP_WEBHOOK_SECRET) throw new Error('Set WHASP_WEBHOOK_SECRET (the whsec_ secret from whasp)');

const headers = $input.first().json.headers;
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];

// Genau die Bytes, die whasp signiert hat: Die Option Raw Body des Webhook-Nodes hält sie in binary.data.
const raw = await this.helpers.getBinaryDataBuffer(0, 'data');

const key = Buffer.from($env.WHASP_WEBHOOK_SECRET.replace(/^whsec_/, ''), 'base64');
const expected = crypto.createHmac('sha256', key)
  .update(`${id}.${timestamp}.`)
  .update(raw)
  .digest();

const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300; // 5 Minuten
const signed = String(headers['webhook-signature'] ?? '').split(' ').some((part) => {
  const [version, sig] = part.split(',');
  if (version !== 'v1' || !sig) return false;
  const given = Buffer.from(sig, 'base64');
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});

if (!id || !timestamp || !fresh || !signed) return [{ json: { valid: false } }];

const event = JSON.parse(raw.toString('utf8'));
const m = event.data?.message ?? {};
return [{
  json: {
    valid: true,
    eventId: id,
    type: event.type,
    number: m.number,   // die whasp-Nummer, die die Nachricht empfangen hat
    chat: m.chat,       // das Telefon des Absenders, E.164
    name: m.sender_name ?? null,
    text: m.text ?? '',
  },
}];
```

Der Code folgt der Regel von whasp: HMAC-SHA256 über `<id>.<timestamp>.<raw body>`, mit dem base64-dekodierten Teil des Secrets nach `whsec_` als Schlüssel und einem Zeitstempel, der höchstens fünf Minuten alt ist. Er gibt `valid: false` zurück, statt einen Fehler zu werfen. Ein Fehler vor dem Respond-to-Webhook-Node lässt n8n mit 500 antworten, und whasp würde das Event bis zu 12-mal wiederholen. Ein sauberes 401 ist besser.

Zwei Einstellungen bringen bei selbst gehostetem n8n 2.x viele ins Stolpern, und beide scheitern mit einer klaren Meldung. Seit [n8n 2.0](https://docs.n8n.io/2-0-breaking-changes/) blockiert der Code-Node Umgebungsvariablen standardmäßig, und ohne `N8N_BLOCK_ENV_ACCESS_IN_NODE=false` scheitert dieser Node mit „access to env vars denied“. Auch eingebaute Module sind blockiert: Ohne `NODE_FUNCTION_ALLOW_BUILTIN=crypto` erscheint „Module 'crypto' is disallowed“. Bei externen Task Runnern gehört die zweite Variable in die eigene Konfiguration des Runners. In der n8n Cloud ist [`crypto` erlaubt](https://docs.n8n.io/code/code-node/). Wenn du keine Umgebungsvariablen setzen kannst, lege dort, wo dein Tarif sie bietet, eine Custom Variable an und lies stattdessen `$vars.WHASP_WEBHOOK_SECRET`.

## Wie antwortest du schnell und überspringst Duplikate?

whasp wertet jede 2xx-Antwort innerhalb von 10 Sekunden als zugestellt. Andernfalls wiederholt es nach 30 Sekunden, 2 Minuten, 10 Minuten, 30 Minuten und dann stündlich, bis zu 12-mal. Events können doppelt und in falscher Reihenfolge ankommen. Drei Nodes kümmern sich darum.

1. **Respond to Webhook.** Respond With: No Data. Response Code: `{{ $json.valid ? 200 : 401 }}`. Der Node reicht seine Eingabe nach der Antwort weiter, der Workflow läuft also weiter, während whasp seine Antwort schon hat.
2. **Text message? (If).** Mach nur weiter, wenn `valid` true ist, `type` gleich `message.received` ist und `text` nicht leer ist. Alles andere (`message.sent`, `number.status_changed`, ein Foto ohne Text) fällt in den false-Zweig. Verbinde dort `message.failed` und `number.status_changed` mit einem Alarm.
3. **Remove Duplicates.** Operation: Remove Items Processed in Previous Executions. Keep Items Where: Value Is New. Value to Dedupe On: `{{ $json.eventId }}`. Die Standardhistorie umfasst 10.000 Einträge.

Wir haben dasselbe signierte Event zweimal und ein manipuliertes einmal gesendet: Das erste bekam eine Antwort, das Duplikat bekam ein 200 und keinen zweiten Lauf, das manipulierte bekam ein 401. Eine Grenze gibt es: Hast du einmal mit 200 geantwortet, geht das Event bei einem Absturz in n8n verloren, weil whasp es nicht erneut sendet. Wenn dir das wichtig ist, schreibe jedes Event vor der Bestätigung in eine Tabelle oder Queue. [WhatsApp-Webhooks verarbeiten: Signatur, Retries, Duplikate](https://whasp.ai/de/blog/whatsapp-webhook-signatur-retries-duplikate) behandelt das Muster.

## Wie richtest du AI Agent, Memory und Antwort ein?

**AI Agent.** Source for Prompt (User Message): Define below. Prompt (User Message): `{{ $json.text }}`. Schreibe unter Options, System Message, wer der Agent ist und wie er schreiben soll, zum Beispiel „Du bist Ava, eine freundliche Assistentin, die auf WhatsApp schreibt. Nur Klartext, kein Markdown. Stell immer nur eine Frage auf einmal.“ Für einen ausführlicheren Prompt siehe [KI-Agent auf WhatsApp wie ein Mensch schreiben lassen](https://whasp.ai/de/blog/ki-agent-whatsapp-menschlich-schreiben). Die Antwort des Agenten kommt in `$json.output` an.

**Chat Model.** Hänge ein OpenAI Chat Model oder ein Anthropic Chat Model an. Beide laden die Modellliste aus deinem Account, wähle dort also ein aktuelles Modell.

**Memory.** Hänge Postgres Chat Memory an. Setze Session ID auf Define below und Key auf `{{ $json.number }}:{{ $json.chat }}`, sodass jede Person pro Nummer einen eigenen Verlauf hat. Die Standardtabelle heißt `n8n_chat_histories`, und Context Window Length steht standardmäßig auf 5, erhöhe den Wert also so weit, wie dein Prompt-Budget es erlaubt. Vermeide Simple Memory in der Produktion. Sein [Quellcode](https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts) hält Sessions in einer Map im n8n-Prozess und löscht jede Session, die eine Stunde ungenutzt bleibt, und [seine Doku](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorybufferwindow/) sagt, dass er im Queue Mode nicht funktioniert. Die Server von whasp speichern keine Nachrichten, der Verlauf gehört also dir, und eine Datenbank, die du selbst kontrollierst, ist das richtige Zuhause dafür. [WhatsApp-KI-Agent mit Konversationsgedächtnis: Verlauf, Fakten](https://whasp.ai/de/blog/whatsapp-ki-agent-gedaechtnis) geht tiefer.

**Send reply.** Füge einen HTTP-Request-Node hinzu:

| Feld | Wert |
| --- | --- |
| Method, URL | POST, `https://api.whasp.ai/v1/messages` |
| Authentication | Generic Credential Type, Header Auth: Name `Authorization`, Value `Bearer wsk_...` |
| Send Headers | `Idempotency-Key`: `{{ $('Verify signature').first().json.eventId }}` |
| Send Body, JSON, Using JSON | `{{ { to: $('Verify signature').first().json.chat, text: $json.output } }}` |
| Settings | Retry On Fail an, Max Tries 3, Wait Between Tries 2000 ms |

Baue den Body als Ausdruck, der ein Objekt zurückgibt. Wenn du Modelltext in einen JSON-String klebst, bricht das beim ersten Anführungszeichen oder Zeilenumbruch. Der Idempotency-Key macht Wiederholungen sicher: Derselbe Key liefert die erste Nachricht zurück und sendet nie doppelt. n8n erlaubt höchstens 5 Versuche mit höchstens 5 Sekunden dazwischen, es deckt also kurze Aussetzer ab, keine Ausfälle. whasp antwortet mit 202 und sendet die Nachrichten einer Nummer der Reihe nach, im Abstand von etwa einer Sekunde.

## Wie gehst du mit STOP, Fehlern und Nachrichtenschüben um?

whasp meldet einen Chat selbst ab, wenn die Person STOP, UNSUBSCRIBE, OPTOUT, CANCEL, END, QUIT oder irgendeine Schreibweise von „opt out“ sendet, und spätere Sendungen an diesen Chat liefern 403 `chat_opted_out`. Die Nachricht erreicht trotzdem deinen Webhook, also füge nach Remove Duplicates einen If-Node namens **Is STOP?** hinzu. Setze den Wert auf den Ausdruck `{{ /^(STOP|UNSUBSCRIBE|OPTOUT|CANCEL|END|QUIT)$/.test($json.text) || /^opt[\s_-]*out$/i.test($json.text) }}` und die Bedingung auf Boolean, is true. Im true-Zweig überspringst du das Modell und sendest einen einzelnen HTTP Request mit `"override_optout": true` und einer Zeile wie „Du hast dich abgemeldet. Schreib jederzeit wieder, wenn du weiterreden möchtest.“ Höfliche Bitten wie „bitte hör auf“ musst du selbst abfangen.

Für Fehler öffnest du Workflow Settings, setzt einen Error workflow und baust ihn aus einem Error-Trigger-Node, der in Slack oder per E-Mail meldet. Achte auf die 429-Antworten: `rate_limited` heißt mehr als 100 API-Anfragen in 10 Sekunden pro Account, und `number_daily_limit` heißt, dass die Nummer 1.500 Nachrichten in 24 Stunden gesendet hat. Ein 409 `number_unavailable` heißt, dass die Nummer gesperrt, offline oder stillgelegt ist.

Nachrichtenschübe verlangen eine Entscheidung. Eine Person, die in fünf Sekunden drei Nachrichten schickt, startet drei Läufe und bekommt drei Antworten. Entwickler im n8n-Forum empfehlen, [schnell aufeinanderfolgende Nachrichten eines Kunden per Debouncing zu einem Turn zusammenzufassen](https://community.n8n.io/t/title-building-a-production-ready-whatsapp-automation-with-n8n-architecture-security-rate-limiting-retries-and-human-handoff/322131), bevor das Modell aufgerufen wird. Diesen Teil haben wir nicht gebaut.

## Wie übergibst du einen Chat an einen Menschen?

whasp ist eine Leitung: Es hat keinen Posteingang und keine Übernahme, der Zustand der Übergabe liegt also in n8n. Ein brauchbarer Entwurf nutzt eine [Data table](https://docs.n8n.io/data/data-tables/) namens `chat_state` mit den Spalten `chat` und `handoff`. Diesen Teil haben wir nicht ausgeführt, aber die Operationen der Nodes sind dokumentiert.

1. Füge nach Remove Duplicates einen Data-table-Node hinzu: Row, If Row Exists, mit `chat` gleich `{{ $json.chat }}` und `handoff` true. Der true-Zweig leitet die Nachricht an Slack oder dein Helpdesk weiter und endet. Der false-Zweig geht weiter zum Agenten.
2. Setze das Flag mit einem Data-table-Upsert-row, wenn der Text zu einer Regel wie `/mensch|echte person/i` passt oder wenn dein Agent die Übergabe verlangt. Sende dann „Bald antwortet dir hier ein Mensch.“
3. Baue einen zweiten Workflow, der auslöst, wenn Mitarbeiter antworten, `POST /v1/messages` mit demselben Chat aufruft und `handoff` bei einem Fortsetzungsbefehl per Upsert wieder auf false setzt.

Prüfe das Flag direkt vor dem Senden der Modellantwort noch einmal, denn eine Übergabe kann beginnen, während das Modell noch denkt.

## Was du als Nächstes tun kannst

Veröffentliche den Workflow, registriere seine Produktions-URL mit `PUT /v1/webhook`, sende `POST /v1/webhook/test` und schreibe dann von deinem eigenen Smartphone an deine Nummer. Kommt nichts an, zeigt `GET /v1/webhook/deliveries` jeden Versuch mit Status. Der [Quickstart](https://whasp.ai/docs#quickstart) führt durch die API. Für denselben Ablauf in Code siehe [ChatGPT mit WhatsApp verbinden: KI-Agent mit OpenAI API bauen](https://whasp.ai/de/blog/chatgpt-whatsapp-verbinden-openai-api).

## Fragen

### Wie verbinde ich n8n mit WhatsApp für einen KI-Agenten?

Empfange eingehende Nachrichten mit einem Webhook-Node, lass einen AI-Agent-Node laufen und sende die Antwort mit einem HTTP-Request-Node, der POST https://api.whasp.ai/v1/messages aufruft. Der eigene WhatsApp-Business-Cloud-Node von n8n nutzt Metas offizielle API, die für einen allgemeinen Assistenten nicht passt.

### Warum meldet mein n8n-Code-Node, dass das Modul crypto nicht erlaubt ist?

Seit n8n 2.0 läuft der Code-Node auf Task Runnern und blockiert eingebaute Module standardmäßig. Setze bei selbst gehostetem n8n NODE_FUNCTION_ALLOW_BUILTIN=crypto dort, wo der Task Runner seine Umgebung liest. Die n8n Cloud erlaubt crypto bereits.

### Warum meldet mein n8n-Code-Node access to env vars denied?

n8n 2.0 blockiert Umgebungsvariablen im Code-Node standardmäßig. Setze bei selbst gehostetem n8n N8N_BLOCK_ENV_ACCESS_IN_NODE=false oder speichere das Secret als Custom Variable und lies es mit $vars.

### Wie verhindere ich, dass n8n dieselbe WhatsApp-Nachricht zweimal beantwortet?

Setze nach dem Webhook einen Remove-Duplicates-Node ein, der Einträge aus früheren Ausführungen entfernt, und dedupliziere über die Event-ID aus dem Header webhook-id. Sende dann die Antwort mit derselben ID als Idempotency-Key.

### Welchen n8n-Memory-Node sollte ein WhatsApp-Agent nutzen?

Nimm Postgres Chat Memory oder Redis Chat Memory, mit der Telefonnummer des Absenders als Session-Key. Simple Memory läuft im Prozess, wird nach einer Stunde ohne Nutzung verworfen und funktioniert im Queue Mode nicht.
