Costruisci un agente WhatsApp
con OpenClaw + TimelinesAI
Trasforma WhatsApp in un canale cliente che il tuo agente OpenClaw opera: risponde ai messaggi in arrivo, manda notifiche transazionali e attivate da eventi, sincronizza col tuo CRM, condivide la inbox con il team. TimelinesAI gestisce il gateway WhatsApp. Il tuo agente gestisce il ragionamento.
Auto-rispostaInvii transazionaliSync CRMInbox condivisa
Cosa può fare il tuo agente con WhatsApp
Con uno skill TimelinesAI installato, il tuo agente OpenClaw può operare WhatsApp come un vero canale cliente. La versione lunga di questa lista è nelle sezioni qua sotto — ogni capacità ha le chiamate API specifiche di cui ha bisogno.
Rispondere automaticamente ai messaggi in arrivo dei clienti — autoresponder 24/7, risponditore fuori orario o un chatbot completo che escalation a un umano quando si blocca.
Inviare messaggi transazionali e attivati da eventi — conferme d'ordine, aggiornamenti di spedizione, promemoria di appuntamento, ricevute di pagamento e notifiche da altri strumenti (HubSpot, Stripe, Calendly). Solo a clienti che si aspettano il messaggio.
Qualificare lead con conversazioni multi-turno — fare una sequenza di domande, salvare le risposte, taggare la chat come qualificata o no.
Sincronizzare l'attività WhatsApp con il tuo CRM — cercare i numeri in arrivo in HubSpot/Pipedrive, aggiornare gli stage del deal, riscrivere note.
Riassumere e valutare conversazioni — «cosa ha chiesto ACME la settimana scorsa», «dai un punteggio 1–10 a questa chat per intent».
Condividere la inbox con il team — il tuo agente bozza risposte come note private e gli umani le inviano; oppure l'agente invia e gli umani guardano.
Gestire i media — foto di scontrini, PDF, note vocali — elaborati da OpenClaw, risposti automaticamente.
Come usare questa guida
Questa guida è dual-mode. Puoi leggerla tu stesso e seguire il Setup manualmente — il percorso che le sezioni qui sotto stendono — oppure puoi passarla intera al tuo agente OpenClaw (o a qualsiasi agente che possa leggere un URL) e lasciare che faccia il setup al posto tuo. Entrambi i percorsi arrivano allo stesso posto: quattro skill WhatsApp installati, token e webhook collegati, il tuo agente che risponde ai messaggi dei clienti.
Se la stai leggendo tu stesso
Scorri i gruppi di capability qui sotto — Incoming, Outbound, CRM & analytics, Operations — e decidi quali contano per il tuo caso d'uso. Poi segui il Setup manualmente; quattro passi, circa dieci minuti. Ogni capability elenca le chiamate API esatte che fa, e ogni code block in questa guida è stato eseguito contro l'API TimelinesAI reale prima della pubblicazione. Se ti blocchi, Cose da sapere raccoglie ogni gotcha che ti costerebbe un'ora se non lo sapessi in anticipo.
Se vuoi che il tuo agente faccia il setup
Incolla questo prompt in OpenClaw (o Claude Code, Cursor, Claude desktop, qualsiasi agente che possa leggere un URL). Punta a questa guida via URL così che l'agente legga la versione live, poi ti accompagna attraverso install, token, webhook, smoke-test e l'abilitazione del tuo primo skill — chiedendoti input solo dove ha davvero bisogno di qualcosa da te.
Read https://timelines.ai/guide/openclaw-whatsapp-skills end to end.
Then help me set up an OpenClaw + TimelinesAI WhatsApp agent:
1. Clone InitechSoftware/openclaw-whatsapp-skills into ~/.openclaw/workspace/
and symlink the four skills into ~/.openclaw/skills/.
2. Ask me for my TimelinesAI API token (Integrations → Public API → Copy
on app.timelines.ai) and write it to ~/.openclaw/workspace/.env.timelinesai
as TIMELINES_AI_API_KEY.
3. Run the smoke-test curl from Setup. If it fails, walk me through the
"If you see something else" failure block in the guide.
4. Help me deploy examples/vercel-webhook-receiver from the companion repo
and register the webhook with TimelinesAI.
5. Ask which of the four skills I want to enable first — whatsapp-autoresponder,
whatsapp-lead-qualifier, whatsapp-send, or whatsapp-delivery-check — and
walk me through its capability section plus any relevant gotchas from
Things-to-know.
6. Before enabling anything that sends outbound messages, show me Channel
choice so I pick the right WhatsApp channel (personal number vs Business
API) for my use case.Il prompt si ferma intenzionalmente prima di accendere qualsiasi invio in uscita — ti instrada prima attraverso Scelta del canale così scegli il canale WhatsApp giusto (numero personale vs Business API) per quello che stai davvero cercando di fare. Il rischio di ban è reale e vive upstream, non è qualcosa da cui il gateway possa proteggerti.
Setup
Quattro cose da collegare. Dopo, il tuo skill fa solo chiamate API.
- 1
Connetti il tuo numero WhatsApp
Accedi a app.timelines.ai, scansiona il QR con il telefono che ha il tuo numero aziendale. TimelinesAI gestisce il gateway da qui in poi. Stesso flusso del setup standard di numero personale.
- 2
Prendi un token API
Integrazioni → API Pubblica → Copia. Salva come TIMELINES_AI_API_KEY. Un solo token copre tutto il workspace.
- 3
Installa uno skill dal repo compagno
Clona InitechSoftware/openclaw-whatsapp-skills in ~/.openclaw/workspace/, poi fai un symlink di ogni directory di skill in ~/.openclaw/skills/. Quickstart di quattro righe nel README del repo compagno.
- 4
Registra un webhook
Punta message:received:new su una URL HTTPS pubblica. TimelinesAI ci pusherà i messaggi in arrivo. Il tuo receiver invoca OpenClaw.
Smoke-test del token
Prima di costruire qualunque cosa, conferma che l'auth funzioni:
curl -sS -w "\nHTTP: %{http_code}\n" \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/whatsapp_accountsDovresti vedere
{
"status": "ok",
"data": {
"whatsapp_accounts": [
{
"id": "<phone>@s.whatsapp.net",
"phone": "+<phone>",
"status": "connected",
"account_name": "<your label>"
}
]
}
}Se vedi qualcos'altro
- HTTP 401 col testo puro Unauthorized — il token non è arrivato. Ricontrolla il passo 2, assicurati di aver incluso il prefisso Bearer e che il token non si sia spezzato in più righe incollando. La risposta è testo puro, non JSON — fare pipe su jq darà errore.
- Una pagina HTML in stile bootstrap “Page not found” con HTTP 404 — hai uno slash finale o un errore di battitura nel path. L'API restituisce HTML (non un errore JSON) per path errati, quindi se il tuo output inizia con <!DOCTYPE html>, togli ogni slash finale e ricontrolla il path.
Registra il webhook una volta
# Generate a random secret once. Add it as a query param so only
# TimelinesAI's registered URL can reach your receiver.
WEBHOOK_SECRET=$(openssl rand -hex 16)
WEBHOOK_URL="https://your-app.example.com/api/webhook?secret=${WEBHOOK_SECRET}"
curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"event_type\":\"message:received:new\",\"url\":\"${WEBHOOK_URL}\",\"enabled\":true}" \
https://app.timelines.ai/integrations/api/webhooksReceiver webhook minimo (Node / Vercel)
~40 righe. Valida il query param ?secret=, scarta gli echo in uscita, risponde 2xx entro la finestra di retry di 5 secondi, poi inoltra il messaggio al tuo host OpenClaw.
// api/webhook.js
export default async function handler(req, res) {
// Path-segment auth — TimelinesAI doesn't sign webhooks yet, so the
// ?secret=<token> you registered is the only thing stopping strangers
// from invoking this endpoint.
if (req.query.secret !== process.env.WEBHOOK_SECRET) {
return res.status(404).end();
}
if (req.method !== "POST") return res.status(405).end();
const { event_type, data } = req.body || {};
if (event_type !== "message:received:new") {
return res.status(200).json({ ignored: event_type });
}
// Drop your agent's own sends echoing back as "received".
// TimelinesAI also fires message:received:new for outbound messages
// synced from another WhatsApp client on the same number.
if (data.from_me === true) {
return res.status(200).json({ ignored: "from_me" });
}
// Ack FAST. TimelinesAI retries 3x with a 5-second timeout per attempt —
// if you take longer than 5s the same event hits you up to 3 times.
res.status(200).json({ ok: true });
// TL webhook payloads sometimes arrive flat (data.chat_id) and sometimes
// nested (data.chat.id) depending on event flavor and version. Destructure
// with fallback so both shapes work — see Things to know below.
const chatId = data.chat_id ?? data.chat?.id;
const whatsappAccountId =
data.whatsapp_account_id ?? data.chat?.whatsapp_account_id;
// Fire-and-forget handoff to wherever your OpenClaw host accepts
// inbound messages. For production durability replace this with a
// push to a queue (QStash, Inngest, SQS) your agent drains separately.
try {
await fetch(process.env.OPENCLAW_HOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: chatId,
whatsapp_account_id: whatsappAccountId,
text: data.text,
sender_phone: data.sender_phone,
message_uid: data.message_uid,
}),
});
} catch (err) {
console.error("[timelinesai-webhook] handoff failed:", err);
}
}OPENCLAW_HOOK_URL è l'endpoint che il tuo deployment OpenClaw espone per i messaggi in arrivo — dipende da come ospiti l'agente. Per un riferimento production-ready con note sulla durabilità, vedi examples/vercel-webhook-receiver nel repo compagno.
Cosa c'è dentro il tuo workspace OpenClaw
Hai connesso un token di TimelinesAI e fatto symlink degli skill dal repo compagno. Ecco cosa hai davvero installato, e quali file modifichi quando vuoi cambiare come il tuo agente pensa, cosa sa e come si comporta. Niente di magico qui — è tutto markdown semplice e file env in due directory.
Skills — ~/.openclaw/skills/<skill>/
Ogni skill che hai symlinkato è una directory con un file obbligatorio (SKILL.md) più le risorse che questo referenzia. Se vuoi cambiare come suona il tuo FAQ handler o riordinare le domande del lead qualifier, la modifica vive nel SKILL.md di quello skill. Nessun codice da ricompilare, nessun build step.
| File | Cosa controlla |
|---|---|
| SKILL.md | L'intero skill. YAML frontmatter in cima (name, description, user-invocable e qualsiasi flag di dispatch) più il body con le istruzioni/prompt sotto. Qui modifichi cosa fa lo skill, quando OpenClaw lo sceglie e come si comporta. Ognuno dei quattro skill di WhatsApp del repo compagno è un singolo SKILL.md — nessun codice da ricompilare, basta modificare e riavviare. |
| README.md | Documentazione umana dello skill. OpenClaw NON legge questo file — è per te, il tuo team e chiunque sfogli lo skill su GitHub. Opzionale; gli skill del repo compagno ne hanno tutti uno. |
| Scripts, resources, schemas | Qualsiasi cosa referenziata da SKILL.md via {baseDir}/... — piccoli script helper, template di prompt, schemi JSON, fixture di test. Ignorato da OpenClaw a meno che il tuo SKILL.md non ci punti esplicitamente. Metti ciò di cui lo skill ha bisogno accanto a SKILL.md e referenzialo per path. |
Per i quattro skill di WhatsApp — whatsapp-autoresponder, whatsapp-lead-qualifier, whatsapp-send, whatsapp-delivery-check — in cima a ciascun SKILL.md c'è il role e le tool consentite; il body ha il prompt vero con cui il tuo agente gira. Leggine uno end-to-end prima di modificare; i pattern si ripetono.
Workspace — ~/.openclaw/workspace/
La directory workspace è la casa di OpenClaw per tutto ciò che non è uno skill: chi è il tuo agente, cosa sa di te, quali tool ha, come si avvia. I docs di OpenClaw dicono “questa cartella è casa. Trattala come tale”. Questi file arrivano con l'install — li compili col tempo:
| File | Cosa controlla |
|---|---|
| IDENTITY.md | Chi è il tuo agente — nome, creature (come si concettualizza), vibe, emoji signature, avatar. Compilalo presto; la maggior parte degli altri file lo referenziano. |
| SOUL.md | Principi operativi e personalità core. Come l'agente deve comportarsi quando nessuno guarda — quando essere genuino vs performativo, come guadagnare fiducia, dove stanno i limiti di privacy. I docs di OpenClaw lo chiamano la coscienza dell'agente. |
| USER.md | Chi sta aiutando l'agente (tu). Nome, pronomi, timezone, interessi, contesto di progetto. Gli agenti sono migliori ad aiutare qualcuno di specifico che un “user” generico — questo file è come se ne ricordano. |
| TOOLS.md | Config specifica dell'ambiente che non vuoi dentro nessuno skill — nomi di dispositivi, indirizzi host, preferenze locali. Vive nel workspace così puoi modificarlo senza toccare nulla che potresti condividere. |
| AGENTS.md | Il README del workspace per gli agenti. Descrive come OpenClaw si aspetta che questo workspace venga eseguito. Di solito arriva con l'install e viene raramente modificato. |
| BOOT.md | Istruzioni brevi ed esplicite su cosa OpenClaw dovrebbe fare all'avvio. Vuoto di default; riempilo se vuoi comportamento di boot deterministico tra riavvii. |
| BOOTSTRAP.md | Conversazione di onboarding del primo boot. Guida un workspace nuovo a stabilire la sua identità, poi ti dirige a salvare i risultati in IDENTITY.md, SOUL.md, USER.md. Eliminalo una volta che hai riempito quegli altri. |
| HEARTBEAT.md | Definizioni di task periodici. File vuoto significa nessun heartbeat; aggiungi task qui quando vuoi che l'agente controlli qualcosa a intervallo (es. poll una queue ogni cinque minuti). |
File env per integrazione
Accanto ai file canonici sopra, aggiungi un file env per ogni integrazione che connetti. Questi NON fanno parte dell'install OpenClaw — sono tuoi, e contengono segreti che non dovrebbero mai essere committati in un repo pubblico:
| File | Cosa controlla |
|---|---|
| .env.<integration> | Segreti e config per un'integrazione, un file per integrazione. Per il lavoro su WhatsApp hai creato .env.timelinesai con TIMELINES_AI_API_KEY e ALLOWED_SENDER_JID nel Setup passo 2. Replica questo pattern quando aggiungi HubSpot, Stripe, Pipedrive o qualunque altra tool — un .env.<name> e lo source nello skill che ne ha bisogno. |
Numeri personali vs WhatsApp Business API
Prima di costruire flussi in uscita, capisci a quale canale WhatsApp appartiene il tuo caso d'uso. Scegliere male fa bannare i numeri.
Sicuro con gli skill di questa guida (numero personale)
- •Conversazioni in arrivo. Il cliente ti scrive per primo, tu rispondi. Nessun rischio.
- •Invii transazionali. Conferme d'ordine, aggiornamenti di spedizione, notifiche di consegna, ricevute di pagamento, promemoria di appuntamento — qualsiasi messaggio che un cliente si aspetta perché ha appena fatto qualcosa col tuo business.
- •Notifiche attivate da eventi in altri strumenti. Deal HubSpot → conferma demo, pagamento Stripe fallito → nota di recovery, prenotazione Calendly → promemoria pre-meeting. Il cliente ha optato quando ha usato lo strumento upstream.
- •Rispondere dentro la finestra di customer service di 24 ore di WhatsApp. Una volta che un cliente ti scrive, hai 24 ore per rispondere liberamente. Gli skill autoresponder, FAQ e qualifier operano dentro questa finestra.
NON sicuro su numeri personali
- •Cold outreach a liste che hai comprato o scrapato — WhatsApp banna i numeri per questo nel giro di ore.
- •Broadcast di marketing a clienti che non hanno optato esplicitamente.
- •Campagne promozionali, offerte di vendita, push stagionali, lanci di prodotto.
- •Qualsiasi cosa che assomigli a un blast di marketing. Se ti stai chiedendo «quanto throughput posso ottenere», sei sul canale sbagliato.
Come capire quale canale ti serve
- 1.Il cliente ti ha scritto per primo, o stai rispondendo dentro una sessione attiva di 24 ore? → Numero personale, questa guida.
- 2.Il cliente sta per ricevere qualcosa che si aspetta esplicitamente (ordine, appuntamento, pagamento, consegna)? → Numero personale, invio transazionale.
- 3.Innescato da un evento opt-in del cliente nel tuo CRM o strumento di billing? → Numero personale, invio attivato da evento.
- 4.Broadcast, cold outreach o campagna promozionale? → Business API, usa il dashboard di TimelinesAI.
- 5.Non sicuro? → Non inviare. Trattalo come promozionale e indirizzalo sul percorso Business API.
Ogni capacità in uscita qua sotto presume che tu abbia passato questo test. Se non sei sicuro, rileggi questa sezione prima di shippare. TimelinesAI non può proteggerti dal ban a livello WhatsApp — il ban viene applicato a monte, nell'infrastruttura WhatsApp, non nel gateway.
Riferimento API
Ogni endpoint che ti serve per costruire ogni capacità qua sotto. URL base https://app.timelines.ai/integrations/api. Auth Authorization: Bearer $TIMELINES_AI_API_KEY.
Lettura
| Metodo | Path | Cosa restituisce |
|---|---|---|
| GET | /whatsapp_accounts | I tuoi numeri WhatsApp connessi, ognuno con JID, telefono, status e nome account. |
| GET | /chats | Lista delle chat. Supporta filtri ?phone=... e ?label=.... Ogni chat ha un campo whatsapp_account_id con il JID del numero proprietario. |
| GET | /chats/{id} | Dettaglio completo di una chat. |
| GET | /chats/{id}/messages | Storico messaggi con ?limit=N. Include from_me, sender_phone, text, timestamp, message_type (whatsapp vs note). |
| GET | /chats/{id}/labels | Label della chat. |
| GET | /messages/{uid}/status_history | Timeline Sent / Delivered / Read di un messaggio in uscita. |
| GET | /messages/{uid}/reactions | Reazioni a un messaggio. Restituisce {data: {users: [{name, phone, reaction, current}], reactions: {<emoji>: count}, total: N}} — un oggetto, non un array piatto. users elenca chi ha reagito (ognuno porta l'emoji scelto più un booleano current che marca il tuo workspace); reactions è un istogramma indicizzato per emoji. Stato vuoto: {users: [], reactions: {}, total: 0}. |
| GET | /files | File che hai caricato via API. |
| GET | /webhooks | Le tue iscrizioni webhook registrate. |
Scrittura
| Metodo | Path | Cosa fa |
|---|---|---|
| POST | /messages | Invia a un numero di telefono. Body: {"phone":"+...","text":"..."}. Restituisce {"message_uid":"..."}. |
| POST | /chats/{id}/messages | Invia dentro una chat esistente. Body: {"text":"..."}. Il mittente è il numero WhatsApp proprietario della chat. |
| POST | /chats/{id}/notes | Allega una nota privata a una chat. Non viene inviata a WhatsApp, visibile solo dentro TimelinesAI. Usata per stato dell'agente e workflow di revisione. |
| POST | /chats/{id}/labels | Aggiunge una label alla chat. Per tracking di stage, routing e flag di stop-reply. |
| PATCH | /chats/{id} | Aggiorna i metadati della chat — responsabile (responsible_email), stato di lettura, e chatgpt_autoresponse_enabled. Disattiva l'ultimo prima che il tuo agente inizi a rispondere, o il responder integrato di TL corre contro il tuo. |
| PATCH | /messages/{uid}/reactions | Imposta un emoji di reazione su un messaggio. Il body prende il carattere emoji letterale, non uno shortcode. |
| POST | /files_upload | Carica un file via multipart/form-data (campo: file). Restituisce data.uid, che passi a chat/messages come file_uid. Nessuna variante upload-per-URL. |
| POST | /webhooks | Registra un'iscrizione webhook. |
| PUT | /webhooks/{id} | Aggiorna o abilita/disabilita un'iscrizione. |
| DELETE | /webhooks/{id} | Rimuove un'iscrizione. |
Cose da sapere
Alcuni dettagli facili da perdere nella reference che ti costeranno un'ora ciascuno se non li conosci.
Quando le chiamate API falliscono
Cinque modalità di errore prima o poi colpiranno il tuo skill. Ognuna si vede diversa nella risposta e ognuna ha una risposta corretta diversa.
Un piccolo pattern shell che ramifica per ogni status code:
$ # Capture the HTTP status into a variable, then branch
$ http_code=$(curl -sS -o /tmp/resp.json -w "%{http_code}" \
-X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/send.json \
https://app.timelines.ai/integrations/api/messages)
$ case "$http_code" in
200|201) ;; # ok, carry on
400) echo "bad body: $(cat /tmp/resp.json)"; exit 1 ;;
401) echo "token expired - rotate"; exit 2 ;;
429) sleep 30; retry_once ;; # respect Retry-After
5*) sleep $((2 ** attempt)); retry_with_backoff ;;
esacOperare in sicurezza in produzione
Altre due cose che non sono fallimenti, ma ti risparmieranno una brutta giornata più avanti.
Contesto della conversazione
Il webhook consegna al tuo skill un solo messaggio — quello appena arrivato. Nessun turno precedente, nessuna cronologia di chat, nessuna trascrizione in corso. Per un FAQ senza stato o un risponditore fuori orario basta e avanza. Per un agente conversazionale che deve ricordare cosa ha detto il cliente tre turni prima, è lo skill a dover recuperare il contesto da solo. Tre pattern coprono l'intera gamma, dal loop più economico possibile a quello che si ricorda davvero.
Senza stato
Rispondi solo all'ultimo messaggio. Un POST per turno, nessun GET, nessuna cronologia. È il loop più semplice e l'impostazione predefinita negli skill pronti. Usalo per bot FAQ, risponditori fuori orario e classificazione o routing — ovunque la risposta dipenda solo dal messaggio corrente. L'agente dimentica tutto tra un turno e l'altro: se il cliente scrive un follow-up che fa riferimento a qualcosa detto prima, non lo capirà.
Finestra di contesto completa
Recupera gli ultimi 20 messaggi prima di ogni risposta e passali al modello come conversazione precedente. Due chiamate API per turno invece di una, più i token extra nel prompt a ogni chiamata. Usala per agenti conversazionali, qualificazione multi-turno e ovunque l'agente debba ricordare il filo. Venti turni recenti di solito bastano — andare più in largo aiuta raramente e rende il prompt costoso; andare più stretto fa dimenticare all'agente cosa ha detto il cliente un minuto fa.
Contesto adattivo
Recupera il contesto solo quando il messaggio in arrivo sembra un follow-up. Un'euristica economica sul testo — inizia con un pronome, riferimento a “quello” o “esso”, arriva entro 30 secondi dalla tua risposta precedente, acknowledgment di una parola sola — decide se tirare la cronologia o rispondere senza stato. La maggior parte dei turni resta economica; i follow-up ottengono memoria. Parti dalla finestra completa e passa all'adattivo solo dopo aver conosciuto il tuo budget di costo e aver traffico reale in produzione per tarare l'euristica.
Recuperare la finestra è una singola GET. La risposta è un array piatto di messaggi della chat, il più nuovo per ultimo, mescolando turni in entrata del cliente con le tue stesse risposte in uscita e qualsiasi nota che il tuo skill ha scritto:
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
"https://app.timelines.ai/integrations/api/chats/12345678/messages?limit=20"
{"status":"ok","data":[
{"message_uid":"INBOUND-UID-PLACEHOLDER",
"text":"hi, is the blue one still available?",
"sender_phone":"+15550200",
"message_type":"text","origin":"WhatsApp",
"created_at":"2026-04-14T10:22:03Z"},
{"message_uid":"OUTBOUND-UID-PLACEHOLDER",
"text":"Yes, one left. Want me to hold it for you?",
"sender_phone":"",
"message_type":"text","origin":"Public API",
"created_at":"2026-04-14T10:22:41Z"},
{"message_uid":"INBOUND-UID-PLACEHOLDER-2",
"text":"yes please, until tomorrow",
"sender_phone":"+15550200",
"message_type":"text","origin":"WhatsApp",
"created_at":"2026-04-14T10:23:08Z"}
]}Trappole che mordono in produzione
Persistenza dello stato
Gli skill OpenClaw non mantengono stato in memoria tra invocazioni. Le conversazioni WhatsApp sono multi-turno. La soluzione è salvare lo stato sulla chat stessa:
- •Le label tengono lo stage discreto — discovery/q1, qualified, escalate. Aggiungi con POST /chats/{id}/labels, leggi con GET /chats/{id}/labels.
- •Le note tengono dati strutturati — team_size=8, bozze di risposta, score di lead. Aggiungi con POST /chats/{id}/notes. Leggi iterando GET /chats/{id}/messages e filtrando message_type == "note".
Il vantaggio: sicurezza ai crash, visibilità per i compagni umani, handoff pulito — un umano può cancellare una label per riavvolgere il flusso, o aggiungere escalate per prendere il controllo. Il trade-off: ogni transizione di stato è una chiamata HTTP. Per flussi rivolti al cliente va bene.
Inviare dal numero giusto
Se il tuo workspace ha più di un numero WhatsApp connesso, il tuo skill deve assicurarsi di inviare dal numero che intende. Ogni chat ha un campo whatsapp_account_id col JID completo (tipo TELEFONO@s.whatsapp.net) del numero proprietario. Quando fai POST /chats/{id}/messages, il mittente è sempre quel JID — non lo scegli tu, lo sceglie la chat.
Il pattern:
- 1Hardcoda il JID mittente permesso nell'environment di ogni skill (es. ALLOWED_SENDER_JID).
- 2Prima di inviare, GET /chats/{id} e confronta whatsapp_account_id col tuo JID permesso.
- 3Se non combaciano, salta l'invio — escala a un umano o scarta l'evento.
Due chiamate HTTP in più per turno, zero possibilità di inviare dalla persona sbagliata. Per workspace a singolo numero non si applica.
In entrata
Messaggi in arrivo — cosa può gestire il tuo agente
Ogni volta che un cliente scrive al tuo numero WhatsApp, TimelinesAI fa scattare un evento message:received:new verso il tuo webhook con l'id chat, il testo, il telefono del mittente e gli allegati. Il tuo skill legge l'evento, decide cosa fare e risponde con POST /chats/{chat_id}/messages. Tutto qua sotto è una variazione di questo loop.
Rispondere automaticamente a ogni messaggio in arrivo
“Rispondi alle domande su spedizioni, resi e orari. Per tutto il resto, bozza una risposta e tagga la chat per revisione.”
“Tra le 22:00 e le 08:00 rispondi automaticamente. Negli orari d'ufficio segnalami solo le chat in arrivo.”
“Gestisci le mie risposte WhatsApp mentre sono in questa riunione.”
lo skill riceve il payload del webhook, compone una risposta e chiama POST /chats/{id}/messages con {"text":"..."}. Senza stato per impostazione predefinita — una chiamata API per turno. Per conversazioni multi-turno in cui l'agente deve ricordare i messaggi precedenti, vedi Contesto della conversazione sotto.
Gestione FAQ con escalation a un umano
“Rispondi alle domande su spedizioni, resi e orari. Per tutto il resto, tagga la chat needs-human e smetti di rispondere finché non rimuovo il tag.”
prima di rispondere, controlla GET /chats/{id}/labels. Se la chat ha needs-human, esci senza inviare. Se il testo in arrivo combacia con un tema FAQ, rispondi. Altrimenti POST /chats/{id}/labels con il tag di escalation ed esci in silenzio. La inbox del tuo team filtra sul tag.
Indirizzare le conversazioni alla persona giusta
“Per ogni chat in arrivo, deduci se è vendite, supporto o fatturazione e taggala. Assegna le chat di vendita a alex@ours e fatturazione a jamie@ours.”
classifica l'intent dal testo, POST /chats/{id}/labels con intent/sales o simile, poi PATCH /chats/{id} con {"responsible_email":"..."} per consegnare la chat dentro la inbox di TimelinesAI.
Qualificare lead con una sequenza di domande
“Per ogni chat nuova dalla nostra campagna Facebook, chiedi il caso d'uso, le dimensioni del team e le tempistiche. Salva le risposte come note sulla chat. Taggala qualified se il team è di 5 o più.”
le label tracciano a quale domanda sei (discovery/q1, q2, q3); le note tengono le risposte. Ogni turno: leggi la label di stage corrente, parsa il testo in arrivo come risposta, scrivila via POST /chats/{id}/notes, avanza la label, fai la prossima domanda. Nessun database esterno — vedi Persistenza dello stato più sotto.
$ # 1. Read current stage from the chat's labels. Labels nest under
$ # data.labels as a string array — see Response shapes in Things to know.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/chats/12345678/labels
{"status":"ok","data":{"labels":["discovery/q1"]}}
$ # 2. Save the customer's answer as a structured note. Notes are stored
$ # as messages with message_type=note and aren't pushed to WhatsApp.
$ cat > /tmp/note.json <<'JSON'
{"text":"team_size=12"}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/note.json \
https://app.timelines.ai/integrations/api/chats/12345678/notes
{"status":"ok","data":{"message_uid":"NOTE-UID-PLACEHOLDER"}}
$ # 3. Advance the stage label. POST /labels REPLACES the full set, so
$ # read existing labels, swap q1 for q2, POST the combined list. The
$ # body key is "labels" (plural, array) — not "label" singular.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/chats/12345678/labels \
| jq -c '.data.labels | map(if . == "discovery/q1" then "discovery/q2" else . end) | {labels: .}' \
> /tmp/labels.json
$ cat /tmp/labels.json
{"labels":["discovery/q2"]}
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/labels.json \
https://app.timelines.ai/integrations/api/chats/12345678/labels
{"status":"ok","data":{"labels":["discovery/q2"]}}Capire foto, PDF e scontrini
“Quando un cliente manda una foto di uno scontrino, estrai l'importo e il venditore e aggiungili come nota.”
“Se qualcuno manda un PDF, classificalo come fattura / contratto / documento e tagga la chat di conseguenza.”
i payload del webhook includono una URL all'allegato. Scaricala dentro l'handler (scade in fretta), elabora con gli strumenti vision o documento di OpenClaw, e scrivi i dati estratti via POST /chats/{id}/notes.
$ # The attachment URL in the webhook payload expires in ~15 minutes.
$ # Download it inline in the receiver, before queuing async work.
$ ATTACH_URL="https://files.timelines.ai/abc123/receipt.jpg?expires=..."
$ curl -sS "$ATTACH_URL" -o /tmp/receipt.jpg
$ # Process /tmp/receipt.jpg with OpenClaw vision tools, then write
$ # the extracted fields back as a structured note on the chat:
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary '{"text":"receipt: amount=$42.50 vendor=Cafe Madrid"}' \
https://app.timelines.ai/integrations/api/chats/12345678/notesTrascrivere note vocali e rispondere
“Trascrivi le note vocali in arrivo. Rispondi via testo — se ti serve davvero una risposta vocale, mandala dal dashboard di TimelinesAI.”
il webhook consegna una URL al file vocale. Scarica, trascrivi, componi una risposta testo via POST /chats/{id}/messages. Le risposte vocali sono oggi una funzione del dashboard di TimelinesAI e non sono nella reference API pubblica attuale — se il tuo skill deve mandare risposte vocali a livello di codice, contatta il supporto di TimelinesAI per confermare se l'endpoint voice_message legacy è ancora disponibile sul tuo workspace.
Combaciare con la lingua del cliente
“Se il cliente scrive in spagnolo, rispondi in spagnolo. Se cambia lingua a metà conversazione, cambia con lui.”
ragionamento puramente lato OpenClaw sul testo in arrivo. TimelinesAI trasporta solo la risposta.
Reagire ai messaggi senza inviare una risposta completa
“Reagisci con 👀 a ogni messaggio in arrivo così i clienti sanno che l'ho visto, e poi mi prendo il mio tempo per scrivere la risposta vera.”
PATCH /messages/{uid}/reactions con {"reaction":"👀"}. Il campo reaction deve contenere il carattere emoji letterale — shortcode come "eyes" o ":eyes:" vengono rifiutati con HTTP 400 "Reaction has invalid format". Nessun credito di messaggio consumato — le reazioni sono leggere.
$ # Send the raw emoji character in the body. Save to a UTF-8 file and
$ # use --data-binary so shells can't downgrade it.
$ printf '{"reaction":"👀"}' > /tmp/react.json
$ curl -sS -X PATCH \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/react.json \
https://app.timelines.ai/integrations/api/messages/INBOUND-UID-PLACEHOLDER/reactions
{"status":"ok"}In uscita
In uscita — messaggi che il tuo agente inizia
Messaggi che il tuo agente inizia, non risposte. Innescati da un evento nei tuoi altri strumenti (un nuovo ordine, un pagamento fallito, una riunione prenotata) o da un'istruzione umana diretta su una persona specifica.
Prima di ogni capacità di questa sezione: il cliente o ha aperto il thread di recente (dentro la finestra di sessione di 24 ore di WhatsApp) o si aspetta esplicitamente questo messaggio. Se nessuna delle due è vera, non inviare da un numero personale — quello è territorio Business API.
Inviare un messaggio transazionale per nome o a un nuovo destinatario
“Di' a Giovanni che la sua fattura è pronta.”
“Manda all'idraulico il nuovo indirizzo dell'ufficio così consegna i pezzi.”
“Invia il contratto firmato al cliente che ha appena bonificato l'acconto.”
per una chat esistente, cerca via GET /chats?name=Giovanni (o il tuo CRM) e chiama POST /chats/{id}/messages. Per un nuovo destinatario, POST /messages con {"phone":"+...","text":"..."}. Lo skill whatsapp-send del repo compagno gestisce entrambe le modalità con serializzazione UTF-8 sicura.
$ cat > /tmp/send.json <<'JSON'
{"phone":"+15550200",
"text":"Hi - your order shipped. Tracking: ABC123."}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/send.json \
https://app.timelines.ai/integrations/api/messages
{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}Follow-up programmati dentro una conversazione attiva
“Ogni lunedì alle 9, controlla le chat taggate to-follow-up che hanno avuto attività del cliente nelle ultime 24 ore ma nessuna risposta da noi, e manda un richiamo gentile.”
schedulalo con un cron job OpenClaw o uno standing order (vedi i docs di automazione di OpenClaw). Il job prende l'audience via GET /chats?label=to-follow-up&read=false, legge last_message_timestamp di ogni chat e invia solo se l'ultimo turno del cliente è di meno di 24 ore. Fuori dalla finestra, un follow-up diventa un re-engagement e torni in territorio Business API — vedi Channel choice.
Far partire messaggi da eventi nei tuoi altri strumenti
“Quando un deal HubSpot passa a 'demo scheduled', invia una conferma WhatsApp con il link al meeting.”
“Quando Stripe segnala un pagamento fallito, invia un messaggio gentile di recovery con un link per aggiornare la carta.”
“Quando viene creata una prenotazione Calendly, invia un promemoria pre-meeting la mattina stessa.”
il tuo strumento esistente (HubSpot, Stripe, Calendly, Pipedrive) fa partire il proprio webhook verso lo stesso receiver che hai configurato in Setup — aggiungi un handler instradato per event source o path, branca dal handler di TimelinesAI, e chiama POST /messages (nuovo destinatario) o POST /chats/{id}/messages (dentro una chat esistente). WhatsApp diventa un canale di consegna per qualsiasi flusso che hai già. Visto che gli invii seguono un'azione del cliente nello strumento upstream, sono transazionali per natura — dentro le regole del numero personale.
Inviare file e documenti su richiesta
“Genera il PDF del preventivo e mandalo al cliente che ha appena chiesto il prezzo.”
“Manda il contratto in PDF via email e droppalo anche nella chat WhatsApp del cliente.”
due passi. Carica i byte del file via POST /files_upload come multipart/form-data (non esiste un endpoint di upload per URL — il tuo agent deve scaricare il file prima e poi POSTarlo). La risposta restituisce un uid che passi a POST /chats/{id}/messages nel campo file_uid. Il cliente ha chiesto il documento — questa è una risposta alla sua richiesta, non cold outreach.
$ # Step 1 - upload the file bytes as multipart/form-data.
$ # The form field name is "file". The response gives you a uid.
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-F "file=@/path/to/quote.pdf" \
https://app.timelines.ai/integrations/api/files_upload
{"status":"ok","data":{
"uid":"FILE-UID-PLACEHOLDER",
"filename":"quote.pdf",
"size":128453,
"mimetype":"application/pdf",
"uploaded_by_email":"you@yourcompany.com",
"uploaded_at":"2026-04-14 10:22:03 +0000",
"temporary_download_url":"https://tl-prod-data.s3.amazonaws.com/..."
}}
$ # Step 2 - attach the uploaded file to a chat message using file_uid.
$ cat > /tmp/send.json <<'JSON'
{"text":"Quote attached for your review.",
"file_uid":"FILE-UID-PLACEHOLDER"}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/send.json \
https://app.timelines.ai/integrations/api/chats/12345678/messages
{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}Verificare se un messaggio è stato davvero consegnato
“Giovanni ha davvero ricevuto il messaggio della fattura che ho mandato stamattina?”
ogni invio restituisce un message_uid. Più tardi, GET /messages/{uid}/status_history restituisce la timeline Sent / Delivered / Read. La consegna avviene di solito in uno o due secondi su un numero attivo. Lo skill whatsapp-delivery-check del repo compagno avvolge tutto questo.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/messages/OUTBOUND-UID-PLACEHOLDER/status_history
{"status":"ok","data":[
{"status":"Sent", "timestamp":"2026-04-12 12:28:40 +0000"},
{"status":"Delivered","timestamp":"2026-04-12 12:28:41 +0000"}
]}CRM
CRM e analisi
Gli endpoint di lettura danno al tuo agente abbastanza dati per rispondere a domande analitiche e sincronizzare lo stato col tuo CRM in linguaggio naturale.
Reportistica del tempo di risposta
“Qual è stato il nostro tempo medio di prima risposta su WhatsApp questa settimana?”
“Chi nel mio team è il più lento a rispondere?”
tira le chat recenti, poi per ogni chat tira la timeline dei messaggi, trova la prima in uscita dopo ogni in entrata e aggrega i delta lato client. Due endpoint, nessuna chiamata analitica speciale.
$ # Pull the last page of a chat's messages (50 per page, fixed).
$ # Messages nest under data.messages — not a flat .data[] array.
$ # FILTER message_type=="whatsapp" to exclude notes (see Things to know
$ # below — notes live in the same timeline with from_me=false and would
$ # otherwise corrupt the response-time calculation).
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
"https://app.timelines.ai/integrations/api/chats/12345678/messages?page=1" \
| jq -r '.data.messages[] | select(.message_type=="whatsapp") | "\(.timestamp)\tfrom_me=\(.from_me)\t\(.text[0:40])"'
# 2026-04-12 12:28:40 +0300 from_me=false Hi - is the order shipped?
# 2026-04-12 12:30:15 +0300 from_me=true Yes - tracking ABC123, ETA 2-3
# 2026-04-12 14:02:11 +0300 from_me=false Got it, thanks!
# ...
$ # Compute first-reply latency from each consecutive (false -> true)
$ # pair, then average across all chats. Pure client-side aggregation.
$ # For full history walk ?page=2, ?page=3, ... until data.has_more_pages is false.Rilevamento dei messaggi senza risposta
“Quanti messaggi abbiamo ricevuto ieri? Quanti sono ancora senza risposta?”
“Mostrami ogni chat con un messaggio in arrivo nelle ultime 24 ore e nessuna risposta.”
GET /chats?read=false per le chat non lette, poi filtra a .data.messages[] dove message_type=="whatsapp" E from_me==false — anche le note hanno from_me=false e gonfierebbero il conteggio dei non-risposti. Vedi Cose da sapere.
Riassumere conversazioni a richiesta
“Riassumi tutta la conversazione con ACME Corp. Quali sono i loro pain point?”
“Dammi un brief di un paragrafo per ogni chat che non ho ancora risposto.”
tira GET /chats/{id}/messages, filtra a .data.messages[] dove message_type=="whatsapp" (altrimenti le note trascinerebbero gli scarabocchi precedenti del tuo stesso agente nel riassunto come se fossero parole del cliente — vedi Cose da sapere), poi fai riassumere a OpenClaw. Pagina con ?page=N per i thread lunghi.
Arricchire il tuo CRM con l'attività WhatsApp
“Per ogni chat nuova di questa settimana, cerca il numero in HubSpot. Se è un contatto, tagga la chat con il suo stage di deal. Altrimenti, crea il contatto.”
combina le letture di TimelinesAI con l'API del tuo CRM. Riscrivi i risultati nella chat via POST /chats/{id}/labels e POST /chats/{id}/notes.
$ # 1. Pull recent chats with their phone numbers. /chats returns 50 per
$ # page under data.chats — paginate with ?page=N if you need more.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
"https://app.timelines.ai/integrations/api/chats?page=1" \
| jq -r '.data.chats[] | "\(.id)\t\(.phone)"'
# 12345678 +15550100
# 12345679 +15550200
# ...
$ # 2. For each phone, look up your CRM (HubSpot/Pipedrive/Close).
$ # Pseudo-code:
$ # contact = hubspot.search_by_phone(phone)
$ # stage = contact.deal_stage if contact else "unknown"
$ # 3. Add the deal stage label WITHOUT wiping existing labels.
$ # POST /labels is REPLACE semantics (full set), so GET current labels,
$ # append the new one, and POST the combined list.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/chats/12345678/labels \
| jq -c '.data.labels + ["hubspot/qualified-lead"] | unique | {labels: .}' \
> /tmp/labels.json
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/labels.json \
https://app.timelines.ai/integrations/api/chats/12345678/labelsDare un punteggio ai lead dal contenuto della conversazione
“Dai un punteggio a ogni chat taggata inbound-lead da 1 a 10 per fit e urgenza. Scrivi il punteggio come nota.”
ragionamento LLM su GET /chats/{id}/messages — filtra prima a message_type=="whatsapp", altrimenti le tue stesse note precedenti di lead-score saranno rilette come parole del cliente e il punteggio driverà a ogni passaggio. Scrivi il risultato via POST /chats/{id}/notes con un prefisso prevedibile (es. "lead_score: fit=8 urgency=6") così il tuo prossimo giro può trovarlo e sostituirlo.
Operazioni
Scala e handoff
Pattern per workflow human-in-the-loop, routing multi-agente e memoria di conversazione. Tutte e quattro le capacità qua sotto sono progettate per coesistere con umani che lavorano sulle stesse chat dalla inbox condivisa di TimelinesAI.
Bozzare risposte per revisione umana invece di inviare
“Per ogni nuovo messaggio in arrivo, bozza una risposta e salvala come nota. Non inviare — le riguardo io e le mando.”
POST /chats/{id}/notes con il testo della bozza al posto di /messages. La nota appare nella stessa vista chat che il tuo team usa già.
Passare a un umano quando l'agente si blocca
“Se la conversazione passa più di 5 turni senza risoluzione, o il cliente chiede un umano, tagga escalate e smetti di rispondere finché non rimuovo il tag.”
conta i turni con GET /chats/{id}/messages, controlla le label di stop-reply con GET /chats/{id}/labels prima di ogni invio. Se scatta l'escalation, POST /chats/{id}/labels con escalate ed esci.
$ # 1. Count outbound 'whatsapp' messages (skip notes) in this chat.
$ # Messages nest under data.messages — not a flat .data[] array.
$ TURNS=$(curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
"https://app.timelines.ai/integrations/api/chats/12345678/messages?page=1" \
| jq '[.data.messages[] | select(.from_me==true and .message_type=="whatsapp")] | length')
$ echo $TURNS
8
$ # 2. Past the threshold? Append 'escalate' to the chat's labels and stop.
$ # POST /labels REPLACES the full set, so read existing first, combine,
$ # then POST — otherwise you wipe every other label on the chat.
$ if [ "$TURNS" -ge 5 ]; then
curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/chats/12345678/labels \
| jq -c '.data.labels + ["escalate"] | unique | {labels: .}' \
> /tmp/labels.json
curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/labels.json \
https://app.timelines.ai/integrations/api/chats/12345678/labels
fi
$ # 3. Before every future send, GET /chats/{id}/labels and bail
$ # if .data.labels contains 'escalate' or 'needs-human'.Far girare più agenti specializzati su una sola inbox
“L'agente Sales gestisce le domande di prezzo, l'agente Support le domande di prodotto. Indirizza per intent; se entrambi sono incerti, escalation a me.”
due skill specialisti in uno stesso workspace OpenClaw (uno vendite, uno supporto) più un terzo skill classificatore di intent che gira per primo su ogni messaggio in arrivo, tagga la chat con lo specialista scelto ed esce. Ogni specialista legge la label di intent prima di rispondere e aborta se punta all'altro — così un solo skill parte per messaggio.
Ricordare conversazioni precedenti con lo stesso cliente
“La settimana scorsa mi hai detto che eri in viaggio — com'è andato?”
la memoria propria di OpenClaw più GET /chats/{id}/messages per tutto lo storico WhatsApp. Lo storico sopravvive tra invocazioni perché vive su TimelinesAI.
Testa il tuo agente end-to-end con un secondo numero WhatsApp
Dopo il setup puoi vedere le capability elencate qui, ma non puoi davvero vedere il tuo agente al lavoro finché un cliente reale non gli scrive. È un loop brutto per iterare. Eccone uno migliore: collega un secondo numero WhatsApp allo stesso workspace TimelinesAI, usalo come cliente scripted e guarda il tuo skill reale gestire la conversazione end-to-end. Nessun mock, nessun webhook fake — è il tuo agente reale, la tua API reale, solo con un secondo numero che interpreta il ruolo dell'umano dall'altra parte.
Il pattern
TimelinesAI permette a più numeri WhatsApp di vivere in un solo workspace e instrada i loro webhook in entrata attraverso lo stesso receiver. Il trucco è il campo whatsapp_account_id su ogni payload di webhook — porta il JID del numero che ha ricevuto il messaggio. Leggilo, fai uno switch su di esso e smista verso lo step persona (quando il tuo agente ha appena risposto al cliente) o verso lo step agent (quando il cliente ha appena scritto al tuo agente). Ogni lato invia tramite il normale POST /messages o POST /chats/{id}/messages dalla prospettiva dell'altro.
Architettura
[+1 555 0100] [+1 555 0200]
persona agent under test
| |
| one TimelinesAI workspace |
| |
+-------> Public API <-------+
|
message:received:new
|
v
+--------------------------+
| Your test receiver |
| switch (accountJid) { |
| persona_jid -> next |
| agent_jid -> skill |
| } |
+--------------------------+Persona sender
Il lato persona è un cliente scripted: una lista di battute da dire in successione. Quando è ora del prossimo turno, POST a /messages con il numero di telefono dell'agente come destinatario. TimelinesAI si occupa dell'instradamento perché i due numeri sono in uno stesso workspace.
// persona-sender.js — send the next scripted customer turn from the
// persona WhatsApp number to the agent-under-test number. TimelinesAI
// routes the send because both numbers share one workspace.
const PERSONA_SCRIPT = [
"hi I saw your ad",
"we want to reduce customer churn",
"we are 25 people",
"we need to roll out in Q3",
];
let idx = 0;
export async function sendNextPersonaTurn() {
if (idx >= PERSONA_SCRIPT.length) return;
const text = PERSONA_SCRIPT[idx++];
await fetch("https://app.timelines.ai/integrations/api/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TIMELINES_AI_API_KEY}`,
"Content-Type": "application/json",
},
body: Buffer.from(JSON.stringify({
phone: process.env.AGENT_UNDER_TEST_PHONE,
text,
}), "utf-8"),
});
console.log(`[persona] sent: ${text}`);
}Receiver con jid-switch
Un receiver, un branch per lato. Tutto il resto che già conosci dal Setup — l'auth con ?secret=, il filtro from_me degli echo, il fallback piatto-vs-annidato del payload — vale qui invariato.
// api/webhook.js — route events by which number received them
export default async function handler(req, res) {
if (req.query.secret !== process.env.WEBHOOK_SECRET) return res.status(404).end();
const { event_type, data } = req.body || {};
if (event_type !== "message:received:new") return res.status(200).end();
if (data?.from_me === true) return res.status(200).end();
res.status(200).json({ ok: true });
const accountJid = data.whatsapp_account_id ?? data.chat?.whatsapp_account_id;
if (accountJid === process.env.AGENT_UNDER_TEST_JID) {
// Customer just messaged the agent — run your agent-under-test skill
await handleAgentTurn(data);
} else if (accountJid === process.env.PERSONA_JID) {
// Agent just replied to the persona — advance the scripted scenario
await advancePersona(data);
}
}Questi due snippet sono l'idea core. L'harness completo — script di persona, loader di scenari, kickoff da CLI, logger split-pane colorato, config Vercel — vive in examples/test-harness/ nel repo compagno, con il proprio README che ti guida attraverso deploy, env var, registrazione del webhook e l'esecuzione del tuo primo scenario end-to-end.
Guardarlo accadere
L'harness gira in autonomia — non ha bisogno che tu approvi ogni risposta. La tua supervisione avviene nel dashboard TimelinesAI: apri la chat tra il tuo numero persona e il tuo numero agent in un'altra tab del browser e ogni turno appare dal vivo. Se l'agente dice qualcosa di stupido, manda una risposta manuale dall'inbox dell'agent per rimettere in carreggiata la conversazione. Se devi cambiare comportamento nel mezzo di un run, aggiungi una label o scrivi una nota sulla chat — entrambe sono visibili allo skill dell'agent al suo prossimo turno. Ferma il run cancellando il webhook registrato o spegnendo la funzione Vercel; lo scenario si ferma immediatamente.
Esempio: un giro completo
Un loop concreto in cinque passi che mostra come appare l'API in pratica — come il tuo agente invia un outbound, conferma la consegna, processa la risposta del cliente e manda un follow-up. I numeri di telefono, chat ID e message UID qua sotto sono placeholder — sostituiscili con i tuoi.
Placeholder usati in tutto questo esempio:
Your business number → +1 555 0100 (JID: 15550100@s.whatsapp.net) Your customer's number → +1 555 0200 API token → $TIMELINES_AI_API_KEY
Conferma che il tuo token funzioni e elenca i numeri connessi
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/whatsapp_accounts
{"status":"ok","data":{"whatsapp_accounts":[
{"id":"15550100@s.whatsapp.net","phone":"+15550100",
"status":"active","account_name":"Your Business"}
]}}Dovresti vedere una lista dei tuoi numeri connessi. Se lo status non è active, sistemalo nel dashboard di TimelinesAI prima di continuare.
Invia un messaggio in uscita
Scrivi il payload in un file con codifica UTF-8 esplicita, poi passalo a curl. Questo è il pattern per ogni invio.
$ cat > /tmp/send.json <<'JSON'
{"phone":"+15550200",
"text":"Hi - your order shipped. Tracking: ABC123."}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/send.json \
https://app.timelines.ai/integrations/api/messages
{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}La risposta è una ricevuta, non una conferma di consegna. Tieni il message_uid per il prossimo passo.
Controlla lo stato di consegna
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
.../messages/OUTBOUND-UID-PLACEHOLDER/status_history
{"status":"ok","data":[
{"status":"Sent", "timestamp":"2026-04-12 12:28:40 +0000"},
{"status":"Delivered","timestamp":"2026-04-12 12:28:41 +0000"}
]}Sent → Delivered è di solito questione di un secondo su un numero attivo. Lo stato Read appare dopo, quando il destinatario apre davvero la chat.
Il cliente risponde, il tuo webhook scatta
Quando il cliente risponde, TimelinesAI fa POST verso la URL webhook che hai registrato:
{
"event_type": "message:received:new",
"data": {
"chat_id": 12345678,
"message_uid": "INBOUND-UID-PLACEHOLDER",
"sender_phone": "+15550200",
"sender_name": "Customer",
"text": "Thanks! When will it arrive?",
"timestamp": "2026-04-12 12:29:20 +0000"
}
}Il tuo receiver risponde 200 immediatamente, poi passa il payload al tuo skill OpenClaw.
Rispondi dalla stessa chat
$ cat > /tmp/reply.json <<'JSON'
{"text":"Estimated delivery is 2-3 business days. You'll get tracking updates to this chat."}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/reply.json \
https://app.timelines.ai/integrations/api/chats/12345678/messages
{"status":"ok","data":{"message_uid":"REPLY-UID-PLACEHOLDER"}}Visto che stai inviando in una chat esistente via /chats/{id}/messages, il mittente è automaticamente il numero WhatsApp proprietario della chat — non lo scegli tu, lo sceglie il record della chat. Per questo non invii accidentalmente dal numero sbagliato in workspace multi-numero.
Limiti e avvertenze
Cosa questa guida NON copre, e perché.
Guide correlate
Se stai costruendo questo per un canale cliente vero, queste tre pagine coprono territorio adiacente che toccherai prima o poi.
Collegare più numeri WhatsApp →
Far girare diversi numeri personali da un solo workspace, con routing della inbox condivisa e pinning JID per numero.
Integrazione HubSpot + WhatsApp →
Sincronizzare le conversazioni nei deal HubSpot — il complemento off-the-shelf più vicino alla capacità #11 (invii attivati da evento).
Agenti ChatGPT per WhatsApp →
La versione non-OpenClaw della stessa idea — un autoresponder gestito che configuri dal dashboard, senza skill.
Mattoncini
Bundle skill compagno su GitHub →4 skill che funzionano, un receiver webhook Vercel, docs di compliance e un mirror completo di questa guida. Licenza MIT.
Docs API TimelinesAI · Docs Skills OpenClaw · release v0.1.0
Guida alle capacità · 2026 · URL canonica timelines.ai/guide/openclaw-whatsapp-skills