n8n e WhatsApp: costruire un agente AI pronto per la produzione

Agente AI WhatsApp con n8n pronto per la produzione: webhook firmato, 200 immediato, niente duplicati, nodo AI Agent con memoria, retry e passaggio a un operatore.

Un grafo di blocchi arrotondati collegati da fili, con il percorso attivo che brilla di verde e termina in un telefono.

In breve

  • Un agente AI WhatsApp in n8n richiede solo un nodo Webhook, un nodo Code che verifica la firma, un nodo AI Agent con memoria e un nodo HTTP Request che invia la risposta.
  • Attiva l'opzione Raw Body del nodo Webhook, perché la firma copre i byte esatti inviati da whasp e un body JSON serializzato di nuovo può non superare la verifica.
  • Rispondi 200 subito dopo la verifica, scarta i duplicati con un nodo Remove Duplicates basato sull'id dell'evento e invia le risposte con un Idempotency-Key, così un retry non invia mai due volte.
  • In produzione usa Postgres Chat Memory o un'altra memoria su database, perché Simple Memory vive nella memoria del processo di n8n e dimentica una chat dopo un'ora di silenzio.
  • Il passaggio a un operatore vive in n8n, non in whasp: tieni un flag per chat in una Data table e controllalo prima di ogni chiamata all'AI.

Per costruire un agente AI WhatsApp con n8n, punta un nodo Webhook sugli eventi in arrivo dell’agente, verifica la firma in un nodo Code, rispondi subito 200, scarta gli eventi duplicati, esegui un nodo AI Agent con un modello di chat e una memoria legata al telefono del mittente, e invia la risposta con un nodo HTTP Request. Questa guida usa whasp per il lato WhatsApp ed è stata provata su n8n 2.43.8.

Perché non usare il nodo WhatsApp di n8n?

Il nodo WhatsApp Business Cloud integrato in n8n invia messaggi, template e contenuti multimediali tramite la WhatsApp Business Platform ufficiale di Meta. Dal 15 gennaio 2026 i termini di quella piattaforma escludono gli assistenti AI generici, come spiega l’articolo sul divieto dei chatbot AI su WhatsApp, e le altre opzioni hanno ciascuna i propri compromessi. Questa guida usa un numero gestito da whasp. A n8n non serve un nodo speciale: whasp invia gli eventi a un URL e accetta le risposte con una semplice API HTTPS, quindi bastano i nodi Webhook e HTTP Request.

La catena ha otto passaggi: Webhook, Verify signature (Code), Respond to Webhook, Text message? (If), Remove Duplicates, Is STOP? (If), AI Agent con il suo modello e la sua memoria, Send reply (HTTP Request). Il 19 ottobre 2026 abbiamo eseguito tutto tranne il nodo Postgres su n8n 2.43.8, con il modello linguistico e l’API di whasp simulati.

Ti servono n8n raggiungibile via HTTPS pubblico (dietro un reverse proxy, imposta N8N_WEBHOOK_URL), un numero whasp, una chiave API e una credenziale per il modello.

Come ricevi gli eventi di whasp in n8n?

Aggiungi un nodo Webhook e imposta:

  • HTTP Method: POST. Path: whasp.
  • Respond: Using ‘Respond to Webhook’ Node.
  • Options, Raw Body: on.

Il nodo Webhook ha un URL di test e un URL di produzione. L’URL di test (/webhook-test/whasp) funziona mentre clicchi Listen for Test Event. L’URL di produzione (/webhook/whasp) funziona solo dopo aver pubblicato il workflow. Registra con whasp l’URL di produzione. La risposta include il segreto di firma (whsec_...), che salvi come WHASP_WEBHOOK_SECRET:

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 conta. whasp firma i byte esatti che invia, secondo lo schema Standard Webhooks. Con l’opzione attiva, il nodo conserva quei byte in binary.data accanto al body già elaborato. Serializzare di nuovo il JSON elaborato può cambiare gli spazi o l’ordine delle chiavi e far fallire il controllo.

Chiama POST /v1/webhook/test per inviare un evento webhook.test e vederlo arrivare.

Come verifichi la firma in un nodo Code?

Aggiungi un nodo Code chiamato Verify signature, mode Run Once for All Items, language JavaScript:

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'];

// I byte esatti firmati da whasp: l'opzione Raw Body del nodo Webhook li conserva 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 minuti
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,   // il numero whasp che l'ha ricevuto
    chat: m.chat,       // il telefono del mittente, E.164
    name: m.sender_name ?? null,
    text: m.text ?? '',
  },
}];

Il codice segue la regola di whasp: HMAC-SHA256 su <id>.<timestamp>.<raw body>, con chiave la parte del segreto dopo whsec_ decodificata in base64, e un timestamp non più vecchio di cinque minuti. Restituisce valid: false invece di lanciare un errore. Un errore prima del nodo Respond to Webhook fa rispondere 500 a n8n, e whasp ritenterebbe l’evento fino a 12 volte. Meglio un 401 pulito.

Due impostazioni fanno inciampare molte persone su n8n 2.x self-hosted, ed entrambe falliscono con un messaggio chiaro. Da n8n 2.0 il nodo Code blocca per impostazione predefinita le variabili d’ambiente, e senza N8N_BLOCK_ENV_ACCESS_IN_NODE=false questo nodo fallisce con «access to env vars denied». Sono bloccati anche i moduli integrati: senza NODE_FUNCTION_ALLOW_BUILTIN=crypto ottieni «Module ‘crypto’ is disallowed». Con i task runner esterni, questa seconda variabile va nella configurazione del runner stesso. Su n8n Cloud, crypto è consentito. Se non puoi impostare variabili d’ambiente, crea una variabile personalizzata dove il tuo piano le prevede e leggi invece $vars.WHASP_WEBHOOK_SECRET.

Come rispondi subito e scarti i duplicati?

whasp considera consegnato qualsiasi 2xx entro 10 secondi. Altrimenti ritenta dopo 30 secondi, 2 minuti, 10 minuti, 30 minuti, poi ogni ora, fino a 12 volte. Gli eventi possono arrivare due volte e fuori ordine. Se ne occupano tre nodi.

  1. Respond to Webhook. Respond With: No Data. Response Code: {{ $json.valid ? 200 : 401 }}. Dopo aver risposto, il nodo passa avanti il suo input, quindi il workflow prosegue mentre whasp ha già la sua risposta.
  2. Text message? (If). Continua solo quando valid è true, type è uguale a message.received e text non è vuoto. Tutto il resto (message.sent, number.status_changed, una foto senza testo) finisce nel ramo false. Collega lì message.failed e number.status_changed a un avviso.
  3. Remove Duplicates. Operation: Remove Items Processed in Previous Executions. Keep Items Where: Value Is New. Value to Dedupe On: {{ $json.eventId }}. La cronologia predefinita è di 10.000 elementi.

Abbiamo inviato due volte lo stesso evento firmato e una volta un evento manomesso: il primo ha ricevuto una risposta, il duplicato un 200 e nessuna seconda esecuzione, quello manomesso un 401. Un limite: una volta risposto 200, un crash dentro n8n fa perdere quell’evento, perché whasp non lo reinvierà. Se è un problema, scrivi ogni evento in una tabella o in una coda prima di confermare. Webhook WhatsApp: verifica della firma, retry e duplicati tratta questo schema.

Come configuri il nodo AI Agent, la memoria e la risposta?

AI Agent. Source for Prompt (User Message): Define below. Prompt (User Message): {{ $json.text }}. Sotto Options, System Message, scrivi chi è l’agente e come deve scrivere, per esempio «Sei Ava, un’assistente amichevole che scrive su WhatsApp. Solo testo semplice, niente Markdown. Fai una domanda alla volta.» Per un prompt più completo, vedi come far scrivere un agente AI su WhatsApp come una persona. La risposta dell’agente arriva in $json.output.

Chat model. Collega un OpenAI Chat Model o un Anthropic Chat Model. Entrambi caricano l’elenco dei modelli dal tuo account, quindi scegli lì un modello attuale.

Memory. Collega Postgres Chat Memory. Imposta Session ID su Define below e Key su {{ $json.number }}:{{ $json.chat }}, così ogni persona ha una sola cronologia per numero. La tabella predefinita è n8n_chat_histories e Context Window Length vale 5 per impostazione predefinita: alzalo quanto consente il budget del tuo prompt. Evita Simple Memory in produzione. Il suo codice sorgente tiene le sessioni in una mappa dentro il processo di n8n ed elimina ogni sessione inattiva da un’ora, e la sua documentazione dice che non funziona in queue mode. I server di whasp non conservano i messaggi, quindi la cronologia è tua, e un database che controlli è la sede giusta. Memoria della conversazione per un agente AI su WhatsApp approfondisce il tema.

Send reply. Aggiungi un nodo HTTP Request:

Campo Valore
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 on, Max Tries 3, Wait Between Tries 2000 ms

Costruisci il body come un’espressione che restituisce un oggetto. Incollare il testo del modello in una stringa JSON si rompe alla prima virgoletta o al primo a capo. La chiave di idempotenza rende sicuri i retry: la stessa chiave restituisce il primo messaggio e non invia mai due volte. n8n consente al massimo 5 tentativi e 5 secondi tra uno e l’altro, quindi copre i brevi intoppi, non i blackout. whasp risponde 202 e invia in ordine i messaggi di un numero, a circa un secondo di distanza l’uno dall’altro.

Come gestisci STOP, errori e raffiche di messaggi?

whasp disiscrive da solo una chat quando la persona invia STOP, UNSUBSCRIBE, OPTOUT, CANCEL, END, QUIT o una qualsiasi grafia di «opt out», e gli invii successivi a quella chat restituiscono 403 chat_opted_out. Il messaggio arriva comunque al tuo webhook, quindi aggiungi un nodo If Is STOP? dopo Remove Duplicates. Imposta il valore con l’espressione {{ /^(STOP|UNSUBSCRIBE|OPTOUT|CANCEL|END|QUIT)$/.test($json.text) || /^opt[\s_-]*out$/i.test($json.text) }} e la condizione su Boolean, is true. Nel ramo true, salta il modello e invia un solo HTTP Request con "override_optout": true e una riga come «Disiscrizione confermata. Scrivi quando vuoi per ricominciare.» Le richieste educate come «per favore smetti» spettano a te.

Per gli errori, apri Workflow Settings, imposta un Error workflow e costruiscilo con un nodo Error Trigger che scrive su Slack o via email. Leggi bene i 429: rate_limited significa più di 100 richieste API in 10 secondi per account, e number_daily_limit significa che il numero ha inviato 1.500 messaggi in 24 ore. Un 409 number_unavailable significa che il numero è bannato, offline o ritirato.

Le raffiche richiedono una decisione. Una persona che manda tre messaggi in cinque secondi avvia tre esecuzioni e riceve tre risposte. Chi costruisce workflow sul forum di n8n consiglia di accorpare in un solo turno i messaggi ravvicinati di un cliente prima di chiamare il modello. Questa parte non l’abbiamo costruita.

Come passi una chat a un operatore?

whasp fa solo da canale di passaggio: non ha una inbox né una funzione di presa in carico, quindi lo stato del passaggio vive in n8n. Un design praticabile usa una Data table chiamata chat_state con le colonne chat e handoff. Questa parte non l’abbiamo eseguita, ma le operazioni dei nodi sono documentate.

  1. Dopo Remove Duplicates, aggiungi un nodo Data table: Row, If Row Exists, con chat uguale a {{ $json.chat }} e handoff true. Il ramo true inoltra il messaggio a Slack o al tuo helpdesk e si ferma. Il ramo false prosegue verso l’agente.
  2. Imposta il flag con un Upsert row della Data table quando il testo corrisponde a una regola come /operatore|persona vera|umano/i, o quando lo chiede il tuo agente. Poi invia «Una persona ti risponderà qui a breve.»
  3. Costruisci un secondo workflow che parte quando lo staff risponde, chiama POST /v1/messages con la stessa chat e rimette handoff a false con un upsert quando arriva un comando di ripresa.

Controlla di nuovo il flag subito prima di inviare la risposta del modello, perché un passaggio può iniziare mentre il modello sta ancora elaborando.

Cosa fare adesso

Pubblica il workflow, registra il suo URL di produzione con PUT /v1/webhook, invia POST /v1/webhook/test, poi scrivi al tuo numero dal tuo telefono. Se non arriva nulla, GET /v1/webhook/deliveries mostra ogni tentativo e il suo stato. Il quickstart percorre l’API passo dopo passo. Per lo stesso flusso nel codice, vedi Collegare WhatsApp a ChatGPT: agente OpenAI in TypeScript.

Domande

Come collego n8n a WhatsApp per un agente AI?

Ricevi i messaggi in arrivo con un nodo Webhook, esegui un nodo AI Agent e invia la risposta con un nodo HTTP Request che chiama POST https://api.whasp.ai/v1/messages. Il nodo WhatsApp Business Cloud di n8n usa l'API ufficiale di Meta, che non è adatta a un assistente generico.

Perché il mio nodo Code di n8n dice che il modulo crypto non è consentito?

Dalla versione n8n 2.0 il nodo Code gira su task runner e blocca per impostazione predefinita i moduli integrati. Su n8n self-hosted imposta NODE_FUNCTION_ALLOW_BUILTIN=crypto dove il task runner legge il suo ambiente. n8n Cloud consente già crypto.

Perché il mio nodo Code di n8n dice access to env vars denied?

n8n 2.0 blocca per impostazione predefinita le variabili d'ambiente nel nodo Code. Su n8n self-hosted imposta N8N_BLOCK_ENV_ACCESS_IN_NODE=false, oppure salva il segreto come variabile personalizzata e leggilo con $vars.

Come evito che n8n risponda due volte allo stesso messaggio WhatsApp?

Metti dopo il Webhook un nodo Remove Duplicates, impostato per rimuovere gli elementi elaborati nelle esecuzioni precedenti, e deduplica sull'id dell'evento preso dall'header webhook-id. Poi invia la risposta con lo stesso id come Idempotency-Key.

Quale nodo di memoria di n8n devo usare per un agente WhatsApp?

Usa Postgres Chat Memory o Redis Chat Memory, con il numero di telefono del mittente come chiave di sessione. Simple Memory sta nel processo, viene scartata dopo un'ora senza uso e non funziona in queue mode.

Tutti gli articoli