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 puoi costruire

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.


Inizia qui

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.


Per iniziare

Setup

Quattro cose da collegare. Dopo, il tuo skill fa solo chiamate API.

  1. 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. 2

    Prendi un token API

    Integrazioni → API Pubblica → Copia. Salva come TIMELINES_AI_API_KEY. Un solo token copre tutto il workspace.

  3. 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. 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_accounts

Dovresti 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/webhooks

Receiver 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.


La tua install

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.

FileCosa controlla
SKILL.mdL'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.mdDocumentazione 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, schemasQualsiasi 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:

FileCosa controlla
IDENTITY.mdChi è il tuo agente — nome, creature (come si concettualizza), vibe, emoji signature, avatar. Compilalo presto; la maggior parte degli altri file lo referenziano.
SOUL.mdPrincipi 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.mdChi 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.mdConfig 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.mdIl 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.mdIstruzioni brevi ed esplicite su cosa OpenClaw dovrebbe fare all'avvio. Vuoto di default; riempilo se vuoi comportamento di boot deterministico tra riavvii.
BOOTSTRAP.mdConversazione 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.mdDefinizioni 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:

FileCosa 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.
La regola in una riga: cambi come si COMPORTA il tuo agente modificando il SKILL.md di uno skill; cambi CHI è il tuo agente modificando SOUL.md o IDENTITY.md nel workspace; cambi QUALI SEGRETI usa modificando il giusto file .env.<integrazione>. Tutto il resto nel workspace è macchinario che tocchi raramente.

Scelta del canale

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.

Questa guida riguarda i numeri WhatsApp personali — quelli che colleghi a TimelinesAI scansionando un QR. Servono per conversazioni in arrivo e invii transazionali a clienti che si aspettano il messaggio. Per cold outreach, broadcast di marketing o campagne promozionali, usa WhatsApp Business API — TimelinesAI lo supporta oggi via dashboard, e l'automazione via API pubblica per i flussi Business API arriva nel Q2 2026.

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. 1.Il cliente ti ha scritto per primo, o stai rispondendo dentro una sessione attiva di 24 ore? → Numero personale, questa guida.
  2. 2.Il cliente sta per ricevere qualcosa che si aspetta esplicitamente (ordine, appuntamento, pagamento, consegna)? → Numero personale, invio transazionale.
  3. 3.Innescato da un evento opt-in del cliente nel tuo CRM o strumento di billing? → Numero personale, invio attivato da evento.
  4. 4.Broadcast, cold outreach o campagna promozionale? → Business API, usa il dashboard di TimelinesAI.
  5. 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

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

MetodoPathCosa restituisce
GET/whatsapp_accountsI tuoi numeri WhatsApp connessi, ognuno con JID, telefono, status e nome account.
GET/chatsLista 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}/messagesStorico messaggi con ?limit=N. Include from_me, sender_phone, text, timestamp, message_type (whatsapp vs note).
GET/chats/{id}/labelsLabel della chat.
GET/messages/{uid}/status_historyTimeline Sent / Delivered / Read di un messaggio in uscita.
GET/messages/{uid}/reactionsReazioni 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/filesFile che hai caricato via API.
GET/webhooksLe tue iscrizioni webhook registrate.

Scrittura

MetodoPathCosa fa
POST/messagesInvia a un numero di telefono. Body: {"phone":"+...","text":"..."}. Restituisce {"message_uid":"..."}.
POST/chats/{id}/messagesInvia dentro una chat esistente. Body: {"text":"..."}. Il mittente è il numero WhatsApp proprietario della chat.
POST/chats/{id}/notesAllega 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}/labelsAggiunge 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}/reactionsImposta un emoji di reazione su un messaggio. Il body prende il carattere emoji letterale, non uno shortcode.
POST/files_uploadCarica 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/webhooksRegistra un'iscrizione webhook.
PUT/webhooks/{id}Aggiorna o abilita/disabilita un'iscrizione.
DELETE/webhooks/{id}Rimuove un'iscrizione.

Prima di iniziare

Cose da sapere

Alcuni dettagli facili da perdere nella reference che ti costeranno un'ora ciascuno se non li conosci.

!
Niente slash finali. GET /chats funziona. GET /chats/ restituisce la pagina 404 di TimelinesAI — che sembra un problema di rete e non lo è. Ogni URL in questa guida è scritta senza slash finale di proposito.
!
I body JSON devono essere UTF-8 valido — e la trappola va oltre gli heredoc. Il parser rifiuta tutto il resto. Em-dash e virgolette tipografiche sono il trigger più comune, ma ANCHE qualsiasi shell girando in locale non-UTF-8 (Git Bash su Windows, alcune immagini Docker base, vecchie sessioni SSH) corrompe quei caratteri sia negli heredoc SIA negli argomenti inline curl -d "..." — l'em-dash diventa il byte 0x97, che fallisce il decode UTF-8 sul server. Scrivi il payload in un file salvato esplicitamente come UTF-8 e usa curl --data-binary @file.json; non fidarti di -d inline per niente con punteggiatura tipografica.
!
L'URL base è https://app.timelines.ai/integrations/api. Alcuni post vecchi referenziano un altro sottodominio con header X-API-KEY — è obsoleto. Usa Bearer auth su app.timelines.ai/integrations/api.
!
Le URL degli allegati nei payload webhook scadono in fretta. Quando un cliente manda una foto o un PDF, scaricala inline dentro l'handler del webhook, non da un worker async.
!
I numeri personali vengono bannati per cold outreach. Gli endpoint di invio di questa guida ti lasceranno inviare a chiunque, ma WhatsApp bannerà il tuo numero in fretta per outbound non sollecitato. Manda outbound solo a persone che ti hanno scritto per prime o che esplicitamente si aspettano un messaggio transazionale. Per i broadcast usa WhatsApp Business API — vedi Scelta del canale.
!
L'invio è asincrono. POST /messages restituisce un message_uid — è una ricevuta, non una conferma di consegna. Usa GET /messages/{uid}/status_history per verificare la consegna reale.
!
Le forme di risposta variano — gli endpoint di lista nidificano sotto una chiave tipizzata. La maggior parte degli endpoint di lista restituisce {"data":{"<chiave-tipizzata>":[...]}}, non un {"data":[...]} piatto. GET /whatsapp_accounts nidifica sotto data.whatsapp_accounts, GET /chats sotto data.chats (con data.has_more_pages per la paginazione), GET /chats/{id}/messages sotto data.messages, GET /chats/{id}/labels sotto data.labels. Eccezioni: GET /messages/{uid}/status_history e GET /files restituiscono data come array piatto. Il codice che assume una forma uniforme si rompe al primo mismatch — leggilo come res?.data?.<tipizzata> ?? res?.data ?? [].
!
Usa from_me, non sender_phone, per la direzione in letture di storia. GET /chats/{id}/messages restituisce i messaggi in uscita (i tuoi invii via API pubblica) con sender_phone impostato al numero del team, non stringa vuota. Rilevare la direzione via sender_phone != "" etichetta ogni uscente come entrante e inverte la conversazione. Fidati del booleano from_me — true è uscente (il tuo agent che parla), false è entrante (cliente).
!
Nello storico il campo è uid; nel webhook è message_uid. Stesso valore, chiave diversa. Quando TL consegna un webhook message:received:new, il payload usa message_uid. Quando leggi lo storico della stessa chat via GET /chats/{id}/messages, il campo si chiama uid. È lo stesso identificatore. Codice che salva UID dai webhook e poi li cerca nello storico deve normalizzare entrambi i nomi. I messaggi dello storico usano anche timestamp, non created_at.
!
Le label sono REPLACE, non ADD. POST /chats/{id}/labels prende {"labels":["needs-human","intent/sales"]} — un array, chiave plurale. La chiamata sostituisce l'intero set di label della chat; per aggiungere una label prima fai GET del set esistente, ci aggiungi la tua e POSTi la lista combinata. Per pulire tutto: POST {"labels":[]}. Non esiste un endpoint DELETE /chats/{id}/labels/{nome} funzionante per rimuovere una singola label — devi POSTare il nuovo set senza quella che vuoi togliere.
!
Un webhook appena registrato può rigiocare storia recente. Sui primissimi eventi dopo POST /webhooks, TL può consegnare una raffica di eventi message:received:new passati — oppure l'handshake di registrazione può ritentare contro un container ancora in cold-start. In entrambi i casi il tuo receiver deve essere idempotente dall'evento #1. Deduplica su message_uid fin dall'inizio, non assumere “stato pulito” al momento della sottoscrizione.
!
I payload di webhook possono arrivare piatti O annidati. A volte TL consegna data.chat_id / data.whatsapp_account_id al livello superiore e altre volte annidati come data.chat.id / data.chat.whatsapp_account_id — la variazione dipende dal tipo di evento e dalla versione. Destrutturi difensivamente: const chatId = data.chat_id ?? data.chat?.id. Gli allegati sono peggio: data.attachment_url, data.attachment?.url e data.file_url sono tutti possibili. Un receiver che legge solo la forma piatta fallisce silenziosamente sulle consegne annidate.
!
Gli endpoint di lista restituiscono 50 elementi per pagina, fisso. ?limit=N, ?per_page=N e ?page_size=N vengono silenziosamente ignorati — l'API sceglie la propria dimensione. Pagina con ?page=N (indicizzato da 1) e fermati quando data.has_more_pages è false. Verificato su /chats e /chats/{id}/messages; il pattern si applica ad altre letture di lista. Se hai bisogno di “tutti i messaggi da X”, aspettati di percorrere le pagine, non una singola grossa chiamata limit=1000.
!
Disabilita l'autoresponder ChatGPT integrato di TimelinesAI prima che il tuo agente prenda il controllo. Ogni chat ha per default chatgpt_autoresponse_enabled: true e il responder integrato di TL risponderà ai messaggi in arrivo insieme al tuo skill, producendo doppi reply che il cliente vede. PATCH /chats/{id} con {"chatgpt_autoresponse_enabled": false} lo disattiva per chat. Non c'è un disattivazione in bulk sulla Public API, quindi rendi questa la prima azione che il tuo receiver esegue su qualunque chat di cui sta per prendere controllo (e falla una volta su ogni chat che il tuo agente già gestisce).
!
Le note vivono in /chats/{id}/messages con message_type=note e from_me=false, sempre. POST /chats/{id}/notes scrive nella stessa timeline dei messaggi WhatsApp reali — quando leggi lo storico, le note tornano intercalate con from_me: false indipendentemente da chi le ha scritte. Qualunque codice che ragiona sul flusso di conversazione DEVE filtrare per .message_type == "whatsapp": un calcolo ingenuo del tempo di risposta che accoppia entrante→uscente solo su from_me tratterà ogni nota come messaggio in arrivo dal cliente e corromperà la latenza, e uno skill di summary o scoring che alimenta lo storico completo a un LLM vedrà le sue stesse note precedenti come parole del cliente. Tieni le note nello stream solo quando vuoi esplicitamente stato strutturato (il pattern del qualifier); scartale ovunque altro.

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.

!
400 — corpo richiesta invalido. La causa più comune sono caratteri non-UTF-8 nel tuo JSON (em-dash, virgolette tipografiche da un heredoc shell). Scrivi il payload in un file con codifica UTF-8 esplicita e usa curl --data-binary @file.json. Stessa radice del tip UTF-8 più sopra.
!
401 — token scaduto o revocato. Ruotalo da app.timelines.ai → Integrazioni → API Pubblica → Rigenera. Il nuovo token entra in vigore subito; il vecchio smette di funzionare nello stesso istante. Leggilo dal tuo environment così puoi ruotarlo senza redeployare.
!
429 — rate limited. La risposta porta un header Retry-After in secondi. Rispettalo e riprova una volta. Se sbatti su 429 ripetutamente, il tuo skill sta inviando più in fretta del throttle ~30 messaggi al minuto del numero personale — rallenta lato client o spalma il carico in uscita su più numeri connessi.
!
5xx — problema upstream del gateway. Riprova con backoff esponenziale: 2s, 4s, 8s. Dopo il terzo fallimento, esca verso un umano e smetti di inviare. Niente loop infiniti — un 5xx persistente è di solito un evento della status page di TimelinesAI, non qualcosa che i tuoi retry sistemano.
!
Idempotenza dei webhook. TimelinesAI riprova ogni consegna fino a 3 volte con timeout di 5 secondi. Se il tuo receiver non deduplica, il cliente vede la stessa risposta 3 volte. Salva il message_uid in arrivo come nota della chat prima di processare; se il prossimo webhook arriva con la stessa uid, saltalo. Le note costano poco e sono visibili agli umani per il debug.

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 ;;
  esac

Operare in sicurezza in produzione

Altre due cose che non sono fallimenti, ma ti risparmieranno una brutta giornata più avanti.

!
Debuggare un webhook silenziosamente perso. Quando il tuo skill smette di rispondere a una conversazione vera, controlla in questo ordine: (1) il log di consegna webhook di TimelinesAI — se l'evento non arriva nemmeno alla tua URL, il problema è di registrazione o DNS, non del tuo skill; (2) i log della funzione della tua piattaforma di hosting (Vercel logs, ecc.) per un'eccezione non gestita; (3) lascia una nota briciola nella chat con POST /chats/{id}/notes prima di processare l'evento, così confermi che il receiver è davvero scattato; (4) GET /webhooks per verificare che la sottoscrizione sia ancora attiva (ruotare i token a volte fa cadere sottoscrizioni in silenzio).
!
Un singolo token copre tutto il workspace. Il tuo token API legge e scrive ogni chat, messaggio, label, nota, file e webhook del workspace. Trattalo come una password: salvalo in .env, non committarlo mai, e ruotalo da Integrazioni → API Pubblica → Rigenera quando un collega va via o un environment di deploy cambia. Il nuovo token entra in vigore subito; il vecchio smette di funzionare nello stesso istante. Oggi non c'è scope per-skill o sola lettura.

Contesto e memoria

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.

1

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à.

2

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.

3

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

!
Filtra le note e i tuoi stessi in-uscita dalla cronologia. GET /chats/{id}/messages restituisce tutto ciò che c'è sulla chat: turni in entrata del cliente, le tue stesse risposte in uscita e qualsiasi nota che il tuo skill ha scritto per stato o debug. Le note portano message_type == "note" e origin == "Public API" — filtrale prima di passare la cronologia al modello, altrimenti il tuo agente vede la propria contabilità interna come se fosse conversazione reale e comincia a rispondere alle proprie note.
!
Iniziare a metà conversazione. Quando il tuo skill vede una chat per la prima volta, quella chat può già avere una lunga cronologia da prima che tu ti iscrivessi al webhook. Recupera sempre contesto recente su una chat mai vista — altrimenti risponderai a un “ciao” nuovo come se fosse un'apertura fresca, quando il cliente in realtà è trenta turni dentro un thread esistente con il tuo team.
!
Deduplica le raffiche di webhook. Quando un cliente digita tre messaggi in quattro secondi, ricevi tre webhook — possibilmente in parallelo. Senza un lock per chat o un breve debounce, il cliente riceve tre risposte sovrapposte. Pattern: tieni il primo webhook per 1–2 secondi, poi recupera la cronologia una volta e rispondi allo stato combinato. Anche i retry del webhook (fino a 3 tentativi, 5 secondi di timeout ciascuno) arrivano dalla stessa via — deduplica su message_uid prima di elaborare.
!
Ordina per created_at, non per message_uid. I message_uid hanno scope workspace e non sono ordinabili globalmente. Quando assembli la cronologia nel prompt, ordina i turni per il timestamp created_at del payload, non per UID. Anche gli UID cross-workspace non sono condivisi — lo stesso messaggio WhatsApp fisico consegnato a due workspace TimelinesAI diversi ha un UID diverso in ciascuno.

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:

  1. 1Hardcoda il JID mittente permesso nell'environment di ogni skill (es. ALLOWED_SENDER_JID).
  2. 2Prima di inviare, GET /chats/{id} e confronta whatsapp_account_id col tuo JID permesso.
  3. 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.

1

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.
How it works

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.

2

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.
How it works

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.

3

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.
How it works

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.

4

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ù.
How it works

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"]}}
5

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.
How it works

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/notes
6

Trascrivere 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.
How it works

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.

7

Combaciare con la lingua del cliente

Se il cliente scrive in spagnolo, rispondi in spagnolo. Se cambia lingua a metà conversazione, cambia con lui.
How it works

ragionamento puramente lato OpenClaw sul testo in arrivo. TimelinesAI trasporta solo la risposta.

8

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.
How it works

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.

9

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.
How it works

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"}}
10

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.
How it works

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.

11

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.
How it works

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.

12

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.
How it works

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"}}
13

Verificare se un messaggio è stato davvero consegnato

Giovanni ha davvero ricevuto il messaggio della fattura che ho mandato stamattina?
How it works

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.

14

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?
How it works

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.
15

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.
How it works

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.

16

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.
How it works

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.

17

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.
How it works

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/labels
18

Dare 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.
How it works

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.

19

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.
How it works

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à.

20

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.
How it works

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'.
21

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.
How it works

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.

22

Ricordare conversazioni precedenti con lo stesso cliente

La settimana scorsa mi hai detto che eri in viaggio — com'è andato?
How it works

la memoria propria di OpenClaw più GET /chats/{id}/messages per tutto lo storico WhatsApp. Lo storico sopravvive tra invocazioni perché vive su TimelinesAI.


Testing

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.

Prerequisito — due numeri WhatsApp collegati. Questo pattern ha bisogno di due numeri nel tuo workspace TimelinesAI: uno che fa da persona cliente, uno che fa girare il tuo stack reale di agente. Se al momento hai un solo numero collegato, salta alla capability #19 (Draft per review umana) — ti dà un loop di iterazione più lento ma ancora utile, dove il tuo agente scrive draft come note e tu le approvi nel dashboard.

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

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
1

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.

2

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.

3

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.

4

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.

5

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

Limiti e avvertenze

Cosa questa guida NON copre, e perché.

!
I numeri personali vengono bannati per cold outreach. I broadcast non sollecitati da numeri personali non sono un caso d'uso supportato. Il ban viene applicato dall'infrastruttura WhatsApp, non dal gateway TimelinesAI, quindi non possiamo proteggerti.
!
La finestra di customer service di 24 ore di WhatsApp. Puoi rispondere a un cliente liberamente per 24 ore dopo il suo ultimo messaggio. Fuori da quella finestra, i messaggi richiedono opt-in e template — territorio Business API.
!
Consegna asincrona. POST /messages restituisce un message_uid (una ricevuta), non una conferma di consegna. Usa /status_history per confermare la consegna reale.
!
Le URL degli allegati scadono in fretta. Scarica i media inline nell'handler webhook, non da un worker ritardato.
!
Solo testo, media, reazioni e metadati — niente chiamate voce/video, niente broadcast-status, niente WhatsApp Channels o Stories.

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