Construa um agente de WhatsApp
com OpenClaw + TimelinesAI
Transforme o WhatsApp em um canal de cliente que seu agente OpenClaw opera: responde mensagens recebidas, envia notificações transacionais e disparadas por eventos, sincroniza com seu CRM, compartilha a caixa de entrada com o time. O TimelinesAI gerencia o gateway do WhatsApp. Seu agente cuida do raciocínio.
Resposta automáticaEnvios transacionaisSync de CRMCaixa compartilhada
O que seu agente pode fazer com WhatsApp
Com um skill do TimelinesAI instalado, seu agente OpenClaw pode operar o WhatsApp como um canal de cliente real. A versão longa desta lista está nas seções abaixo — cada capacidade tem as chamadas de API específicas que precisa.
Responder mensagens recebidas automaticamente — autoresponder 24/7, respondedor fora do horário comercial ou um chatbot completo que escala para um humano quando empaca.
Enviar mensagens transacionais e disparadas por eventos — confirmações de pedido, atualizações de envio, lembretes de compromisso, recibos de pagamento e notificações de outras ferramentas (HubSpot, Stripe, Calendly). Apenas para clientes que esperam a mensagem.
Qualificar leads em conversas de vários turnos — fazer uma sequência de perguntas, guardar respostas, marcar o chat como qualificado ou não.
Sincronizar a atividade do WhatsApp com seu CRM — buscar números recebidos no HubSpot/Pipedrive, atualizar estágios de negócio, devolver notas.
Resumir e pontuar conversas — “o que a ACME perguntou semana passada”, “pontue este chat 1–10 por intenção”.
Compartilhar a caixa de entrada com o time — seu agente rascunha respostas como notas privadas e os humanos enviam; ou o agente envia e os humanos observam.
Lidar com mídia — fotos de recibos, PDFs, notas de voz — processadas pelo OpenClaw e respondidas automaticamente.
Como usar este guia
Este guia é dual-mode. Você pode ler você mesmo e seguir o Setup manualmente — o caminho que as seções abaixo descrevem — ou pode entregar o guia inteiro para o seu agente OpenClaw (ou qualquer agente que consiga ler uma URL) e deixar que ele faça o setup pra você. Os dois caminhos chegam no mesmo lugar: quatro skills de WhatsApp instalados, token e webhook conectados, seu agente respondendo mensagens de clientes.
Se você está lendo isso você mesmo
Passe pelos grupos de capabilities abaixo — Incoming, Outbound, CRM & analytics, Operations — e decida quais importam pro seu caso de uso. Depois siga o Setup manualmente; são quatro passos e uns dez minutos. Cada capability lista as chamadas exatas de API que ela faz, e todo code block deste guia foi executado contra a API real do TimelinesAI antes da publicação. Se você travar, Coisas para saber reúne cada gotcha que vai te custar uma hora se você não souber dele de antemão.
Se você quer que seu agente faça o setup
Cole este prompt no OpenClaw (ou Claude Code, Cursor, Claude desktop, qualquer agente que consiga ler uma URL). Ele aponta pra este guia por URL pra que o agente leia a versão live, e depois te acompanha por install, token, webhook, smoke-test e habilitar seu primeiro skill — pedindo input só onde ele genuinamente precisa de algo seu.
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.O prompt para de propósito antes de ligar qualquer envio outbound — ele te rotea primeiro por Channel choice pra você escolher o canal de WhatsApp certo (número pessoal vs Business API) pro que você está realmente tentando fazer. O risco de ban é real e vive upstream, não é algo do qual o gateway pode te proteger.
Setup
Quatro coisas para conectar. Depois disso, seu skill só faz chamadas de API.
- 1
Conecte seu número de WhatsApp
Entre em app.timelines.ai, escaneie o QR com o telefone que tem seu número comercial. O TimelinesAI gerencia o gateway daqui em diante. Mesmo fluxo do setup padrão de número pessoal.
- 2
Pegue um token de API
Integrações → API Pública → Copiar. Salve como TIMELINES_AI_API_KEY. Um único token cobre todo o workspace.
- 3
Instale um skill do repositório companheiro
Clone InitechSoftware/openclaw-whatsapp-skills em ~/.openclaw/workspace/, depois faça symlink de cada diretório de skill em ~/.openclaw/skills/. Guia rápido de quatro linhas no README do repositório companheiro.
- 4
Registre um webhook
Aponte message:received:new para uma URL HTTPS pública. O TimelinesAI vai mandar push das mensagens recebidas. Seu receptor invoca o OpenClaw.
Smoke-test do token
Antes de construir qualquer coisa, confirme que o auth funciona:
curl -sS -w "\nHTTP: %{http_code}\n" \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/whatsapp_accountsVocê deveria ver
{
"status": "ok",
"data": {
"whatsapp_accounts": [
{
"id": "<phone>@s.whatsapp.net",
"phone": "+<phone>",
"status": "connected",
"account_name": "<your label>"
}
]
}
}Se você vir outra coisa
- HTTP 401 com o texto puro Unauthorized — o token não foi aplicado. Reveja o passo 2, confirme que incluiu o prefixo Bearer e que o token não quebrou em linhas ao colar. A resposta é texto puro, não JSON — passar para o jq vai dar erro.
- Uma página HTML estilizada “Page not found” com HTTP 404 — você tem uma barra final ou um erro de digitação no caminho. A API serve HTML (não um erro JSON) para caminhos errados, então se sua saída começa com <!DOCTYPE html>, tire qualquer barra final e confira o caminho.
Registre o webhook uma vez
# 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/webhooksReceptor mínimo de webhook (Node / Vercel)
~40 linhas. Valida o query param ?secret=, descarta os echos de saída, responde 2xx dentro da janela de retry de 5 segundos e depois encaminha a mensagem para o seu 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 é o endpoint que o seu deployment de OpenClaw expõe para mensagens recebidas — depende de como você hospeda o agente. Para uma referência com notas de durabilidade pronta para produção, veja examples/vercel-webhook-receiver no repositório companheiro.
O que está dentro do seu workspace do OpenClaw
Você conectou um token de TimelinesAI e fez symlink de skills do repositório companheiro. Aqui está o que você realmente instalou, e quais arquivos você edita quando quer mudar como seu agente pensa, o que ele sabe e como se comporta. Nada aqui é mágica — é tudo markdown simples e arquivos env em dois diretórios.
Skills — ~/.openclaw/skills/<skill>/
Cada skill que você fez symlink é um diretório com um arquivo obrigatório (SKILL.md) mais os recursos que ele referencia. Se quer mudar como seu FAQ handler soa ou re-ordenar as perguntas do lead qualifier, a edição mora no SKILL.md daquele skill. Sem código para recompilar, sem build step.
| Arquivo | O que controla |
|---|---|
| SKILL.md | O skill inteiro. YAML frontmatter no topo (name, description, user-invocable e quaisquer flags de dispatch) mais o body com as instruções/prompt abaixo. Aqui você edita o que o skill faz, quando o OpenClaw o escolhe e como ele se comporta. Cada um dos quatro skills de WhatsApp do repositório companheiro é um único SKILL.md — sem código para recompilar, só edita e reinicia. |
| README.md | Documentação humana do skill. O OpenClaw NÃO lê esse arquivo — é para você, seu time e qualquer um navegando o skill no GitHub. Opcional; os skills do repositório companheiro todos trazem um. |
| Scripts, resources, schemas | Qualquer coisa referenciada a partir do SKILL.md via {baseDir}/... — scripts auxiliares, templates de prompt, schemas JSON, fixtures de teste. Ignorado pelo OpenClaw a menos que seu SKILL.md aponte explicitamente. Coloque o que o skill precisar ao lado de SKILL.md e referencie por path. |
Para os quatro skills de WhatsApp — whatsapp-autoresponder, whatsapp-lead-qualifier, whatsapp-send, whatsapp-delivery-check — o topo de cada SKILL.md tem o role e as tools permitidas; o body tem o prompt real com o qual seu agente roda. Leia um deles end-to-end antes de editar; os padrões se repetem.
Workspace — ~/.openclaw/workspace/
O diretório workspace é a casa do OpenClaw para tudo que não é skill: quem é seu agente, o que ele sabe de você, que tools ele tem, como ele inicializa. Os próprios docs do OpenClaw dizem “esta pasta é casa. Trate-a assim”. Esses arquivos vêm com o install — você preenche com o tempo:
| Arquivo | O que controla |
|---|---|
| IDENTITY.md | Quem é seu agente — nome, creature (como ele se conceitua), vibe, emoji de assinatura, avatar. Preencha cedo; a maioria dos outros arquivos referencia este. |
| SOUL.md | Princípios operacionais e personalidade core. Como o agente deve se comportar quando ninguém está olhando — quando ser genuíno vs performativo, como ganhar confiança, onde ficam os limites de privacidade. Os docs do OpenClaw chamam isso de consciência do agente. |
| USER.md | Quem o agente está ajudando (você). Nome, pronomes, timezone, interesses, contexto do projeto. Agentes são melhores ajudando alguém específico do que um “usuário” genérico — este arquivo é como eles se lembram. |
| TOOLS.md | Config específica do ambiente que você não quer dentro de nenhum skill — nomes de dispositivos, endereços de host, preferências locais. Mora no workspace para que você possa editá-lo sem tocar em nada que você possa compartilhar. |
| AGENTS.md | O README do workspace para agentes. Descreve como o OpenClaw espera que este workspace seja rodado. Normalmente vem com o install e raramente é editado. |
| BOOT.md | Instruções curtas e explícitas sobre o que o OpenClaw deve fazer na inicialização. Vazio por padrão; preencha se quiser comportamento de boot determinístico entre reinicializações. |
| BOOTSTRAP.md | Conversa de onboarding do primeiro boot. Guia um workspace novo para estabelecer sua identidade, depois te direciona a salvar os resultados em IDENTITY.md, SOUL.md, USER.md. Apague uma vez que tenha preenchido esses outros. |
| HEARTBEAT.md | Definições de tarefas periódicas. Arquivo vazio significa sem heartbeats; adicione tarefas aqui quando quiser que o agente verifique algo em um intervalo (ex.: poll de uma queue a cada cinco minutos). |
Arquivos env por integração
Junto aos arquivos canônicos acima, você adiciona um arquivo env para cada integração que conecta. Esses NÃO fazem parte do install do OpenClaw — são seus, e guardam segredos que nunca deveriam ser commitados em um repo público:
| Arquivo | O que controla |
|---|---|
| .env.<integration> | Segredos e config para uma integração, um arquivo por integração. Para o trabalho de WhatsApp você criou .env.timelinesai com TIMELINES_AI_API_KEY e ALLOWED_SENDER_JID no Setup passo 2. Repita esse padrão quando adicionar HubSpot, Stripe, Pipedrive ou qualquer outra tool — um .env.<name> e faça source dele no skill que precisa. |
Números pessoais vs WhatsApp Business API
Antes de construir fluxos de saída, entenda a qual canal de WhatsApp seu caso de uso pertence. Escolher errado leva a banimento.
Seguro com os skills deste guia (número pessoal)
- •Conversas recebidas. O cliente te escreve primeiro, você responde. Sem risco.
- •Envios transacionais. Confirmações de pedido, atualizações de envio, notificações de entrega, recibos de pagamento, lembretes de compromisso — qualquer mensagem que o cliente espera porque acabou de fazer algo com seu negócio.
- •Notificações disparadas por eventos em outras ferramentas. Negócio do HubSpot → confirmação de demo, falha de pagamento do Stripe → nota de recuperação, agendamento do Calendly → lembrete pré-reunião. O cliente optou quando usou a ferramenta upstream.
- •Responder dentro da janela de atendimento de 24 horas do WhatsApp. Quando o cliente te escreve, você tem 24 horas para responder livremente. Os skills de auto-resposta, FAQ e qualificação de leads operam dentro dessa janela.
NÃO seguro em números pessoais
- •Cold outreach para listas que você comprou ou raspou — o WhatsApp bane números por isso em horas.
- •Broadcasts de marketing para clientes que não optaram explicitamente.
- •Campanhas promocionais, ofertas de vendas, push sazonal, lançamentos de produto.
- •Qualquer coisa parecida com um blast de marketing. Se você está perguntando “qual throughput eu consigo”, você está no canal errado.
Como saber qual canal você precisa
- 1.O cliente te escreveu primeiro, ou você está respondendo dentro de uma sessão ativa de 24 horas? → Número pessoal, este guia.
- 2.O cliente está prestes a receber algo que ele explicitamente espera (pedido, compromisso, pagamento, entrega)? → Número pessoal, envio transacional.
- 3.Disparado por um evento opt-in do cliente no seu CRM ou ferramenta de billing? → Número pessoal, envio disparado por evento.
- 4.Broadcast, cold outreach ou campanha promocional? → Business API, use o dashboard do TimelinesAI.
- 5.Não tem certeza? → Não envie. Trate como promocional e roteie pelo caminho Business API.
Cada capacidade de saída abaixo assume que você passou neste teste. Se não tem certeza, releia esta seção antes de shipar. O TimelinesAI não pode te proteger do banimento na camada do WhatsApp — o ban acontece upstream, na infraestrutura do WhatsApp, não no gateway.
Referência da API
Cada endpoint que você precisa para construir cada capacidade abaixo. URL base https://app.timelines.ai/integrations/api. Auth Authorization: Bearer $TIMELINES_AI_API_KEY.
Leitura
| Método | Rota | O que devolve |
|---|---|---|
| GET | /whatsapp_accounts | Seus números de WhatsApp conectados, cada um com JID, telefone, status e nome de conta. |
| GET | /chats | Lista de chats. Aceita filtros ?phone=... e ?label=.... Pagine com ?page=N (50 por página, fixo). Cada chat tem whatsapp_account_id com o JID do número dono, mais chatgpt_autoresponse_enabled — veja Coisas para saber antes de lançar seu próprio agente. |
| GET | /chats/{id} | Detalhe completo de um chat. |
| GET | /chats/{id}/messages | Histórico de mensagens, 50 por página. Pagine com ?page=N e olhe data.has_more_pages. Cada mensagem carrega from_me, sender_phone, text, timestamp, message_type, status (Sent/Delivered/Read) e origin (API Pública vs app WhatsApp). |
| GET | /chats/{id}/labels | Etiquetas do chat. |
| GET | /messages/{uid}/status_history | Linha do tempo Sent / Delivered / Read de uma mensagem enviada. |
| GET | /messages/{uid}/reactions | Reações a uma mensagem. Retorna {data: {users: [{name, phone, reaction, current}], reactions: {<emoji>: count}, total: N}} — um objeto, não um array plano. users lista quem reagiu (cada um traz o emoji escolhido mais um booleano current marcando seu próprio workspace); reactions é um histograma indexado por emoji. Estado vazio: {users: [], reactions: {}, total: 0}. |
| GET | /files | Arquivos que você subiu pela API. |
| GET | /webhooks | Suas inscrições de webhook registradas. |
Escrita
| Método | Rota | O que faz |
|---|---|---|
| POST | /messages | Envia para um número de telefone. Body: {"phone":"+...","text":"..."}. Devolve {"message_uid":"..."}. |
| POST | /chats/{id}/messages | Envia dentro de um chat existente. Body: {"text":"..."}. O remetente é o número de WhatsApp dono do chat. |
| POST | /chats/{id}/notes | Anexa uma nota privada ao chat. Não vai para o WhatsApp, só é visível dentro do TimelinesAI. Usado para estado do agente e fluxos de revisão. |
| POST | /chats/{id}/labels | Adiciona uma etiqueta ao chat. Útil para tracking de etapa, roteamento e flags de stop-reply. |
| PATCH | /chats/{id} | Atualiza metadados do chat — responsável (responsible_email), estado de leitura e chatgpt_autoresponse_enabled. Desligue o último antes do seu agente começar a responder, ou o responder integrado do TL vai correr contra o seu. |
| PATCH | /messages/{uid}/reactions | Define um emoji de reação em uma mensagem. O body leva o caractere emoji literal, não um shortcode. |
| POST | /files_upload | Faz upload de um arquivo via multipart/form-data (campo: file). Devolve data.uid, que você passa para chat/messages como file_uid. Não existe variante upload-por-URL. |
| POST | /webhooks | Registra uma inscrição de webhook. |
| PUT | /webhooks/{id} | Atualiza ou ativa/desativa uma inscrição. |
| DELETE | /webhooks/{id} | Remove uma inscrição. |
Coisas para saber
Alguns detalhes fáceis de passar batido na referência e que cada um vai te custar uma hora se você não souber.
Quando as chamadas de API falham
Cinco modos de falha vão atingir seu skill cedo ou tarde. Cada um aparece diferente na resposta e cada um tem uma resposta correta diferente.
Um padrãozinho de shell que ramifica por cada código de status:
$ # 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 ;;
esacOperar com segurança em produção
Mais duas coisas que não são falhas, mas vão te poupar de um dia ruim depois.
Contexto da conversa
O webhook entrega ao seu skill uma única mensagem — a que acabou de chegar. Sem turnos anteriores, sem histórico de chat, sem transcrição em andamento. Para um FAQ sem estado ou um respondedor fora do horário, isso basta. Para um agente de conversa que precisa lembrar o que o cliente disse três turnos atrás, o seu skill tem que buscar o contexto por conta própria. Três padrões cobrem toda a faixa, do loop mais barato ao que realmente lembra.
Sem estado
Responda só à última mensagem. Um POST por turno, sem GET, sem histórico. É o loop mais simples e o padrão nos skills prontos. Use para bots de FAQ, respondedores fora do horário e classificação ou roteamento — onde a resposta depende apenas da mensagem atual. O agente esquece tudo entre turnos: se o cliente escrever um follow-up que faz referência a algo dito antes, ele não vai entender.
Janela de contexto completa
Busque as últimas 20 mensagens antes de cada resposta e passe ao modelo como histórico. Duas chamadas de API por turno em vez de uma, além dos tokens extras no prompt a cada chamada. Use para agentes de conversa, qualificação de leads multi-turno e qualquer cenário em que o agente precisa lembrar o fio. Vinte turnos recentes costumam bastar — aumentar mais raramente ajuda e encarece o prompt; reduzir mais faz o agente esquecer o que o cliente disse há um minuto.
Contexto adaptativo
Busque contexto só quando a mensagem recebida parece um follow-up. Uma heurística barata sobre o texto — começa com um pronome, referência a “isso” ou “ele”, chega dentro de 30 segundos da sua resposta anterior, acks de uma palavra — decide se puxa histórico ou responde sem estado. A maioria dos turnos segue barata; os follow-ups ganham memória. Comece com a janela completa; só migre para adaptativo depois de conhecer seu orçamento de custo e ter tráfego real para calibrar a heurística.
Buscar a janela é um único GET. A resposta é um array plano de mensagens do chat, a mais nova por último, misturando turnos recebidos do cliente com suas próprias respostas enviadas e qualquer nota que o seu skill tenha escrito:
$ 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"}
]}Armadilhas que mordem em produção
Persistência de estado
Skills do OpenClaw não mantêm estado em memória entre invocações. Conversas de WhatsApp são multi-turno. A solução é guardar o estado no próprio chat:
- •Etiquetas guardam o estágio discreto — discovery/q1, qualified, escalate. Adicione com POST /chats/{id}/labels, leia com GET /chats/{id}/labels.
- •Notas guardam dados estruturados — team_size=8, rascunhos de resposta, scores de leads. Adicione com POST /chats/{id}/notes. Leia iterando GET /chats/{id}/messages e filtrando message_type == "note".
A vantagem: segurança contra crash, visibilidade para o time humano, handoff limpo — um humano pode limpar uma etiqueta para voltar o fluxo, ou adicionar escalate para assumir. O trade-off: cada transição de estado é uma chamada HTTP. Para fluxos voltados ao cliente isso é OK.
Enviar do número certo
Se seu workspace tem mais de um número de WhatsApp conectado, seu skill precisa garantir que envia do número esperado. Cada chat tem um campo whatsapp_account_id com o JID completo (tipo TELEFONE@s.whatsapp.net) do número dono. Quando você faz POST /chats/{id}/messages, o remetente é sempre esse JID — você não escolhe, o chat escolhe.
O padrão:
- 1Hardcode o JID de remetente permitido no environment de cada skill (ex.: ALLOWED_SENDER_JID).
- 2Antes de enviar, GET /chats/{id} e compare whatsapp_account_id com o seu JID permitido.
- 3Se não bater, pule o envio — escale para um humano ou descarte o evento.
Duas chamadas HTTP extras por turno, zero chance de enviar do persona errado. Para workspaces de um número só isso não se aplica.
Entrada
Mensagens recebidas — o que seu agente pode lidar
Toda vez que um cliente manda mensagem para o seu número de WhatsApp, o TimelinesAI dispara um evento message:received:new para o seu webhook com o chat id, o texto, o telefone do remetente e os anexos. Seu skill lê o evento, decide o que fazer e responde com POST /chats/{chat_id}/messages. Tudo abaixo é uma variação desse loop.
Responder automaticamente cada mensagem recebida
“Responda perguntas sobre frete, devoluções e horário. Para qualquer outra coisa, rascunhe uma resposta e marque o chat para revisão.”
“Entre 22:00 e 08:00 responda automaticamente. No horário comercial só me sinalize os chats recebidos.”
“Cuide das minhas respostas do WhatsApp enquanto estou nesta reunião.”
o skill recebe o payload do webhook, monta uma resposta e chama POST /chats/{id}/messages com {"text":"..."}. Sem estado por padrão — uma chamada por turno. Para conversas de vários turnos em que o agente precisa lembrar mensagens anteriores, veja Contexto da conversa abaixo.
Manipulador de FAQ com escalação para humano
“Responda perguntas sobre frete, devoluções e horário. Para qualquer outra coisa, marque o chat needs-human e pare de responder até eu remover a etiqueta.”
antes de responder, consulte GET /chats/{id}/labels. Se o chat tiver needs-human, saia sem enviar nada. Se o texto recebido bate com um tópico de FAQ, responda. Caso contrário, POST /chats/{id}/labels com a etiqueta de escalação e saia em silêncio. A caixa do seu time filtra pela etiqueta.
Rotear conversas para a pessoa certa
“Para cada chat recebido, deduza se é vendas, suporte ou financeiro e marque. Atribua chats de vendas a alex@ours e financeiro a jamie@ours.”
classifique a intenção pelo texto, POST /chats/{id}/labels com intent/sales ou similar, então PATCH /chats/{id} com {"responsible_email":"..."} para entregar o chat dentro da caixa do TimelinesAI.
Qualificar leads com uma sequência de perguntas
“Para qualquer chat novo da nossa campanha do Facebook, pergunte sobre o caso de uso, tamanho do time e prazo. Guarde as respostas como notas no chat. Marque qualified se o time tiver 5 ou mais.”
as etiquetas marcam em qual pergunta você está (discovery/q1, q2, q3); as notas guardam as respostas. A cada turno: leia a etiqueta da etapa atual, parseie o texto recebido como resposta, escreva via POST /chats/{id}/notes, avance a etiqueta e faça a próxima pergunta. Sem banco externo — veja Persistência de estado abaixo.
$ # 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"]}}Entender fotos, PDFs e recibos
“Quando um cliente mandar uma foto de recibo, extraia o valor e o vendedor e adicione como nota.”
“Se alguém mandar um PDF, classifique como fatura / contrato / identidade e marque o chat conforme.”
os payloads de webhook incluem uma URL de anexo. Baixe dentro do handler (ela expira rápido), processe com as ferramentas de visão ou documento do OpenClaw e escreva os dados extraídos com 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/notesTranscrever notas de voz e responder
“Transcreva as notas de voz recebidas. Responda por texto — se realmente precisar de uma resposta em voz, manda do dashboard do TimelinesAI.”
o webhook entrega uma URL para o arquivo de voz. Baixe, transcreva, monte uma resposta em texto via POST /chats/{id}/messages. Respostas em voz são hoje uma função do dashboard do TimelinesAI e não estão na referência pública da API atual — se seu skill precisa enviar respostas em voz programaticamente, contate o suporte do TimelinesAI para confirmar se o endpoint voice_message legado ainda está disponível no seu workspace.
Casar com o idioma do cliente
“Se o cliente escrever em português, responda em português. Se ele trocar de idioma no meio da conversa, troque junto.”
raciocínio puro do lado do OpenClaw sobre o texto recebido. O TimelinesAI só transporta a resposta.
Reagir a mensagens sem mandar uma resposta completa
“Reaja com 👀 a cada mensagem recebida para o cliente saber que eu vi, e depois eu monto a resposta de verdade com calma.”
PATCH /messages/{uid}/reactions com {"reaction":"👀"}. O campo reaction precisa conter o caractere emoji literal — shortcodes como "eyes" ou ":eyes:" são rejeitados com HTTP 400 "Reaction has invalid format". Sem crédito de mensagem consumido — reações são leves.
$ # 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"}Saída
Saída — mensagens que seu agente inicia
Mensagens que seu agente inicia, não respostas. Disparadas por um evento nas suas outras ferramentas (um pedido novo, um pagamento que falhou, uma reunião agendada) ou por instrução humana direta sobre uma pessoa específica.
Antes de cada capacidade desta seção: o cliente abriu o thread recentemente (dentro da janela de sessão de 24 horas do WhatsApp) ou explicitamente espera essa mensagem. Se nenhuma das duas é verdade, não envie de um número pessoal — isso é território Business API.
Enviar uma mensagem transacional por nome ou para um novo destinatário
“Avise o João que a fatura dele está pronta.”
“Manda pro encanador o novo endereço do escritório para ele entregar as peças.”
“Envie o contrato assinado para o cliente que acabou de transferir o sinal.”
para um chat existente, busque com GET /chats?name=João (ou seu CRM) e chame POST /chats/{id}/messages. Para um destinatário novo, POST /messages com {"phone":"+...","text":"..."}. O skill whatsapp-send do repositório companheiro cuida dos dois modos com serialização UTF-8 segura.
$ 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-ups agendados dentro de uma conversa ativa
“Toda segunda às 9h, verifique chats marcados to-follow-up que tiveram atividade do cliente nas últimas 24 horas mas nenhuma resposta nossa, e mande um lembrete amável.”
agende com um cron job do OpenClaw ou uma standing order (veja a doc de automação do OpenClaw). O job puxa o público via GET /chats?label=to-follow-up&read=false, lê o last_message_timestamp de cada chat, e só envia quando o último turno do cliente foi há menos de 24 horas. Fora dessa janela um follow-up vira um toque de re-engajamento e volta a ser território Business API — veja Channel choice.
Disparar mensagens a partir de eventos das suas outras ferramentas
“Quando um negócio do HubSpot virar 'demo scheduled', envie uma confirmação por WhatsApp com o link da reunião.”
“Quando o Stripe reportar pagamento falho, envie uma mensagem educada de recuperação com link para atualizar o cartão.”
“Quando um agendamento do Calendly for criado, envie um lembrete pré-reunião na manhã do mesmo dia.”
sua ferramenta existente (HubSpot, Stripe, Calendly, Pipedrive) dispara o webhook dela para o mesmo receiver que você configurou em Setup — adicione um handler roteado por event source ou path, faça um branch a partir do handler do TimelinesAI, e chame POST /messages (destinatário novo) ou POST /chats/{id}/messages (dentro de um chat existente). O WhatsApp vira um canal de entrega para qualquer fluxo que você já tenha. Como os envios vêm atrás de uma ação do cliente na ferramenta de origem, eles são transacionais por natureza — dentro das regras do número pessoal.
Enviar arquivos e documentos sob demanda
“Gere o PDF da proposta e envie para o cliente que acabou de pedir preço.”
“Mande o contrato em PDF por email e também solte no chat de WhatsApp do cliente.”
dois passos. Faça upload dos bytes do arquivo via POST /files_upload como multipart/form-data (não existe endpoint de upload por URL — seu agente tem que baixar o arquivo primeiro e fazer POST). A resposta devolve um uid que você passa para POST /chats/{id}/messages no campo file_uid. O cliente pediu o documento — isso é uma resposta ao pedido dele, não 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"}}Verificar se uma mensagem foi de fato entregue
“O João realmente recebeu a mensagem da fatura que mandei hoje de manhã?”
cada envio devolve um message_uid. Mais tarde, GET /messages/{uid}/status_history devolve a linha do tempo Sent / Delivered / Read. A entrega normalmente acontece em um ou dois segundos num número ativo. O skill whatsapp-delivery-check do repositório companheiro empacota isso.
$ 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 análise
Os endpoints de leitura dão ao seu agente dados suficientes para responder perguntas analíticas e sincronizar estado com seu CRM em linguagem natural.
Relatórios de tempo de resposta
“Qual foi nosso tempo médio de primeira resposta no WhatsApp esta semana?”
“Quem da minha equipe é o mais lento para responder?”
puxe os chats recentes, depois para cada chat busque a linha do tempo de mensagens, encontre a primeira saída depois de cada entrada e agregue os deltas no cliente. Dois endpoints, sem chamada analítica especial.
$ # 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.Detecção de mensagens sem resposta
“Quantas mensagens recebemos ontem? Quantas ainda estão sem resposta?”
“Mostre cada chat com mensagem recebida nas últimas 24 horas e sem resposta.”
GET /chats?read=false para chats não lidos, depois filtre a .data.messages[] onde message_type=="whatsapp" E from_me==false — as notas também têm from_me=false e inflariam a contagem de não respondidas. Veja Coisas para saber.
Resumir conversas sob demanda
“Resuma toda a conversa com a ACME Corp. Quais são as dores deles?”
“Me dê um brief de um parágrafo de cada chat que ainda não respondi.”
puxe GET /chats/{id}/messages, filtre a .data.messages[] onde message_type=="whatsapp" (as notas jogariam os rabiscos anteriores do seu próprio agente no resumo como se fossem falas do cliente — veja Coisas para saber), depois deixe o OpenClaw resumir. Pagine com ?page=N para threads longos.
Enriquecer seu CRM com a atividade do WhatsApp
“Para cada chat novo desta semana, busque o número no HubSpot. Se for um contato, marque o chat com o estágio do negócio. Se não, crie o contato.”
combine as leituras do TimelinesAI com a API do seu CRM. Escreva os resultados de volta no chat com 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/labelsPontuar leads pelo conteúdo da conversa
“Pontue cada chat marcado inbound-lead de 1 a 10 por encaixe e urgência. Escreva a pontuação como uma nota.”
raciocínio LLM sobre GET /chats/{id}/messages — filtre a message_type=="whatsapp" primeiro, senão suas próprias notas anteriores de lead-score vão ser lidas como falas do cliente e a pontuação vai derivar a cada rodada. Escreva o resultado via POST /chats/{id}/notes com um prefixo previsível (ex.: "lead_score: fit=8 urgency=6") para que sua próxima passagem consiga encontrar e substituir.
Operações
Escala e handoff
Padrões para fluxos human-in-the-loop, roteamento multi-agente e memória de conversa. As quatro capacidades abaixo são desenhadas para coexistir com humanos trabalhando os mesmos chats da caixa compartilhada do TimelinesAI.
Rascunhar respostas para revisão humana em vez de enviar
“Para cada mensagem recebida nova, rascunhe uma resposta e salve como nota. Não envie — eu mesmo reviso e mando.”
POST /chats/{id}/notes com o texto do rascunho em vez de /messages. A nota aparece na mesma vista de chat que seu time já usa.
Passar para um humano quando o agente trava
“Se a conversa passar de 5 turnos sem resolução, ou o cliente pedir um humano, marque escalate e pare de responder até eu limpar a etiqueta.”
conte os turnos com GET /chats/{id}/messages, verifique etiquetas de stop-reply com GET /chats/{id}/labels antes de cada envio. Se a escalação disparar, POST /chats/{id}/labels com escalate e saia.
$ # 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'.Rodar vários agentes especializados na mesma caixa
“O agente de Vendas cuida das perguntas de preço, o de Suporte cuida das perguntas de produto. Roteie por intenção; se os dois ficarem em dúvida, escale para mim.”
dois skills especialistas em um mesmo workspace OpenClaw (um de vendas, um de suporte) mais um terceiro skill classificador de intenção que roda primeiro em cada mensagem recebida, marca o chat com o especialista escolhido e sai. Cada especialista lê a label de intenção antes de responder e aborta se apontar para o outro — assim só um skill dispara por mensagem.
Lembrar de conversas anteriores com o mesmo cliente
“Semana passada você me disse que ia viajar — como foi?”
a própria memória do OpenClaw mais GET /chats/{id}/messages para todo o histórico do WhatsApp. O histórico sobrevive entre invocações porque vive no TimelinesAI.
Teste seu agente end-to-end com um segundo número de WhatsApp
Depois do setup você pode ver as capabilities listadas aqui, mas não consegue ver seu agente realmente trabalhando até que um cliente real mande mensagem pra ele. Esse é um loop ruim pra iterar. Aqui está um melhor: conecte um segundo número de WhatsApp ao mesmo workspace do TimelinesAI, use-o como cliente scripted e observe seu skill real lidar com a conversa end-to-end. Sem mocks, sem webhooks falsos — é seu agente real, sua API real, só que com um segundo número fazendo o papel do humano do outro lado.
O padrão
TimelinesAI permite que vários números de WhatsApp vivam em um só workspace e roteia os webhooks recebidos pelo mesmo receiver. O truque é o campo whatsapp_account_id em todo payload de webhook — ele carrega o JID do número que recebeu a mensagem. Leia, faça switch nele, e despache ou para o passo persona (quando seu agente acabou de responder o cliente) ou para o passo agent (quando o cliente acabou de mandar mensagem para seu agente). Cada lado envia via o POST /messages ou POST /chats/{id}/messages normais da perspectiva do outro.
Arquitetura
[+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
O lado persona é um cliente scripted: uma lista de linhas para dizer a seguir. Quando é hora do próximo turno, POST em /messages com o número de telefone do agente como destinatário. O TimelinesAI cuida do roteamento porque os dois números estão em um mesmo 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 com jid-switch
Um receiver, um branch por lado. Todo o resto que você já sabe do Setup — a auth com ?secret=, o filtro from_me de echoes, o fallback flat-vs-nested do payload — se aplica aqui sem mudanças.
// 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);
}
}Esses dois snippets são a ideia central. O harness completo — scripts de persona, loader de cenários, kickoff por CLI, logger split-pane colorido, config de Vercel — mora em examples/test-harness/ no repositório companheiro, com seu próprio README que guia pelo deploy, env vars, registro de webhook e rodar seu primeiro cenário end-to-end.
Observando acontecer
O harness roda autônomo — não precisa que você aprove cada resposta. Sua supervisão acontece no dashboard do TimelinesAI: abra o chat entre seu número persona e seu número agente em outra aba do navegador e cada turno aparece ao vivo. Se o agente disser algo bobo, mande uma resposta manual da caixa de entrada do agente para redirecionar a conversa. Se você precisa mudar o comportamento no meio de um run, adicione uma label ou escreva uma nota no chat — ambas são visíveis para o skill do agente no próximo turno. Pare o run deletando o webhook registrado ou desligando a função da Vercel; o cenário para imediatamente.
Exemplo: uma volta completa
Um loop concreto de cinco passos mostrando como a API se parece na prática — como seu agente envia uma saída, confirma a entrega, processa a resposta do cliente e manda um follow-up. Os números de telefone, chat IDs e message UIDs abaixo são placeholders — substitua pelos seus.
Placeholders usados ao longo deste exemplo:
Your business number → +1 555 0100 (JID: 15550100@s.whatsapp.net) Your customer's number → +1 555 0200 API token → $TIMELINES_AI_API_KEY
Confirme que seu token funciona e liste os números conectados
$ 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"}
]}}Você deve ver uma lista dos seus números conectados. Se o status não for active, ajuste no dashboard do TimelinesAI antes de continuar.
Envie uma mensagem de saída
Escreva o payload em um arquivo com codificação UTF-8 e depois passe para o curl. Esse é o padrão para todo envio.
$ 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"}}A resposta é um recibo, não uma confirmação de entrega. Guarde o message_uid para o próximo passo.
Verifique o status de entrega
$ 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 geralmente leva um segundo num número ativo. O status Read aparece depois, quando o destinatário realmente abre o chat.
O cliente responde, seu webhook dispara
Quando o cliente responde, o TimelinesAI faz POST para a URL de webhook que você registrou:
{
"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"
}
}Seu receptor responde 200 imediatamente e então passa o payload para o seu skill OpenClaw.
Responda de volta no mesmo 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"}}Como você está enviando dentro de um chat existente via /chats/{id}/messages, o remetente é automaticamente o número de WhatsApp dono do chat — você não escolhe, o registro do chat escolhe. Esse é o motivo pelo qual você não envia acidentalmente do número errado em workspaces multi-número.
Limites e ressalvas
O que este guia NÃO cobre, e por quê.
Guias relacionados
Se você está construindo isso para um canal de cliente real, estas três páginas cobrem território adjacente que você vai tocar mais cedo ou mais tarde.
Conectar vários números de WhatsApp →
Rode vários números pessoais de um workspace só, com roteamento de caixa compartilhada e pinning JID por número.
Integração HubSpot + WhatsApp →
Sincronize conversas com negócios do HubSpot — o complemento mais próximo da capacidade #11 (envios disparados por evento).
Agentes ChatGPT para WhatsApp →
A versão não-OpenClaw da mesma ideia — um autoresponder gerenciado configurado pelo dashboard, sem precisar de skill.
Blocos de construção
Bundle de skills companheiro no GitHub →4 skills funcionando, um receptor de webhook em Vercel, docs de compliance e um mirror completo deste guia. Licença MIT.
Docs da API do TimelinesAI · Docs dos Skills do OpenClaw · release v0.1.0
Guia de capacidades · 2026 · URL canônica timelines.ai/guide/openclaw-whatsapp-skills