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 você pode construir

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.


Comece aqui

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.


Começando

Setup

Quatro coisas para conectar. Depois disso, seu skill só faz chamadas de API.

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

Você 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/webhooks

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


Sua instalação

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.

ArquivoO que controla
SKILL.mdO 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.mdDocumentaçã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, schemasQualquer 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:

ArquivoO que controla
IDENTITY.mdQuem é seu agente — nome, creature (como ele se conceitua), vibe, emoji de assinatura, avatar. Preencha cedo; a maioria dos outros arquivos referencia este.
SOUL.mdPrincí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.mdQuem 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.mdConfig 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.mdO README do workspace para agentes. Descreve como o OpenClaw espera que este workspace seja rodado. Normalmente vem com o install e raramente é editado.
BOOT.mdInstruçõ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.mdConversa 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.mdDefiniçõ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:

ArquivoO 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.
A regra de uma linha: muda como seu agente SE COMPORTA editando o SKILL.md de um skill; muda QUEM é seu agente editando SOUL.md ou IDENTITY.md no workspace; muda QUAIS SEGREDOS ele usa editando o arquivo .env.<integração> correto. Todo o resto do workspace é maquinaria que você raramente toca.

Escolha do canal

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.

Este guia é sobre números pessoais de WhatsApp — aqueles que você conecta ao TimelinesAI escaneando um QR. Servem para conversas recebidas e envios transacionais para clientes que esperam a mensagem. Para cold outreach, broadcasts de marketing ou campanhas promocionais, use WhatsApp Business API — o TimelinesAI suporta hoje pelo dashboard, e a automação por API pública dos fluxos Business API chega no Q2 2026.

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. 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. 2.O cliente está prestes a receber algo que ele explicitamente espera (pedido, compromisso, pagamento, entrega)? → Número pessoal, envio transacional.
  3. 3.Disparado por um evento opt-in do cliente no seu CRM ou ferramenta de billing? → Número pessoal, envio disparado por evento.
  4. 4.Broadcast, cold outreach ou campanha promocional? → Business API, use o dashboard do TimelinesAI.
  5. 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

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étodoRotaO que devolve
GET/whatsapp_accountsSeus números de WhatsApp conectados, cada um com JID, telefone, status e nome de conta.
GET/chatsLista 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}/messagesHistó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}/labelsEtiquetas do chat.
GET/messages/{uid}/status_historyLinha do tempo Sent / Delivered / Read de uma mensagem enviada.
GET/messages/{uid}/reactionsReaçõ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/filesArquivos que você subiu pela API.
GET/webhooksSuas inscrições de webhook registradas.

Escrita

MétodoRotaO que faz
POST/messagesEnvia para um número de telefone. Body: {"phone":"+...","text":"..."}. Devolve {"message_uid":"..."}.
POST/chats/{id}/messagesEnvia dentro de um chat existente. Body: {"text":"..."}. O remetente é o número de WhatsApp dono do chat.
POST/chats/{id}/notesAnexa 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}/labelsAdiciona 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}/reactionsDefine um emoji de reação em uma mensagem. O body leva o caractere emoji literal, não um shortcode.
POST/files_uploadFaz 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/webhooksRegistra uma inscrição de webhook.
PUT/webhooks/{id}Atualiza ou ativa/desativa uma inscrição.
DELETE/webhooks/{id}Remove uma inscrição.

Antes de começar

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.

!
Sem barras finais. GET /chats funciona. GET /chats/ devolve a página 404 do TimelinesAI — que parece um problema de rede e não é. Toda URL deste guia é escrita sem barra final de propósito.
!
Bodies JSON precisam ser UTF-8 válido — e a armadilha vai além dos heredocs. O parser rejeita qualquer outra coisa. Em-dashes e aspas tipográficas são o gatilho mais comum, mas TAMBÉM qualquer shell rodando em locale não-UTF-8 (Git Bash no Windows, algumas imagens Docker base, sessões SSH antigas) corrompe esses caracteres tanto em heredocs QUANTO em args inline curl -d "..." — o em-dash vira byte 0x97, que falha o decode UTF-8 no servidor. Escreva o payload em um arquivo salvo explicitamente como UTF-8 e use curl --data-binary @file.json; nunca confie em -d inline para nada com pontuação tipográfica.
!
A URL base é https://app.timelines.ai/integrations/api. Alguns posts antigos referenciam outro subdomínio com header X-API-KEY — isso está obsoleto. Use Bearer auth em app.timelines.ai/integrations/api.
!
URLs de anexo nos payloads de webhook expiram rápido. Quando um cliente manda foto ou PDF, baixe inline no handler do webhook, não num worker assíncrono.
!
Números pessoais são banidos por cold outreach. Os endpoints de envio deste guia vão te deixar enviar para qualquer um, mas o WhatsApp vai banir seu número rápido por outbound não solicitado. Só envie outbound para gente que te escreveu primeiro ou que explicitamente espera uma mensagem transacional. Para broadcasts, use WhatsApp Business API — veja Escolha do canal.
!
O envio é assíncrono. POST /messages devolve um message_uid — isso é um recibo, não uma confirmação de entrega. Use GET /messages/{uid}/status_history para verificar a entrega real.
!
Formatos de resposta variam — endpoints de lista são aninhados sob uma chave tipada. A maioria dos endpoints de lista devolve {"data":{"<chave-tipada>":[...]}}, não um {"data":[...]} plano. GET /whatsapp_accounts aninha sob data.whatsapp_accounts, GET /chats sob data.chats (com a flag de paginação data.has_more_pages), GET /chats/{id}/messages sob data.messages, GET /chats/{id}/labels sob data.labels. Exceções: GET /messages/{uid}/status_history e GET /files devolvem data como array plano. Código que assume uma forma uniforme quebra no primeiro descasamento — leia como res?.data?.<tipada> ?? res?.data ?? [].
!
Use from_me, não sender_phone, para direção em leituras de histórico. GET /chats/{id}/messages devolve mensagens enviadas (seus próprios envios via API Pública) com sender_phone setado para o número da equipe, não vazio. Detecção por sender_phone != "" marca cada saindo como entrando e inverte a conversa. Confie no booleano from_me — true é saída (seu agente falando), false é entrada (cliente).
!
No histórico o campo é uid; no webhook é message_uid. Mesmo valor, chave diferente. Quando o TL entrega um webhook message:received:new, o payload usa message_uid. Quando você lê o histórico do mesmo chat via GET /chats/{id}/messages, o campo chama uid. É o mesmo identificador. Código que guarda UIDs de webhooks e depois os busca no histórico precisa normalizar ambos os nomes. Mensagens de histórico também usam timestamp, não created_at.
!
Labels são REPLACE, não ADD. POST /chats/{id}/labels recebe {"labels":["needs-human","intent/sales"]} — array, chave plural. A chamada substitui o conjunto inteiro de labels do chat; para adicionar uma label você primeiro faz GET do conjunto existente, adiciona a sua e faz POST da lista combinada. Para limpar tudo: POST {"labels":[]}. Não existe DELETE /chats/{id}/labels/{nome} funcional para remover uma label individual — você faz POST do novo conjunto sem ela.
!
Um webhook recém-registrado pode reproduzir história recente. Nos primeiros eventos depois do POST /webhooks, o TL pode entregar uma rajada de eventos message:received:new passados — ou o handshake de registro pode tentar várias vezes contra um container ainda no cold-start. Em ambos os casos seu receiver precisa ser idempotente desde o evento #1. Deduplique por message_uid desde o início, não assuma “estado limpo” no momento da assinatura.
!
Payloads de webhook podem chegar planos OU aninhados. Às vezes o TL entrega data.chat_id / data.whatsapp_account_id no nível superior e outras vezes aninhados como data.chat.id / data.chat.whatsapp_account_id — depende do tipo de evento e da versão. Desestruture defensivamente: const chatId = data.chat_id ?? data.chat?.id. Os anexos são piores: data.attachment_url, data.attachment?.url e data.file_url são todas possíveis. Um receiver que só lê o formato plano falha silenciosamente em entregas aninhadas.
!
Endpoints de listagem retornam 50 itens por página, fixo. ?limit=N, ?per_page=N e ?page_size=N são silenciosamente ignorados — a API escolhe o próprio tamanho. Pagine com ?page=N (indexado em 1) e pare quando data.has_more_pages for false. Verificado em /chats e /chats/{id}/messages; o padrão se aplica a outras leituras de listas. Se você precisa de “todas as mensagens desde X”, espere percorrer páginas, não uma única chamada grande limit=1000.
!
Desative o autoresponder ChatGPT integrado do TimelinesAI antes do seu agente assumir. Cada chat tem por padrão chatgpt_autoresponse_enabled: true e o responder integrado do TL vai responder a mensagens recebidas junto com seu skill, produzindo respostas duplas que o cliente vê. PATCH /chats/{id} com {"chatgpt_autoresponse_enabled": false} desliga por chat. Não há desligamento em massa na API Pública, então faça disso a primeira ação que seu receiver roda em qualquer chat que está prestes a assumir (e rode uma vez em cada chat que seu agente já está gerenciando).
!
Notas vivem em /chats/{id}/messages com message_type=note e from_me=false, sempre. POST /chats/{id}/notes escreve na mesma timeline das mensagens reais de WhatsApp — quando você lê o histórico, as notas voltam intercaladas com from_me: false independentemente de quem as escreveu. Qualquer código que raciocina sobre fluxo de conversa PRECISA filtrar por .message_type == "whatsapp": um cálculo ingenuo de tempo de resposta que pareia recebido→enviado só por from_me vai tratar cada nota como mensagem recebida do cliente e corromper a latência, e um skill de resumo ou scoring que alimenta o histórico completo para um LLM vai ver suas próprias notas anteriores como falas do cliente. Mantenha as notas no stream só quando você explicitamente quer estado estruturado (o padrão do qualifier); descarte em todo o resto.

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.

!
400 — corpo de requisição inválido. A causa mais comum são caracteres não-UTF-8 no seu JSON (em-dashes, aspas tipográficas de um heredoc de shell). Escreva o payload em um arquivo com codificação UTF-8 explícita e use curl --data-binary @file.json. Mesma raiz do tip de UTF-8 acima.
!
401 — token expirado ou revogado. Rotacione em app.timelines.ai → Integrações → API Pública → Regenerar. O token novo entra em vigor imediatamente; o velho para de funcionar no mesmo instante. Leia do seu environment para conseguir rotacionar sem redeployar.
!
429 — rate limited. A resposta carrega um header Retry-After em segundos. Respeite e tente de novo uma vez. Se você bate em 429 repetidamente, seu skill está enviando mais rápido que o throttle de ~30 mensagens por minuto do número pessoal — freie no cliente ou divida a carga de saída entre vários números conectados.
!
5xx — problema upstream do gateway. Tente de novo com backoff exponencial: 2s, 4s, 8s. Depois da terceira falha, escale para um humano e pare de enviar. Não entre em loop infinito — 5xx persistente normalmente é evento da status page do TimelinesAI, não algo que suas tentativas vão consertar.
!
Idempotência de webhooks. O TimelinesAI tenta cada entrega até 3 vezes com timeout de 5 segundos. Se seu receptor não deduplica, o cliente vê a mesma resposta 3 vezes. Guarde o message_uid recebido como nota do chat antes de processar; se o próximo webhook chegar com o mesmo uid, pule. Notas são baratas e visíveis para humanos no debug.

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

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

!
Debugar um webhook que sumiu em silêncio. Quando seu skill para de responder a uma conversa real, verifique nesta ordem: (1) o log de entrega de webhooks do TimelinesAI — se o evento nem chega na sua URL, o problema é de registro ou DNS, não do seu skill; (2) os logs de função da sua plataforma de hosting (Vercel logs, etc.) por uma exceção não tratada; (3) deixe uma nota de migalha no chat com POST /chats/{id}/notes antes de processar o evento, assim você confirma que o receptor realmente disparou; (4) GET /webhooks para verificar que a inscrição ainda está ativa (rotacionar tokens às vezes derruba inscrições em silêncio).
!
Um único token cobre o workspace todo. Seu token de API lê e escreve cada chat, mensagem, etiqueta, nota, arquivo e webhook do workspace. Trate como senha: guarde no .env, nunca comite, e rotacione em Integrações → API Pública → Regenerar quando alguém sai do time ou um environment de deploy muda. O token novo entra em vigor imediatamente; o velho para de funcionar no mesmo instante. Hoje não existe scope per-skill nem somente leitura.

Contexto e memória

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.

1

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.

2

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.

3

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

!
Filtre notas e seu próprio envio do histórico. GET /chats/{id}/messages devolve tudo que está no chat: turnos recebidos do cliente, suas próprias respostas enviadas e quaisquer notas que o seu skill tenha escrito para estado ou debug. As notas têm message_type == "note" e origin == "Public API" — filtre antes de passar o histórico ao modelo, senão o seu agente vê o próprio bookkeeping interno como se fosse conversa real e começa a responder às próprias notas.
!
Começar no meio de uma conversa. Quando seu skill vê um chat pela primeira vez, esse chat pode já ter um histórico longo de antes de você se inscrever no webhook. Sempre busque contexto recente em um chat novo para você — senão vai responder a um “olá” novo como se fosse uma abertura fresca, quando o cliente está na verdade trinta turnos dentro de um fio existente com o seu time.
!
Deduplique rajadas de webhooks. Quando um cliente digita três mensagens em quatro segundos, você recebe três webhooks — possivelmente em paralelo. Sem um lock por chat ou um debounce curto, o cliente recebe três respostas atropeladas. Padrão: segure o primeiro webhook por 1–2 segundos, busque o histórico uma vez e responda ao estado combinado. Retentativas do webhook (até 3 tentativas, 5 segundos de timeout cada) chegam pelo mesmo caminho — deduplique por message_uid antes de processar.
!
Ordene por created_at, não por message_uid. Os message_uid são de escopo workspace e não são globalmente ordenáveis. Ao montar o histórico no prompt, ordene os turnos pelo timestamp created_at do payload, não pelo UID. UIDs entre workspaces também não são compartilhados — a mesma mensagem física do WhatsApp entregue a dois workspaces diferentes do TimelinesAI tem um UID diferente em cada um.

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:

  1. 1Hardcode o JID de remetente permitido no environment de cada skill (ex.: ALLOWED_SENDER_JID).
  2. 2Antes de enviar, GET /chats/{id} e compare whatsapp_account_id com o seu JID permitido.
  3. 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.

1

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

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.

2

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

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.

3

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

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.

4

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

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

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

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

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

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.

7

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

raciocínio puro do lado do OpenClaw sobre o texto recebido. O TimelinesAI só transporta a resposta.

8

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

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.

9

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

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

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

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.

11

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

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.

12

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

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

Verificar se uma mensagem foi de fato entregue

O João realmente recebeu a mensagem da fatura que mandei hoje de manhã?
How it works

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.

14

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

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

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

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.

16

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

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.

17

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

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

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

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.

19

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

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.

20

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

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

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

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.

22

Lembrar de conversas anteriores com o mesmo cliente

Semana passada você me disse que ia viajar — como foi?
How it works

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.


Testing

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.

Pré-requisito — dois números de WhatsApp conectados. Este padrão precisa de dois números no seu workspace TimelinesAI: um atuando como persona cliente, outro rodando seu stack real de agente. Se você só tem um número conectado agora, pule para a capability #19 (Rascunhos para revisão humana) no lugar — ela te dá um loop de iteração mais lento mas ainda útil onde seu agente rascunha respostas como notas e você aprova no dashboard.

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

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
1

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.

2

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.

3

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.

4

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.

5

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

Limites e ressalvas

O que este guia NÃO cobre, e por quê.

!
Números pessoais são banidos por cold outreach. Broadcasts não solicitados de números pessoais não são um caso de uso suportado. O ban acontece na infraestrutura do WhatsApp, não no gateway do TimelinesAI, então não conseguimos te proteger.
!
A janela de atendimento de 24 horas do WhatsApp. Você pode responder a um cliente livremente por 24 horas depois da última mensagem dele. Fora dessa janela, mensagens precisam de opt-in e templates — território Business API.
!
Entrega assíncrona. POST /messages devolve um message_uid (um recibo), não uma confirmação de entrega. Use /status_history para confirmar a entrega real.
!
URLs de anexo expiram rápido. Baixe a mídia inline no handler do webhook, não num worker atrasado.
!
Só texto, mídia, reações e metadados — sem chamadas de voz/vídeo, sem broadcast-status, sem Channels ou Stories do WhatsApp.

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