Consulenza informatica

Attribuzione lead WhatsApp nel CRM

Davide Caio
Davide Caio
11 Agosto 2026 · 7 min di lettura
Ultimo aggiornamento il 5 Agosto 2026
da whatsapp al crm

C’è un buco nell’attribuzione che quasi tutte le aziende che fanno campagne su WhatsApp hanno, e quasi nessuna ha risolto.

Investi in inserzioni click-to-WhatsApp. L’utente clicca, si apre la chat con un messaggio precompilato, scrive. Il messaggio arriva nella casella condivisa del CRM, il commerciale risponde, magari nasce una trattativa. Tutto bene, tranne che nel CRM quel contatto ha il campo “provenienza” vuoto.

Maia Management

Manager senior a ore per la tua PMI.

IT Manager, Marketing Manager, CDO, Odoo, HubSpot. Solo professionisti senior — 0 junior nel team. Risposta entro 24h.

A fine trimestre qualcuno chiede quanto ha reso la campagna, e la risposta onesta è che non si sa. I contatti ci sono, i ricavi anche, ma il collegamento tra i due è andato perso nel momento esatto in cui il messaggio è arrivato.

Ecco come abbiamo chiuso quel buco per un cliente, il CRM in questione è HubSpot, e cosa abbiamo scoperto sul funzionamento reale delle API lungo la strada.

Il vincolo di partenza: non esiste uno storico

Prima cosa da mettere in chiaro, perché condiziona tutta l’architettura: le WhatsApp Business API non espongono un endpoint per rileggere i messaggi ricevuti. Né quelle native di Meta, né i provider che ci stanno sopra. I messaggi in ingresso arrivano solo in push, via webhook, nell’istante in cui vengono ricevuti. Non puoi fare una GET “dammi i messaggi di oggi”.

Questo significa che, se il tuo aggancio è il provider WhatsApp, il sistema deve essere event-driven: se non catturi il messaggio quando arriva, l’informazione è persa per sempre.

Ma c’è una seconda strada, spesso trascurata: se i messaggi WhatsApp confluiscono già dentro il CRM, come succede quando la casella di posta condivisa è collegata al numero aziendale, allora i messaggi sono già archiviati lì. E il CRM, a differenza di WhatsApp, un’API di lettura ce l’ha.

È la strada che abbiamo scelto, per un motivo pratico: non richiede di toccare la configurazione del provider WhatsApp, non introduce un nuovo punto di rottura, e funziona anche in retroattivo sui messaggi già arrivati.

L’architettura

Un workflow schedulato, non un webhook. Nel nostro caso gira su un’istanza n8n self-hosted. Ogni ora:

  1. Trova le conversazioni aggiornate nelle ultime 48 ore
  2. Filtra solo quelle del canale WhatsApp, deduplicando i contatti
  3. Legge i contatti in batch e scarta quelli che hanno già una provenienza
  4. Per i rimanenti, legge i messaggi del thread
  5. Cerca la frase della campagna nel primo messaggio in entrata
  6. Scrive la provenienza sul contatto

La finestra di 48 ore con esecuzione oraria dà un margine di sicurezza abbondante: anche se il workflow resta fermo mezza giornata, al riavvio recupera tutto senza buchi.

Le tre cose che l’API fa in modo diverso da come è documentata

Qui sta il valore vero di questo articolo, perché sono tre comportamenti che si scoprono solo sbattendoci contro.

1. L’endpoint stabile ignora i filtri, in silenzio

L’endpoint v3 della Conversations API di HubSpot accetta parametri come il filtro sul timestamp dell’ultimo messaggio, l’ordinamento e il filtro sul canale di origine. Li accetta nel senso letterale del termine: non restituisce nessun errore. Semplicemente li ignora, e ti risponde con i thread in ordine crescente a partire dal più vecchio della storia del portale.

È il tipo di bug che ti fa perdere un’ora, perché la risposta è HTTP 200 e i dati sembrano validi. Sono solo i dati sbagliati.

La soluzione è usare l’endpoint in versione beta, che quei parametri li valida davvero. Con una regola precisa: il filtro sul timestamp funziona solo se accompagnato dal parametro di ordinamento corrispondente. Passato da solo viene respinto.

2. I timestamp devono essere in UTC, non in ora locale

Passando un timestamp con offset locale, quello che in Italia diventa +02:00, l’API risponde con un errore di parsing. Il motivo è banale e insidioso: il segno + in una query string viene interpretato come uno spazio codificato. L’offset arriva dall’altra parte come 02:00 e la data non è più valida.

Due accortezze risolvono definitivamente:

  • convertire sempre in UTC prima di inviare;
  • usare il pannello dei parametri di query del client HTTP invece di concatenare i valori dentro la stringa dell’URL, così la codifica la gestisce il client.

In n8n, l’espressione corretta è:

{{ $now.minus({ hours: 48 }).toUTC().toISO() }}

Nota a margine: Date.now() non funziona nei campi espressione di n8n, va usato $now.toMillis(). Dentro un nodo Code invece funziona regolarmente. È una differenza che non è documentata da nessuna parte.

3. La lettura in batch nasconde i contatti cancellati

Per non fare una chiamata per contatto, si usa l’endpoint di lettura in batch. Qui c’è la trappola più costosa delle tre.

I contatti trovati tornano nell’array results. I contatti non trovati — perché nel frattempo sono stati cancellati, non tornano affatto in results: finiscono in un array separato errors, con sottocategoria OBJECT_NOT_FOUND.

La conseguenza è che un filtro scritto in logica di esclusione (“passa avanti tutti i contatti che non hanno già la provenienza”) lascia passare anche i contatti inesistenti, perché di loro non sai nulla e quindi il campo risulta vuoto. Il workflow poi tenta di aggiornarli, fallisce, e ogni ciclo riprova.

La forma corretta del filtro è in inclusione: passano avanti solo i contatti esplicitamente trovati in results e con il campo provenienza vuoto. Tutto il resto viene scartato.

const risultati = $json.results ?? [];
const daAggiornare = risultati.filter(c =>
  !c.properties.provenienza_lead ||
  c.properties.provenienza_lead.trim() === ''
);
return daAggiornare.map(c => ({ json: c }));

Leggere il messaggio giusto

I messaggi di un thread tornano in ordine cronologico decrescente: il più recente per primo. Il primo messaggio della conversazione — quello che contiene la frase della campagna — è quindi l’ultimo dell’array.

Ma non basta prendere l’ultimo elemento, perché nell’array non ci sono solo messaggi. Ci sono anche oggetti di tipo cambio di stato del thread e assegnazione a un operatore. Vanno filtrati prima:

const messaggi = ($json.results ?? []).filter(m =>
  m.type === 'MESSAGE' && m.direction === 'INCOMING'
);
const primoMessaggio = messaggi[messaggi.length - 1];
const testo = primoMessaggio?.text ?? '';

Sul matching della frase, un consiglio: usa una regex tollerante, non un confronto esatto. L’utente può modificare il messaggio precompilato prima di inviarlo, aggiungere un saluto, correggere un refuso. Cercare una sottostringa caratteristica funziona molto meglio che cercare la frase intera.

L’errore che costa più di tutti: l’idempotenza

Questo vale per qualunque workflow di polling, non solo per questo.

Nella prima versione filtravamo i contatti su un campo e scrivevamo il risultato su un campo diverso. Sembra ragionevole: leggi la provenienza principale, scrivi quella secondaria.

Il risultato è che il campo di controllo resta vuoto per sempre. Ogni ora il contatto rientra nel filtro, viene riprocessato, riaggiornato. Il valore finale è corretto, quindi nessuno se ne accorge ma stai bruciando chiamate API a ogni ciclo su contatti già sistemati, e dopo qualche settimana inizi a toccare i rate limit senza capire perché.

Regola: in un workflow di polling, il campo su cui filtri e il campo su cui scrivi devono essere lo stesso. Se ti servono più campi, scrivili tutti, ma il filtro deve leggere uno di quelli scritti.

Un’alternativa più solida, se puoi averla

Il matching testuale ha un limite intrinseco: se l’utente cancella il messaggio precompilato e scrive di suo pugno, l’attribuzione salta.

I webhook nativi di Meta per le inserzioni click-to-WhatsApp includono un oggetto referral con l’ID dell’inserzione, il tipo di sorgente, il titolo dell’annuncio e un identificativo di clic. È infinitamente più affidabile: identifica la campagna anche senza il messaggio precompilato.

Il problema è che non tutti i provider intermedi inoltrano quel campo nel proprio payload, e quasi nessuno lo documenta. Vale un test reale prima di scegliere la strada: clicca sulla tua inserzione, manda un messaggio, e guarda il payload grezzo che arriva. Se il referral c’è, usa quello e dimenticati le regex.

Cosa cambia in azienda

Il risultato tecnico è un campo popolato su un contatto. Il risultato pratico è un altro.

Il commerciale che apre la scheda vede da quale campagna arriva la persona, e apre la conversazione con il contesto giusto invece che con “buongiorno, come posso aiutarla?”. Il marketing, a fine mese, ha un numero di lead per campagna che non è una stima. E la domanda “quanto ci ha reso quell’investimento” ha finalmente una risposta che non richiede di andare a leggere le chat a mano.

È uno dei tanti punti in cui un CRM configurato bene fa la differenza tra dati e stime: è il tipo di lavoro che facciamo nella consulenza HubSpot per PMI, e che abbiamo raccontato anche in altri casi reali di automazione.


I vostri lead WhatsApp arrivano nel CRM senza sapere da dove. È un problema che si risolve in pochi giorni di lavoro e che continua a rendere ogni mese. Scriveteci.

Davide Caio
Pubblicato da
Davide Caio

Davide Caio è CTO e Ingegnere Informatico con 15 anni di esperienza in sviluppo software, implementazione ERP e architettura di sistemi digitali per PMI italiane. Specializzato in Odoo, Unity 3D, N8N e soluzioni AI per l'industria manifatturiera. Guida tutte le implementazioni tecnologiche di Maia Management e segue personalmente i progetti Odoo più complessi. Ha completato oltre 22 implementazioni Odoo nel 2025. Contattaci →

Hai bisogno di supporto per la tua azienda?

Prenota una videocall gratuita con i nostri consulenti senior. Nessun impegno, solo risposte concrete.

Esplora altri articoli su Consulenza informatica Tutte le guide →

Vuoi parlare con un esperto?

Prenota una videocall gratuita. Ti rispondiamo entro 24h.

Parliamo del tuo progetto