This is the abridged developer documentation for TopTalking API # Construa em cima do WhatsApp da sua empresa > Envie mensagens, organize contatos no Kanban, agende lembretes e receba eventos em tempo real. Pelo seu sistema, pela sua automação ou direto pela sua IA. ## Por onde começar [Início rápido](/comecando/inicio-rapido/)Gere a chave, teste com GET /me e envie a sua primeira mensagem em 5 minutos. [Referência da API](/referencia/visao-geral/)Todos os endpoints: conexões por QR Code, contatos, mensagens, grupos, Kanban, campanhas e webhooks. [Webhooks](/webhooks/visao-geral/)Receba mensagens, mudanças de status e de etapa em tempo real, com assinatura HMAC. [MCP para Claude e Cursor](/mcp/visao-geral/)Conecte a sua IA à conta e peça em português: “mande uma mensagem para a Maria”. [Prompts prontos para IA](/ia/visao-geral/)Passo a passo para Lovable, Claude, Cursor, ChatGPT, Bolt, v0, n8n e mais. [llms.txt](/recursos/llms-txt/)A documentação inteira em texto puro, pronta para entregar à sua IA. ## O que dá para construir Mensagens automáticas Confirme pedidos, envie boletos e avise sobre entregas direto do seu sistema, pelo WhatsApp já conectado no painel. CRM sincronizado Crie contatos a partir do seu site e mova cards no Kanban quando a venda avança. Lembretes agendados Agende lembretes de consulta, cobrança ou renovação. Eles aparecem na Agenda e saem sozinhos no horário. Eventos em tempo real Webhooks assinados avisam o seu sistema quando chega mensagem, muda o status ou o card troca de etapa. Agentes de IA Dê ao Claude, ao Cursor ou ao seu próprio agente acesso seguro às conversas, com chave somente leitura se quiser. Automação sem código Use n8n, Make ou Zapier com requisições HTTP e gatilhos de webhook, sem escrever um servidor. # Introdução > O que é a API da TopTalking, o que dá para construir com ela e os conceitos principais (organização, canal, contato, mensagem, etapa do Kanban e mensagem agendada). A **TopTalking** é uma plataforma de atendimento no WhatsApp com CRM (Kanban) e agentes de IA. A **API da TopTalking** deixa o seu sistema — ou a sua IA — fazer pelo código o que a equipe faz no painel: enviar mensagens, criar e atualizar contatos, ler o histórico de conversas, mover cards no Kanban, agendar mensagens e receber eventos em tempo real. ## O que dá para construir * **Avisos automáticos:** confirmação de pedido, boleto, código de rastreio, lembrete de consulta. * **Captura de leads:** quem preenche o formulário do seu site vira contato no painel, já na etapa certa do Kanban, e recebe uma mensagem de boas-vindas. * **Sincronização com o seu CRM ou ERP:** quando o card muda de etapa na TopTalking, o seu sistema fica sabendo (webhook) — e vice-versa. * **Painéis e relatórios próprios:** liste conversas, etapas e mensagens para montar o seu dashboard. * **Agentes de IA:** conecte o Claude, o Cursor ou o ChatGPT à sua conta para consultar conversas e enviar mensagens com supervisão. * **Automações sem código:** n8n, Make e Zapier conversam com a API por HTTP e recebem os webhooks. ## Quatro jeitos de integrar [API REST](/referencia/visao-geral/)Endpoints HTTP + JSON em https\://platform.toptalking.com.br/v1. [Webhooks](/webhooks/visao-geral/)A TopTalking avisa o seu sistema quando algo acontece (mensagem recebida, etapa alterada…). [Servidor MCP](/mcp/visao-geral/)Ferramentas prontas para Claude, Cursor, VS Code e Windsurf usarem a sua conta. [llms.txt e prompts](/ia/visao-geral/)A documentação em texto puro e prompts prontos para Lovable, Claude, ChatGPT e outros. ## Conceitos | Conceito | O que é | Objeto na API | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | **Organização** | A empresa (conta) na TopTalking. Toda chave de API pertence a uma organização e só enxerga os dados dela. | `organization` (em [`GET /me`](/referencia/conta/)) | | **Canal** | Por onde as conversas acontecem: um **número de WhatsApp** conectado por QR Code (`whatsapp`) ou o **Chat do site** (`webchat`). Só canais com status `connected` enviam mensagens. Dá para [conectar um WhatsApp pela API](/referencia/canais/#conectar-um-whatsapp-por-qr-code). | [`channel`](/referencia/canais/) | | **Contato** | Uma **conversa** da caixa de entrada: um por número de telefone e canal. Guarda nome, telefone, e-mail, etapa do Kanban, responsável, agente de IA e se a IA está ligada. | [`contact`](/referencia/contatos/) | | **Grupo** | Um grupo do WhatsApp. Também é uma conversa: o `id` do grupo é um `contact_id`. | [`group`](/referencia/grupos/) | | **Mensagem** | Uma mensagem da conversa, recebida (`inbound`) ou enviada (`outbound`), com texto e/ou mídia e status de entrega. | [`message`](/referencia/mensagens/) | | **Etapa** | Uma coluna do Kanban (funil). Cada contato está em no máximo uma etapa. A etapa final (`is_final: true`, “Finalizado”) encerra a conversa. | [`stage`](/referencia/kanban/) | | **Mensagem agendada** | Uma mensagem programada para sair em data e hora definidas — a mesma da **Agenda** do painel. | [`scheduled_message`](/referencia/mensagens-agendadas/) | | **Membro** | Uma pessoa da equipe (dono, administrador ou atendente). Pode ser responsável por conversas. | [`member`](/referencia/equipe/) | | **Agente de IA** | Um assistente que responde as conversas sozinho. Um deles é o padrão da organização. | [`ai_agent`](/referencia/agentes-ia/) | | **Campanha** | Um disparo para muitos contatos pelo **WhatsApp Oficial** (API da Meta), com um template aprovado e revisão da TopTalking. | [`campaign`](/referencia/campanhas/) | | **Webhook** | Um endereço HTTPS do seu sistema que recebe eventos da conta. | [`webhook_endpoint`](/referencia/webhooks/) | | **Chave de API** | A credencial (`tt_live_...`) que o seu backend usa para chamar a API e o MCP. | `api_key` (em [`GET /me`](/referencia/conta/)) | Tudo o que a API faz aparece no painel como se a equipe tivesse feito: uma mensagem enviada pela API aparece no chat, um contato criado pela API aparece na caixa de entrada e um card movido pela API muda de coluna no Kanban. ## Convenções * **URL base:** `https://platform.toptalking.com.br/v1` * **Formato:** JSON em UTF-8, nos pedidos e nas respostas (`Content-Type: application/json`). * **Datas:** ISO 8601 com fuso horário (ex.: `2026-11-11T14:30:00Z` ou `2026-11-11T11:30:00-03:00`). * **IDs:** UUID (ex.: `3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60`). * **Tipo do objeto:** todo recurso tem o campo `object` (`contact`, `message`, `list`…), útil para saber o que você recebeu. * **Telefones:** envie com DDI e DDD. A formatação é ignorada (só os dígitos contam) e números brasileiros sem `55` recebem o `55`. As respostas trazem só dígitos, ex.: `5511999998888`. * **Rastreio:** toda resposta tem o cabeçalho `X-Request-Id`. Guarde-o nos seus logs — ele ajuda o suporte a encontrar a sua requisição. A chave fica no servidor A API foi feita para ser chamada **de um backend**. Ela não aceita chamadas do navegador (não tem CORS, de propósito), porque a chave exposta no front-end daria a qualquer pessoa acesso ao WhatsApp da sua empresa. Veja [Autenticação e chaves](/comecando/autenticacao/). ## Próximo passo Siga o [Início rápido](/comecando/inicio-rapido/) para gerar a chave e enviar a sua primeira mensagem. # Início rápido > Gere a chave de API, teste com GET /me e envie a sua primeira mensagem de WhatsApp pela API da TopTalking com curl, JavaScript ou Python. Em cinco minutos você vai gerar uma chave, confirmar que ela funciona e mandar uma mensagem de WhatsApp pela API. **Antes de começar:** você precisa ser **administrador** da organização na TopTalking e ter um **WhatsApp conectado** — no painel ([Configurações → Conexões](https://toptalking.com.br/configuracoes/conexao)) ou pela própria API (veja abaixo). ## 1. Gere a chave de API 1. Abra o painel em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor). 2. No cartão **Chaves de API**, clique em **Criar chave**. 3. Dê um nome que diga quem vai usar a chave (ex.: “Integração do site”), escolha o acesso **Acesso total** e a validade. 4. Copie a chave (`tt_live_...`) e guarde em um lugar seguro. **Ela é mostrada uma única vez.** ## 2. Guarde a chave numa variável de ambiente Nunca cole a chave direto no código. Use uma variável de ambiente (no servidor, um “secret”): * macOS / Linux ```bash export TOPTALKING_API_KEY="tt_live_SUA_CHAVE" ``` * Windows (PowerShell) ```powershell $env:TOPTALKING_API_KEY = "tt_live_SUA_CHAVE" ``` ## 3. Teste a chave com `GET /me` Quer ver a resposta sem abrir o terminal? Em [Conta](/referencia/conta/), clique em **Testar**, cole a chave e envie: a resposta real da API aparece na própria página. Todos os endpoints da [referência](/referencia/visao-geral/#teste-direto-na-documenta%C3%A7%C3%A3o) têm esse botão. * curl ```bash curl https://platform.toptalking.com.br/v1/me \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript teste.mjs ```js // Node.js 18+ (rode com: node teste.mjs) const res = await fetch('https://platform.toptalking.com.br/v1/me', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); console.log(res.status, await res.json()); ``` * Python teste.py ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/me", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) print(res.status_code, res.json()) ``` Se a chave estiver certa, a resposta é `200` com os dados da organização e da chave: ```json { "object": "account", "organization": { "id": "e044a615-e879-44bb-8510-948b10b5e93c", "name": "Loja Aurora" }, "api_key": { "id": "508a23eb-5e24-4997-b928-3fce4f0f5593", "name": "Integração do site", "prefix": "tt_live_a1b2c3d4", "access": "full", "created_at": "2026-11-11T13:02:11Z" }, "rate_limit": { "limit": 120, "remaining": 119, "reset_at": "2026-11-11T14:31:00Z" } } ``` Recebeu `401`? Confira se o cabeçalho está exatamente como `Authorization: Bearer tt_live_...` (com a palavra `Bearer` e um espaço). Veja [Erros](/comecando/erros/). ## 4. Envie a sua primeira mensagem Use o **seu próprio número** para testar. Informe o telefone com DDI e DDD: * curl ```bash curl https://platform.toptalking.com.br/v1/messages \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: teste-primeira-mensagem-001" \ -d '{ "to": "5511999998888", "text": "Olá! Esta é a minha primeira mensagem pela API da TopTalking. 🚀" }' ``` * JavaScript enviar.mjs ```js const res = await fetch('https://platform.toptalking.com.br/v1/messages', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'teste-primeira-mensagem-001', }, body: JSON.stringify({ to: '5511999998888', text: 'Olá! Esta é a minha primeira mensagem pela API da TopTalking. 🚀', }), }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); console.log('Mensagem enviada:', body.id, body.status); ``` * Python enviar.py ```python import os import requests res = requests.post( "https://platform.toptalking.com.br/v1/messages", headers={ "Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}", "Idempotency-Key": "teste-primeira-mensagem-001", }, json={ "to": "5511999998888", "text": "Olá! Esta é a minha primeira mensagem pela API da TopTalking. 🚀", }, timeout=30, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") print("Mensagem enviada:", body["id"], body["status"]) ``` Resposta `201`: ```json { "id": "03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Olá! Esta é a minha primeira mensagem pela API da TopTalking. 🚀", "media": null, "status": "sent", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:30:12Z", "edited_at": null } ``` Pronto: a mensagem chegou no WhatsApp e já aparece no chat do painel. Se o número ainda não era contato, o contato foi criado e a conversa está na caixa de entrada. ## Próximos passos * Conecte mais números de WhatsApp pela API: [Canais e conexões](/referencia/canais/). * Receba as respostas dos clientes com [webhooks](/webhooks/visao-geral/). * Veja tudo o que dá para fazer na [Referência da API](/referencia/visao-geral/). * Vai integrar com Lovable, Claude, Cursor ou ChatGPT? Comece por [Integrar com IA](/ia/visao-geral/). * Antes de colocar em produção, leia [Boas práticas no WhatsApp](/comecando/boas-praticas-whatsapp/). # Autenticação e chaves > Como criar, usar, proteger, expirar e revogar chaves de API da TopTalking (tt_live_...), os níveis de acesso (total ou somente leitura) e por que a chave nunca pode ir para o navegador. Toda chamada à API e ao servidor MCP é autenticada com uma **chave de API** da sua organização. A chave começa com `tt_live_`. ## Como enviar a chave Use o cabeçalho `Authorization` com o esquema `Bearer`: ```http Authorization: Bearer tt_live_SUA_CHAVE ``` Se a ferramenta que você usa só permite um cabeçalho “API key” com nome livre, também aceitamos: ```http X-Api-Key: tt_live_SUA_CHAVE ``` ## Criar uma chave Só **administradores** da organização criam e revogam chaves. As chaves estão disponíveis em **todos os planos**. 1. No painel, abra [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor). 2. No cartão **Chaves de API**, clique em **Criar chave**. 3. Preencha: * **Nome:** quem ou o que vai usar a chave (ex.: “Integração do ERP”, “Claude do time de vendas”). Ajuda a saber o que revogar depois. * **Acesso:** **Acesso total** ou **Somente leitura** (veja abaixo). * **Validade:** sem validade, 30 dias, 90 dias ou 1 ano. Depois da validade a chave para de funcionar sozinha. 4. Copie a chave e guarde num cofre de senhas ou nos “secrets” do seu servidor. **A chave completa aparece uma única vez** — a TopTalking guarda só uma impressão digital (hash) dela. Se perder, revogue e crie outra. Você pode ter até **25 chaves ativas** por organização. Use uma chave por sistema, automação ou pessoa: assim dá para revogar uma sem derrubar as outras. ## Níveis de acesso | Acesso | Valor em `GET /me` | O que pode fazer | | ------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- | | **Acesso total** | `full` | Tudo: ler, enviar mensagens, criar e alterar contatos, mover cards, agendar, gerenciar webhooks. | | **Somente leitura** | `read` | Só requisições `GET`. Qualquer outro método (`POST`, `PATCH`, `DELETE`) devolve `403` com o código `read_only_key`. | No [servidor MCP](/mcp/visao-geral/), uma chave somente leitura só expõe as ferramentas de consulta. É a escolha certa para dashboards, relatórios e para **explorar a API com uma IA** sem risco de ela mandar mensagens. ## Validade e revogação * **Validade:** quando a data passa, a chave deixa de funcionar e a API responde `401 expired_api_key`. * **Revogar:** no cartão **Chaves de API**, clique em **Revogar chave**. O efeito é imediato e não tem volta: a API passa a responder `401 revoked_api_key`. * O painel mostra quando cada chave foi **usada pela última vez** — útil para achar chaves esquecidas. Revogue a chave na hora se ela vazar (por exemplo, se foi parar num repositório público ou num print). ## A chave nunca vai para o navegador Importante A chave dá acesso ao WhatsApp da empresa: com ela, qualquer pessoa pode ler conversas e mandar mensagens em seu nome. **Nunca** coloque a chave em código que roda no navegador ou no celular do usuário (React, Vue, Angular, HTML, JavaScript do site, app mobile, extensão de navegador). Por isso a API **não aceita chamadas do navegador** (não envia cabeçalhos CORS, de propósito). Se você vir um erro de CORS no console, não é bug: é a API protegendo a sua conta. A solução é sempre a mesma — chamar a API **de um backend**: | Onde está o seu app | Onde colocar a chamada à API | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Lovable / Supabase | Uma **Supabase Edge Function**, com a chave guardada como secret `TOPTALKING_API_KEY`. Veja o [guia do Lovable](/ia/lovable/). | | Next.js / v0 / Vercel | Um **Route Handler** (`app/api/.../route.ts`) ou Server Action, com a chave em variável de ambiente. Veja [Bolt, v0 e Replit](/ia/bolt-v0-replit/). | | Node, Python, PHP… | O seu servidor, com a chave em variável de ambiente. | | Sem código | **n8n, Make ou Zapier**, no nó/módulo HTTP. Veja [n8n, Make e Zapier](/ia/n8n-make-zapier/). | O fluxo seguro fica assim: ```text Navegador do usuário → seu backend (guarda a chave) → API da TopTalking (sem chave) Edge Function / rota / n8n ``` ### Exemplo: Supabase Edge Function como ponte O front-end chama a sua função (com o login do usuário, se houver); a função chama a TopTalking com a chave guardada como secret: * Edge Function (Deno) supabase/functions/toptalking-contatos/index.ts ```ts const TOPTALKING_API = 'https://platform.toptalking.com.br/v1'; const cors = { 'Access-Control-Allow-Origin': '*', // troque pelo domínio do seu app em produção 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; Deno.serve(async (req) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: cors }); // A chave fica só aqui, no servidor (Secrets da Supabase / Lovable Cloud). const apiKey = Deno.env.get('TOPTALKING_API_KEY'); if (!apiKey) return new Response('TOPTALKING_API_KEY não configurada', { status: 500, headers: cors }); const res = await fetch(`${TOPTALKING_API}/contacts?limit=20`, { headers: { Authorization: `Bearer ${apiKey}` }, }); return new Response(await res.text(), { status: res.status, headers: { ...cors, 'Content-Type': 'application/json' }, }); }); ``` * Front-end (React) src/lib/contatos.ts ```ts import { supabase } from '@/integrations/supabase/client'; // O navegador chama a SUA função, nunca a TopTalking direto. export async function listarContatos() { const { data, error } = await supabase.functions.invoke('toptalking-contatos'); if (error) throw error; return data; // { object: "list", data: [...], has_more, next_cursor } } ``` Não crie um “proxy aberto” A função acima só lista contatos. Evite funções que repassam **qualquer** caminho ou corpo para a TopTalking: qualquer visitante do seu site conseguiria usar a sua chave. Faça uma função por ação (ex.: “enviar boas-vindas”), valide os dados e, se o app tiver login, exija o usuário logado. ## Boas práticas com chaves * Guarde a chave em variável de ambiente ou cofre de segredos, nunca no código nem no Git. * Use **uma chave por integração**, com nome claro, e **Somente leitura** sempre que não precisar escrever. * Prefira chaves com **validade** para testes e integrações temporárias. * Para agentes de IA (MCP, GPTs), comece com uma chave **Somente leitura**. * Revogue chaves que não aparecem como usadas há muito tempo. ## Erros de autenticação | HTTP | `code` | Quando acontece | O que fazer | | ---- | ----------------- | ------------------------------------------------------------- | --------------------------------------------------------- | | 401 | `missing_api_key` | Nenhuma chave foi enviada. | Envie `Authorization: Bearer tt_live_...`. | | 401 | `invalid_api_key` | A chave não existe ou foi digitada errada. | Copie a chave de novo (sem espaços extras) ou crie outra. | | 401 | `revoked_api_key` | A chave foi revogada no painel. | Crie uma chave nova. | | 401 | `expired_api_key` | A validade da chave acabou. | Crie uma chave nova. | | 403 | `read_only_key` | Chave **Somente leitura** tentou `POST`, `PATCH` ou `DELETE`. | Use uma chave com **Acesso total**. | Veja todos os códigos em [Erros](/comecando/erros/). # Erros > Formato dos erros da API da TopTalking, todos os códigos (code) com o status HTTP, quando acontecem e o que fazer, e como usar o X-Request-Id. A API usa os status HTTP de sempre: `2xx` deu certo, `4xx` tem algo a corrigir na requisição e `5xx` é problema do nosso lado ou do WhatsApp. ## Formato Todo erro tem o mesmo corpo JSON: ```json { "error": { "type": "invalid_request_error", "code": "missing_field", "message": "Informe `to` ou `contact_id`.", "param": "to", "request_id": "req_6f1d2c3b4a5e" } } ``` | Campo | Descrição | | ------------ | --------------------------------------------------------------------------------------- | | `type` | Categoria do erro (veja abaixo). | | `code` | Código estável e específico. **Use este campo no seu código** para decidir o que fazer. | | `message` | Explicação em português para humanos. Pode mudar de texto; não compare strings com ele. | | `param` | Campo que causou o erro, quando houver (ex.: `to`, `send_at`). | | `request_id` | Identificador da requisição — o mesmo valor do cabeçalho `X-Request-Id`. | Toda resposta (de sucesso ou de erro) traz o cabeçalho **`X-Request-Id`**. Registre-o nos seus logs e envie para o suporte quando precisar de ajuda com uma chamada específica. ## Tipos (`type`) | `type` | Significado | | ----------------------- | ------------------------------------------------------------------- | | `invalid_request_error` | A requisição tem algum problema (JSON, campos, parâmetros). | | `authentication_error` | Chave ausente, inválida, revogada ou expirada. | | `permission_error` | A chave não tem permissão para a ação (ex.: chave somente leitura). | | `not_found_error` | O recurso ou a rota não existe. | | `conflict_error` | Conflito de estado ou de `Idempotency-Key`. | | `channel_error` | Problema com o canal de envio (ex.: WhatsApp desconectado). | | `rate_limit_error` | Limite de requisições por minuto excedido. | | `api_error` | Erro inesperado do nosso lado. | ## Códigos (`code`) | HTTP | `code` | Quando acontece | O que fazer | | ---------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `invalid_json` | O corpo não é um JSON válido. | Confira aspas, vírgulas e o cabeçalho `Content-Type: application/json`. | | 400 | `missing_field` | Faltou um campo obrigatório (`param` diz qual). | Envie o campo. Ex.: `to` ou `contact_id` em `POST /messages`. | | 400 | `invalid_field` | Um campo tem valor ou formato inválido (`param` diz qual). | Corrija o valor: UUID malformado, data no passado, texto longo demais, `limit` fora de 1–100… | | 400 | `invalid_phone` | O telefone não parece um número válido. | Envie com DDI e DDD, ex.: `5511999998888`. | | 401 | `missing_api_key` | Nenhuma chave foi enviada. | Envie `Authorization: Bearer tt_live_...`. | | 401 | `invalid_api_key` | Chave inexistente ou digitada errada. | Copie a chave de novo ou crie outra. | | 401 | `revoked_api_key` | A chave foi revogada. | Crie uma chave nova no painel. | | 401 | `expired_api_key` | A validade da chave acabou. | Crie uma chave nova no painel. | | 403 | `read_only_key` | Chave somente leitura tentou escrever (`POST`, `PATCH`, `DELETE`). | Use uma chave com acesso total. | | 404 | `resource_not_found` | O ID não existe (ou é de outra organização). | Confira o ID. Ex.: o contato foi apagado no painel. | | 404 | `route_not_found` | O caminho não existe. | Confira a URL: ela começa com `/v1/` e usa os nomes em inglês (`/contacts`, `/messages`…). | | 405 | `method_not_allowed` | O caminho existe, mas não aceita esse método. | Ex.: `PUT` em vez de `PATCH`. Veja a [referência](/referencia/visao-geral/). | | 409 | `idempotency_conflict` | A mesma `Idempotency-Key` foi usada com um corpo diferente. | Gere uma chave nova para cada operação diferente. Veja [Idempotência](/comecando/idempotencia/). | | 409 | `invalid_state` | A ação não é possível no estado atual do recurso. | Ex.: cancelar uma mensagem agendada que já foi enviada, gerar QR Code para um WhatsApp já conectado, editar uma mensagem depois de 15 minutos, pausar uma campanha que não está enviando. Busque o recurso e confira o `status`. | | 422 | `channel_unavailable` | Não há WhatsApp conectado para enviar (ou o canal escolhido está desconectado). Em campanhas: não há número do WhatsApp Oficial conectado. | Reconecte o canal com [`POST /channels/{channel_id}/connect`](/referencia/canais/#reconectar-um-whatsapp-desconectado) (ou no painel, em Configurações → Conexões). Para campanhas, conecte o WhatsApp Oficial no painel, em Campanhas. | | 422 | `limit_reached` | Um limite da conta foi atingido: o máximo de **conexões de WhatsApp do plano** (`POST /channels`) ou 10 webhooks por organização (`POST /webhooks`). | Remova itens que não usa mais (ou reconecte um canal existente em vez de criar outro). Para mais conexões, faça upgrade do plano no painel. | | 429 | `rate_limit_exceeded` | Mais de 120 requisições no mesmo minuto com a mesma chave. | Espere os segundos do cabeçalho `Retry-After`. Veja [Limites de uso](/comecando/limites/). | | 500 ou 502 | `internal_error` | Erro inesperado do nosso lado. | Tente de novo com espera progressiva. Se persistir, fale com o suporte com o `request_id`. | | 502 | `provider_error` | O WhatsApp recusou a operação. Num envio, a mensagem fica registrada com status `failed`. Ao conectar, o WhatsApp não devolveu o QR Code ou o código de pareamento. | Leia a `message`. Confira se o número tem WhatsApp e se a mídia é acessível. Não repita em loop. | | 503 | `service_unavailable` | A API está temporariamente indisponível. | Tente de novo com espera progressiva. Use `Idempotency-Key` em `POST`. | | 504 | `timeout` | A requisição demorou demais para ser respondida (ex.: o WhatsApp demorou para gerar o QR Code). | Tente de novo com a mesma `Idempotency-Key`: se a primeira tiver dado certo, você recebe a resposta original. | Repare que há dois `422` com `type` diferentes: `limit_reached` é `invalid_request_error` e `channel_unavailable` é `channel_error`. 422 limit_reached (conexões do plano) ```json { "error": { "type": "invalid_request_error", "code": "limit_reached", "message": "Seu plano permite 3 conexões de WhatsApp. Remova uma ou faça upgrade no painel.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Como tratar os erros no código * **Decida pelo `code`**, não pela `message`. * **Erros `4xx` não melhoram com retentativa** (exceto `429`): corrija a requisição. * **`429`, `500`, `503`, `504` e falhas de rede podem ser repetidos** com espera progressiva (ex.: 1 s, 2 s, 4 s…). Envie `Idempotency-Key` em `POST` para não duplicar nada. * **`502 provider_error`** é uma recusa do WhatsApp para aquela mensagem: repetir em sequência raramente ajuda e pode prejudicar o número. Exemplo (Node.js) ```js async function toptalking(path, options = {}) { const res = await fetch(`https://platform.toptalking.com.br/v1${path}`, { ...options, headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', ...options.headers, }, }); const body = await res.json(); if (!res.ok) { const { code, message, request_id } = body.error; const err = new Error(`[${res.status} ${code}] ${message} (request_id: ${request_id})`); err.status = res.status; err.code = code; err.retryAfter = Number(res.headers.get('Retry-After')) || null; throw err; } return body; } ``` # Limites de uso > Limite de 120 requisições por minuto por chave, cabeçalhos X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset e Retry-After, e outros limites da API da TopTalking. ## Requisições por minuto Cada chave de API pode fazer **120 requisições por minuto**. A contagem é por chave: duas integrações com chaves diferentes têm limites independentes. As respostas trazem o estado do limite nos cabeçalhos: | Cabeçalho | Exemplo | Significado | | ----------------------- | ------------ | ------------------------------------------------------- | | `X-RateLimit-Limit` | `120` | Requisições permitidas por minuto para esta chave. | | `X-RateLimit-Remaining` | `87` | Quantas ainda restam na janela atual. | | `X-RateLimit-Reset` | `1794407460` | Quando a janela reinicia, em Unix timestamp (segundos). | O mesmo estado aparece em [`GET /me`](/referencia/conta/), no objeto `rate_limit`. ## Quando o limite estoura A API responde `429` com o código `rate_limit_exceeded` e o cabeçalho **`Retry-After`** com quantos segundos esperar: ```http HTTP/1.1 429 Too Many Requests Retry-After: 18 X-RateLimit-Limit: 120 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1794407460 ``` ```json { "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Limite de requisições por minuto excedido.", "request_id": "req_9a8b7c6d5e4f" } } ``` Espere o tempo do `Retry-After` e tente de novo: Retentativa respeitando o Retry-After (Node.js) ```js async function comRetentativa(fazerRequisicao, tentativas = 3) { for (let i = 0; i < tentativas; i++) { const res = await fazerRequisicao(); if (res.status !== 429) return res; const espera = Number(res.headers.get('Retry-After') ?? 1); await new Promise((r) => setTimeout(r, espera * 1000)); } throw new Error('Limite de requisições excedido várias vezes seguidas.'); } ``` ## Como gastar menos requisições * **Use webhooks em vez de consultar a API em loop.** Em vez de perguntar a cada segundo “chegou mensagem?”, deixe a TopTalking [avisar o seu sistema](/webhooks/visao-geral/). * **Peça páginas maiores** com `limit=100` quando for ler muitos itens. * **Guarde em cache** o que muda pouco, como [etapas do Kanban](/referencia/kanban/) e [canais](/referencia/canais/). * **Espalhe envios em lote** ao longo do tempo — é melhor para o limite e, principalmente, para a saúde do seu número de WhatsApp. Veja [Boas práticas no WhatsApp](/comecando/boas-praticas-whatsapp/). ## Outros limites | Item | Limite | | ------------------------------------- | ------------------- | | Chaves de API ativas | 25 por organização | | Webhooks | 10 por organização | | Itens por página (`limit`) | 1 a 100 (padrão 20) | | Texto de uma mensagem | 4096 caracteres | | Anexos de uma mensagem agendada | 10 | | Antecedência de uma mensagem agendada | até 365 dias | | Validade de uma `Idempotency-Key` | 24 horas | | Histórico de eventos de webhook | 30 dias | # Paginação > Como funciona a paginação por cursor da API da TopTalking (object list, data, has_more, next_cursor, limit e cursor) com exemplos para percorrer todas as páginas. Os endpoints que listam itens (contatos, mensagens, mensagens agendadas, entregas de webhook…) usam **paginação por cursor**. ## Formato da lista ```json { "object": "list", "data": [ { "id": "721bfe94-e549-4354-b90c-a8d679e9b271", "object": "contact", "name": "João Pereira", "phone": "5521987654321", "email": null, "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": null, "ai_enabled": true, "ai_agent_id": null, "assigned_to": null, "pinned": false, "unread_count": 0, "last_message_at": "2026-11-11T14:30:12Z", "last_message_preview": "Combinado, obrigado!", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-09-14T11:20:00Z", "updated_at": "2026-11-11T14:30:12Z" } ], "has_more": true, "next_cursor": "eyJ0IjoiMjAyNi0xMS0xMVQxNDozMDoxMloiLCJpZCI6IjcyMWJmZTk0In0" } ``` (Exemplo com `limit=1`: há mais itens, então `has_more` é `true` e `next_cursor` vem preenchido.) | Campo | Descrição | | ------------- | -------------------------------------------------------- | | `object` | Sempre `list`. | | `data` | Os itens desta página. | | `has_more` | `true` se existem mais itens depois desta página. | | `next_cursor` | Valor para buscar a próxima página, ou `null` na última. | ## Parâmetros | Parâmetro | Tipo | Descrição | | --------- | ------- | ------------------------------------------------- | | `limit` | inteiro | Itens por página, de **1 a 100**. Padrão: **20**. | | `cursor` | string | O `next_cursor` da página anterior. | O cursor é **opaco**: não tente montar nem interpretar o valor, só repasse o que a API devolveu. Mantenha os mesmos filtros (ex.: `search`, `stage_id`) ao pedir a próxima página. ## Percorrer todas as páginas * JavaScript ```js async function listarTodosOsContatos() { const contatos = []; let cursor = null; do { const url = new URL('https://platform.toptalking.com.br/v1/contacts'); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const page = await res.json(); if (!res.ok) throw new Error(page.error.message); contatos.push(...page.data); cursor = page.has_more ? page.next_cursor : null; } while (cursor); return contatos; } ``` * Python ```python import os import requests def listar_todos_os_contatos(): contatos, cursor = [], None headers = {"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"} while True: params = {"limit": 100} if cursor: params["cursor"] = cursor res = requests.get( "https://platform.toptalking.com.br/v1/contacts", headers=headers, params=params, timeout=30, ) res.raise_for_status() page = res.json() contatos.extend(page["data"]) if not page["has_more"]: return contatos cursor = page["next_cursor"] ``` * curl ```bash # Primeira página curl "https://platform.toptalking.com.br/v1/contacts?limit=100" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" # Próxima página: passe o next_cursor recebido curl "https://platform.toptalking.com.br/v1/contacts?limit=100&cursor=eyJ0IjoiMjAyNi0xMS0xMVQxNDozMDoxMloiLCJpZCI6IjcyMWJmZTk0In0" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` # Idempotência > Use o cabeçalho Idempotency-Key nas requisições POST da API da TopTalking para repetir chamadas com segurança, sem enviar mensagens duplicadas (essencial para agentes de IA e automações). Redes falham. Às vezes o seu servidor manda a requisição, a TopTalking envia a mensagem, mas a resposta se perde no caminho. Se você simplesmente repetir, o cliente recebe a mensagem **duas vezes**. O cabeçalho **`Idempotency-Key`** resolve isso: ele identifica a operação, e repetições com a mesma chave não são executadas de novo. ## Como funciona * Envie `Idempotency-Key: ` em requisições **`POST`** (é opcional). * Se a mesma chave chegar de novo **com o mesmo corpo em até 24 horas**, a API devolve **a resposta original guardada**, sem executar a ação de novo. * Se a mesma chave chegar com um **corpo diferente**, a API responde `409` com o código `idempotency_conflict`. * O valor pode ter até 255 caracteres. Depois de 24 horas, a chave pode ser reutilizada. ## Exemplo * curl ```bash curl https://platform.toptalking.com.br/v1/messages \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-1234-saiu-para-entrega" \ -d '{ "to": "5511999998888", "text": "Seu pedido #1234 saiu para entrega." }' ``` * JavaScript ```js await fetch('https://platform.toptalking.com.br/v1/messages', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', // Mesmo pedido + mesmo evento = mesma chave, em qualquer retentativa. 'Idempotency-Key': `pedido-${pedido.id}-saiu-para-entrega`, }, body: JSON.stringify({ to: pedido.telefone, text: `Seu pedido #${pedido.id} saiu para entrega.`, }), }); ``` ## Como escolher a chave A chave deve representar **a intenção**, não a tentativa: | Situação | Boa chave | Evite | | ----------------------------------- | --------------------------------------- | --------------------------------------------------- | | Aviso de pedido despachado | `pedido-1234-despachado` | Um UUID novo a cada tentativa (não protege nada) | | Boas-vindas a um lead do formulário | `lead--boas-vindas` | O horário atual | | Lembrete agendado de consulta | `consulta-987-lembrete-24h` | A mesma chave para clientes diferentes (gera `409`) | | Fluxo do n8n com retentativa | `n8n--<índice do item>` | Uma chave fixa para todo o fluxo | Se a sua operação não tem um ID natural, gere um UUID **uma vez**, guarde junto com a tarefa e reutilize-o em todas as tentativas. ## Quando usar * **Sempre** em `POST /messages` e `POST /scheduled-messages` feitos por automações, filas ou agentes de IA — eles costumam repetir chamadas quando algo demora. * Em `POST /contacts` e `POST /webhooks`, quando o seu código faz retentativas automáticas. # Boas práticas no WhatsApp > Como enviar mensagens pela API da TopTalking sem arriscar o banimento do número de WhatsApp - opt-in, ritmo de envio, conteúdo e quando usar Campanhas. A conexão de WhatsApp da TopTalking por QR code usa o **WhatsApp do seu celular** — não é a API oficial da Meta. Isso dá liberdade, mas exige cuidado: **o WhatsApp pode banir números que enviam mensagens em massa ou não solicitadas**, e um número banido perde as conversas e o histórico com os clientes. Risco real de banimento Não use `POST /messages` para disparos em massa, listas compradas ou mensagens para quem nunca falou com a sua empresa. Para comunicação em massa, use **[Campanhas](/referencia/campanhas/)**, que saem pelo WhatsApp Oficial (API da Meta) com templates aprovados — pelo painel ou pela API. ## Envie só para quem pediu * **Opt-in:** mande mensagens apenas para quem deu o número e aceitou receber contato (formulário com consentimento, compra, conversa iniciada pelo cliente). * **Contexto claro:** a mensagem deve deixar óbvio por que a pessoa está recebendo (ex.: “Olá, Maria! Aqui é a Loja Aurora, sobre o seu pedido #1234…”). * **Saída fácil:** respeite quem pedir para não receber mais. Uma boa prática é [arquivar](/referencia/contatos/#atualizar-contato-e-mover-no-kanban) o contato ou marcá-lo no seu sistema para não entrar em novos envios. ## Ritmo de envio * **Espalhe os envios.** Nada de 500 mensagens no mesmo minuto. Coloque um intervalo entre as mensagens (vários segundos, com alguma variação) e prefira enviar ao longo do dia. * **Comece devagar** com números novos ou recém-conectados. * **Prefira responder a iniciar.** Mensagens que respondem a uma conversa do cliente são as mais seguras. * **Evite textos idênticos** para muitas pessoas: personalize com nome e contexto. ## Conteúdo * Mensagens curtas, úteis e esperadas (confirmação, status, lembrete, resposta). * Cuidado com links encurtados e muitos links por mensagem. * Use a formatação do WhatsApp com moderação: `*negrito*`, `_itálico_`, `~tachado~`. * Arquivos e imagens: use URLs HTTPS públicas e estáveis. ## Sinais de alerta Pare e revise a estratégia se: * muitas mensagens ficam em `sent` e nunca chegam a `delivered`; * os envios começam a falhar com `502 provider_error`; * clientes reclamam ou bloqueiam o número; * o WhatsApp desconecta com frequência (a API passa a responder `422 channel_unavailable`). ## Quando usar Campanhas em vez da API | Objetivo | Use | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Mensagem transacional para um cliente (pedido, boleto, lembrete) | **API** — `POST /messages` ou `POST /scheduled-messages` | | Responder conversas, automações de atendimento | **API** ou **agentes de IA** do painel | | Promoção, novidade ou aviso para muitos contatos de uma vez | **Campanhas** pelo WhatsApp Oficial, com template aprovado — no painel ou pela API ([Campanhas](/referencia/campanhas/)) | As mensagens enviadas pela API aparecem no chat do painel como qualquer outra, então a equipe sempre vê o que foi enviado. # Referência da API > Visão geral da API REST da TopTalking - URL base, cabeçalhos, erros comuns e a lista completa dos 47 endpoints (conta, canais e conexões por QR Code, contatos, mensagens, grupos, Kanban, mensagens agendadas, equipe, agentes de IA, campanhas e webhooks). ## URL base e cabeçalhos ```text https://platform.toptalking.com.br/v1 ``` | Cabeçalho | Quando | Valor | | ----------------- | --------------------------------------- | --------------------------------------------------------------------------- | | `Authorization` | Sempre | `Bearer tt_live_...` (ou use `X-Api-Key: tt_live_...`) | | `Content-Type` | Requisições com corpo (`POST`, `PATCH`) | `application/json` | | `Idempotency-Key` | Opcional, em `POST` que cria algo | Um valor único por operação. Veja [Idempotência](/comecando/idempotencia/). | Toda resposta traz `X-Request-Id` e os cabeçalhos de [limite de uso](/comecando/limites/). Erros seguem o [formato padrão](/comecando/erros/) e listas usam [paginação por cursor](/comecando/paginacao/). ### Erros que qualquer rota pode responder Cada endpoint lista os **seus** erros específicos. Além deles, qualquer rota pode responder: | HTTP | `code` | Quando acontece | | -------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | 401 | `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key` | Problema com a chave. | | 403 | `read_only_key` | Chave somente leitura em qualquer método que não seja `GET`. | | 429 | `rate_limit_exceeded` | Mais de 120 requisições por minuto (respeite `Retry-After`). | | 500, 502 | `internal_error` | Falha inesperada ou passageira. Tente de novo. | | 503 | `service_unavailable` | API temporariamente indisponível. Tente de novo. | | 504 | `timeout` | A requisição demorou demais. Tente de novo com a mesma `Idempotency-Key`. | ### Sobre os exemplos Cada endpoint tem exemplos em **curl**, **JavaScript** (`fetch` nativo: Node 18+, Deno, Bun, Edge Functions) e **Python** (biblioteca `requests`). Todos leem a chave da variável de ambiente `TOPTALKING_API_KEY` e devem rodar **no seu backend** — nunca no navegador. Em Supabase Edge Functions, troque `process.env.TOPTALKING_API_KEY` por `Deno.env.get('TOPTALKING_API_KEY')`. Os exemplos usam sempre os mesmos dados fictícios: a organização **Loja Aurora**, o WhatsApp **Vendas** (`d781e759-…`), a cliente **Maria Souza** (`3f1c2b9a-…`, `5511999998888`) e a atendente **Carla Mendes** (`4083deb8-…`). ## Teste direto na documentação Todo endpoint da referência tem o botão **Testar**, ao lado do método e do caminho. Ele abre um formulário com os parâmetros e o corpo daquela rota, envia a requisição **de verdade** para a API e mostra a resposta real: status, tempo, cabeçalhos (`X-Request-Id`, `X-RateLimit-*`) e o JSON. 1. Clique em **Testar** no endpoint. 2. Cole a sua chave de API (gere em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor)). Ela vale para todos os testes da aba. 3. Preencha os parâmetros, ajuste o corpo e clique em **Enviar requisição**. O painel também mostra a mesma requisição em **curl**, com os valores que você preencheu, pronta para copiar. As chamadas são reais O teste usa a sua conta de verdade. `POST /messages` **envia a mensagem** pelo seu WhatsApp, `DELETE` remove de fato e `POST /campaigns` cria uma campanha. Para explorar sem risco, use uma chave **Somente leitura**: ela só executa consultas (`GET`) e recebe `403 read_only_key` no resto. * **Onde fica a chave:** só na memória da aba do navegador. Ela some quando você fecha a aba e só é enviada para a API da TopTalking. * **Exemplos seguros:** telefones e URLs dos exemplos vêm como `SEU_NUMERO_COM_DDD` e `SEU-DOMINIO`. Enquanto você não trocar pelos seus dados, a API recusa a requisição, então um clique sem querer não manda mensagem para ninguém. * **Rotas que apagam** pedem um segundo clique de confirmação. * **Sem chave** a requisição também é enviada, e você vê como a API responde nesse caso (`401 missing_api_key`). Isso não muda a regra para o seu código: no seu site ou app, a chave continua **só no backend**. O teste funciona aqui porque a documentação e a API ficam no mesmo endereço. ## Endpoints ### Conta | Método | Caminho | O que faz | | ------ | --------------------------------------------------------------------- | ------------------------------------------------ | | `GET` | [`/me`](/referencia/conta/#dados-da-chave-e-da-organiza%C3%A7%C3%A3o) | Dados da chave e da organização (teste da chave) | ### Canais e conexões | Método | Caminho | O que faz | | -------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `GET` | [`/channels`](/referencia/canais/#listar-canais) | Listar canais (WhatsApp e Chat do site) e o status de cada um | | `POST` | [`/channels`](/referencia/canais/#criar-canal) | Criar canal e iniciar a conexão de um WhatsApp (devolve o QR Code) | | `GET` | [`/channels/{channel_id}`](/referencia/canais/#buscar-canal) | Buscar canal | | `PATCH` | [`/channels/{channel_id}`](/referencia/canais/#renomear-canal) | Renomear canal | | `DELETE` | [`/channels/{channel_id}`](/referencia/canais/#remover-canal) | Remover canal de WhatsApp | | `POST` | [`/channels/{channel_id}/connect`](/referencia/canais/#gerar-qr-code-ou-c%C3%B3digo-de-pareamento) | Gerar novo QR Code ou código de pareamento (reconectar) | | `GET` | [`/channels/{channel_id}/qr`](/referencia/canais/#consultar-o-qr-code) | QR Code atual e status da conexão (consulte a cada 3–5 s) | | `POST` | [`/channels/{channel_id}/disconnect`](/referencia/canais/#desconectar-canal) | Desconectar o WhatsApp | Passo a passo: [Conectar um WhatsApp por QR Code](/referencia/canais/#conectar-um-whatsapp-por-qr-code). ### Contatos | Método | Caminho | O que faz | | ------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `GET` | [`/contacts`](/referencia/contatos/#listar-contatos) | Listar e buscar contatos (filtros por etapa, canal, grupo, responsável…) | | `POST` | [`/contacts`](/referencia/contatos/#criar-ou-atualizar-contato) | Criar ou atualizar contato pelo telefone | | `GET` | [`/contacts/{contact_id}`](/referencia/contatos/#buscar-contato) | Buscar contato | | `PATCH` | [`/contacts/{contact_id}`](/referencia/contatos/#atualizar-contato-e-mover-no-kanban) | Atualizar contato, mover no Kanban, atribuir, fixar, escolher agente, ligar/desligar IA, finalizar, arquivar | | `GET` | [`/contacts/{contact_id}/messages`](/referencia/contatos/#hist%C3%B3rico-da-conversa) | Histórico da conversa | | `POST` | [`/contacts/{contact_id}/read`](/referencia/contatos/#marcar-conversa-como-lida) | Marcar conversa como lida | ### Mensagens | Método | Caminho | O que faz | | -------- | --------------------------------------------------------------------------------- | ------------------------------------------------------ | | `POST` | [`/messages`](/referencia/mensagens/#enviar-mensagem) | Enviar mensagem (texto e/ou mídia), também para grupos | | `GET` | [`/messages/{message_id}`](/referencia/mensagens/#buscar-mensagem) | Buscar mensagem e status de entrega | | `PATCH` | [`/messages/{message_id}`](/referencia/mensagens/#editar-mensagem) | Editar mensagem enviada (até 15 min) | | `DELETE` | [`/messages/{message_id}`](/referencia/mensagens/#apagar-mensagem) | Apagar para todos (até 48 h) ou só no painel | | `POST` | [`/messages/{message_id}/reaction`](/referencia/mensagens/#reagir-a-uma-mensagem) | Reagir (ou remover a reação) com emoji | ### Grupos | Método | Caminho | O que faz | | ------ | ------------------------------------------------------------------------------- | ---------------------------------------------------- | | `GET` | [`/groups`](/referencia/grupos/#listar-grupos) | Listar grupos do WhatsApp (o `id` é um `contact_id`) | | `GET` | [`/groups/{group_id}/participants`](/referencia/grupos/#participantes-do-grupo) | Participantes e administradores do grupo | ### Kanban | Método | Caminho | O que faz | | ------ | ---------------------------------------------- | ------------------------------------------------------------------------------ | | `GET` | [`/stages`](/referencia/kanban/#listar-etapas) | Listar etapas do Kanban (para mover cards, use `PATCH /contacts/{contact_id}`) | ### Mensagens agendadas | Método | Caminho | O que faz | | -------- | ----------------------------------------------------------------------------------------------------------- | -------------------------- | | `GET` | [`/scheduled-messages`](/referencia/mensagens-agendadas/#listar-mensagens-agendadas) | Listar mensagens agendadas | | `POST` | [`/scheduled-messages`](/referencia/mensagens-agendadas/#agendar-mensagem) | Agendar mensagem | | `GET` | [`/scheduled-messages/{scheduled_message_id}`](/referencia/mensagens-agendadas/#buscar-mensagem-agendada) | Buscar mensagem agendada | | `DELETE` | [`/scheduled-messages/{scheduled_message_id}`](/referencia/mensagens-agendadas/#cancelar-mensagem-agendada) | Cancelar mensagem agendada | ### Equipe | Método | Caminho | O que faz | | ------ | ----------------------------------------------------------- | ------------------------ | | `GET` | [`/members`](/referencia/equipe/#listar-membros) | Listar membros da equipe | | `GET` | [`/members/{member_id}`](/referencia/equipe/#buscar-membro) | Buscar membro | Para atribuir uma conversa, veja [Atribuir uma conversa](/referencia/equipe/#atribuir-uma-conversa). ### Agentes de IA | Método | Caminho | O que faz | | ------- | --------------------------------------------------------------------------------- | ------------------------------ | | `GET` | [`/ai-agents`](/referencia/agentes-ia/#listar-agentes-de-ia) | Listar agentes de IA | | `GET` | [`/ai-agents/{agent_id}`](/referencia/agentes-ia/#buscar-agente-de-ia) | Buscar agente de IA | | `PATCH` | [`/ai-agents/{agent_id}`](/referencia/agentes-ia/#ligar-ou-desligar-agente-de-ia) | Ligar ou desligar agente de IA | ### Campanhas | Método | Caminho | O que faz | | ------ | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `GET` | [`/templates`](/referencia/campanhas/#listar-templates) | Listar templates do WhatsApp Oficial (`ready: true` = pode usar) | | `POST` | [`/campaigns/quote`](/referencia/campanhas/#simular-pre%C3%A7o) | Simular o preço de uma campanha | | `POST` | [`/campaigns`](/referencia/campanhas/#criar-campanha) | Criar campanha (vai para aprovação) | | `GET` | [`/campaigns`](/referencia/campanhas/#listar-campanhas) | Listar campanhas | | `GET` | [`/campaigns/{campaign_id}`](/referencia/campanhas/#buscar-campanha) | Buscar campanha (status, contadores e preço) | | `GET` | [`/campaigns/{campaign_id}/recipients`](/referencia/campanhas/#destinat%C3%A1rios-da-campanha) | Status de cada destinatário | | `POST` | [`/campaigns/{campaign_id}/pause`](/referencia/campanhas/#pausar-campanha) | Pausar campanha | | `POST` | [`/campaigns/{campaign_id}/resume`](/referencia/campanhas/#retomar-campanha) | Retomar campanha | ### Webhooks (gestão) | Método | Caminho | O que faz | | -------- | --------------------------------------------------------------------------------------- | ----------------------------------------------- | | `GET` | [`/webhooks`](/referencia/webhooks/#listar-webhooks) | Listar webhooks | | `POST` | [`/webhooks`](/referencia/webhooks/#criar-webhook) | Criar webhook | | `GET` | [`/webhooks/{webhook_id}`](/referencia/webhooks/#buscar-webhook) | Buscar webhook | | `PATCH` | [`/webhooks/{webhook_id}`](/referencia/webhooks/#atualizar-webhook) | Atualizar webhook (URL, eventos, ativar/pausar) | | `DELETE` | [`/webhooks/{webhook_id}`](/referencia/webhooks/#excluir-webhook) | Excluir webhook | | `POST` | [`/webhooks/{webhook_id}/test`](/referencia/webhooks/#enviar-evento-de-teste) | Enviar evento de teste (`ping`) | | `GET` | [`/webhooks/{webhook_id}/deliveries`](/referencia/webhooks/#hist%C3%B3rico-de-entregas) | Histórico de entregas | ## Objetos Todo objeto tem o campo `object` (o tipo) e, quase sempre, um `id` (UUID): | `object` | Página | | -------------------------------------------------------------- | ------------------------------------------------------- | | `account` | [Conta](/referencia/conta/) | | `channel`, `channel_connection` | [Canais e conexões](/referencia/canais/) | | `contact` | [Contatos](/referencia/contatos/) | | `message` | [Mensagens](/referencia/mensagens/) | | `group`, `group_participant` | [Grupos](/referencia/grupos/) | | `stage` | [Kanban](/referencia/kanban/) | | `scheduled_message` | [Mensagens agendadas](/referencia/mensagens-agendadas/) | | `member` | [Equipe](/referencia/equipe/) | | `ai_agent` | [Agentes de IA](/referencia/agentes-ia/) | | `template`, `campaign`, `campaign_quote`, `campaign_recipient` | [Campanhas](/referencia/campanhas/) | | `webhook_endpoint`, `webhook_delivery` | [Webhooks (gestão)](/referencia/webhooks/) | | `event` | [Eventos de webhook](/webhooks/eventos/) | | `list` | [Paginação](/comecando/paginacao/) | Campos sem valor vêm como `null` (não são omitidos). Datas seguem ISO 8601 e telefones vêm só com dígitos, com DDI. ## Contrato OpenAPI A especificação completa (OpenAPI 3.1), com todos os schemas e os payloads dos webhooks, está publicada para você importar no Postman, Insomnia, Bruno, geradores de SDK ou no ChatGPT: [openapi.yaml](/openapi.yaml)https\://platform.toptalking.com.br/openapi.yaml [openapi.json](/openapi.json)https\://platform.toptalking.com.br/openapi.json Veja como usar em [OpenAPI](/recursos/openapi/). # Conta > GET /me da API da TopTalking - devolve a organização, a chave de API em uso (nível de acesso) e o estado do limite de requisições. Ideal para testar a chave. ## Dados da chave e da organização GET`/v1/me`Testar Devolve a organização dona da chave, os dados da chave usada na requisição e o estado do limite de requisições. Use para **testar se a chave funciona** e para saber se ela é somente leitura. Não tem parâmetros. * curl ```bash curl https://platform.toptalking.com.br/v1/me \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/me', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const conta = await res.json(); console.log(conta.organization.name, conta.api_key.access); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/me", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() conta = res.json() print(conta["organization"]["name"], conta["api_key"]["access"]) ``` #### Resposta `200` ```json { "object": "account", "organization": { "id": "e044a615-e879-44bb-8510-948b10b5e93c", "name": "Loja Aurora" }, "api_key": { "id": "508a23eb-5e24-4997-b928-3fce4f0f5593", "name": "Integração do site", "prefix": "tt_live_a1b2c3d4", "access": "full", "created_at": "2026-11-11T13:02:11Z" }, "rate_limit": { "limit": 120, "remaining": 119, "reset_at": "2026-11-11T14:31:00Z" } } ``` | Campo | Tipo | Descrição | | ---------------------- | ------- | -------------------------------------------------------------------------------------- | | `object` | string | Sempre `account`. | | `organization.id` | UUID | ID da organização. | | `organization.name` | string | Nome da organização. | | `api_key.id` | UUID | ID da chave. | | `api_key.name` | string | Nome dado à chave no painel. | | `api_key.prefix` | string | Início da chave (ex.: `tt_live_a1b2c3d4`), para identificar sem expor a chave inteira. | | `api_key.access` | string | `full` (acesso total) ou `read` (somente leitura). | | `api_key.created_at` | data | Quando a chave foi criada. | | `rate_limit.limit` | inteiro | Requisições permitidas por minuto (120). | | `rate_limit.remaining` | inteiro | Quantas ainda restam na janela atual. | | `rate_limit.reset_at` | data | Quando a janela reinicia. | #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | 401 | `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key` | Problema com a chave. Veja [Autenticação](/comecando/autenticacao/#erros-de-autentica%C3%A7%C3%A3o). | | 429 | `rate_limit_exceeded` | Limite de requisições excedido. | 401 invalid_api_key ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "Chave de API inválida.", "request_id": "req_6f1d2c3b4a5e" } } ``` # Canais e conexões > Conecte um WhatsApp pela API da TopTalking - crie o canal (POST /channels), mostre o QR Code (GET /channels/{channel_id}/qr) até o status virar connected, ou use o código de pareamento. Também reconectar, renomear, desconectar e remover canais. Um **canal** é por onde as conversas acontecem. Todo contato, mensagem e grupo pertence a um canal. Antes de enviar a primeira mensagem, a organização precisa de pelo menos um WhatsApp **conectado** (`status: "connected"`). ## Tipos de canal | `type` | O que é | O que dá para fazer pela API | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `whatsapp` | Um número de WhatsApp conectado lendo um **QR Code** (ou digitando um código de pareamento) no celular — o mesmo que Configurações → Conexões no painel. | Tudo: [criar e conectar](#conectar-um-whatsapp-por-qr-code), [reconectar](#reconectar-um-whatsapp-desconectado), [renomear](#renomear-canal), [desconectar](#desconectar-canal) e [remover](#remover-canal). | | `webchat` | O **Chat do site**, o widget de conversa instalado no seu site. | Aparece em [`GET /channels`](#listar-canais) e pode ser [renomeado](#renomear-canal). Criar, configurar e remover é só no painel. | ## Status da conexão | `status` | Significado | O que fazer | | -------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `connected` | O celular leu o QR Code (ou digitou o código). **Pronto para enviar e receber.** | Nada: use o canal normalmente. | | `connecting` | Esperando a leitura do QR Code ou do código de pareamento, ou reconectando sozinho. | Mostre o QR Code e consulte [`GET /channels/{channel_id}/qr`](#consultar-o-qr-code) a cada 3–5 s. | | `disconnected` | Sem sessão do WhatsApp. Envios por este canal respondem `422 channel_unavailable`. | Gere um novo QR Code com [`POST /channels/{channel_id}/connect`](#gerar-qr-code-ou-c%C3%B3digo-de-pareamento). | ```text POST /channels │ ▼ ┌────────────┐ o celular leu o QR Code ┌───────────┐ │ connecting │ ────────────────────────► │ connected │ └────────────┘ (ou digitou o código) └───────────┘ ▲ │ ▲ │ │ └───┘ QR Code novo a cada ~20–40 s │ POST /channels/{id}/disconnect │ (consulte GET /channels/{id}/qr) │ ou a sessão caiu no celular │ ▼ │ POST /channels/{id}/connect ┌──────────────┐ └─────────────────────────────────────│ disconnected │ └──────────────┘ ``` ## Conectar um WhatsApp por QR Code O fluxo é sempre o mesmo: **criar o canal → mostrar o QR Code → atualizar a cada poucos segundos → parar quando conectar.** Tudo pelo **seu backend** (a chave de API nunca vai para o navegador). 1. **Crie o canal.** Chame [`POST /channels`](#criar-canal) com um nome. A resposta `201` traz o ID do canal em `channel.id` e o primeiro QR Code em `qr_code`. ```bash curl https://platform.toptalking.com.br/v1/channels \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: conexao-pos-venda-001" \ -d '{ "name": "Pós-venda" }' ``` ```json { "object": "channel_connection", "channel": { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda", "phone": null, "status": "connecting", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" }, "status": "connecting", "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARQAAAEUCAYAAADqcMl5AAAAAklEQVR4AewaftIAAA...", "qr_code_generated_at": "2026-11-11T15:20:02Z", "pairing_code": null } ``` Guarde o `channel.id`: você vai usá-lo nos próximos passos. 2. **Mostre o QR Code.** `qr_code` é uma imagem PNG em *data URL* (`data:image/png;base64,...`). Não precisa converter: coloque direto no `src` de uma imagem — `` no React ou `img.src = qr_code` em JavaScript puro. No celular que vai ser conectado: **WhatsApp → Configurações** (no Android, toque em ⋮) **→ Aparelhos conectados → Conectar um aparelho** e aponte a câmera para o QR Code. 3. **Atualize a cada 3–5 segundos.** O QR Code **vale só \~20 a 40 segundos** e é renovado automaticamente. Consulte [`GET /channels/{channel_id}/qr`](#consultar-o-qr-code) em intervalos curtos e troque a imagem sempre que `qr_code` mudar (`qr_code_generated_at` diz quando o atual foi gerado). ```bash curl https://platform.toptalking.com.br/v1/channels/742539b3-2c69-4acc-988f-fcd836e565a9/qr \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` Esta rota só **lê** o QR Code atual — não gera um pareamento novo. Pode consultar à vontade (respeitando o [limite de 120 requisições por minuto](/comecando/limites/)). 4. **Pare quando `status` for `connected`.** Assim que o celular lê o código, a consulta devolve `status: "connected"`, `qr_code: null` e o número em `channel.phone`: ```json { "object": "channel_connection", "channel": { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda", "phone": "5511955554444", "status": "connected", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" }, "status": "connected", "qr_code": null, "qr_code_generated_at": null, "pairing_code": null } ``` Prefere não consultar? Assine o webhook [`channel.status_updated`](/webhooks/eventos/#channelstatus_updated): ele chega no instante em que o canal conecta. 5. **Envie uma mensagem de teste.** Use o ID do canal em `channel_id` para garantir que a mensagem sai pelo número recém-conectado (mande para o seu próprio celular): ```bash curl https://platform.toptalking.com.br/v1/messages \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: teste-pos-venda-001" \ -d '{ "to": "5511999998888", "channel_id": "742539b3-2c69-4acc-988f-fcd836e565a9", "text": "Teste: o WhatsApp do Pós-venda está conectado ✅" }' ``` Se ninguém ler o QR Code Se o QR Code não for lido por alguns minutos, o pareamento pode parar: a consulta passa a devolver `status: "disconnected"` com `qr_code: null`. Nesse caso, gere outro com [`POST /channels/{channel_id}/connect`](#gerar-qr-code-ou-c%C3%B3digo-de-pareamento) — **não crie outro canal**, ou você vai gastar conexões do plano. ### Exemplo completo Um backend que cria a conexão e acompanha o QR Code até conectar. Escolha a linguagem: * curl conectar-whatsapp.sh ```bash # Requer curl, jq e base64. Salva o QR Code atual em qr.png até conectar. API=https://platform.toptalking.com.br/v1 AUTH="Authorization: Bearer $TOPTALKING_API_KEY" # 1. Cria o canal e guarda o ID ID=$(curl -s "$API/channels" -H "$AUTH" -H "Content-Type: application/json" \ -H "Idempotency-Key: conexao-pos-venda-001" \ -d '{ "name": "Pós-venda" }' | jq -r '.channel.id') echo "Canal criado: $ID" # 2 a 4. A cada 4 s: salva o QR Code (se houver) e para quando conectar while true; do R=$(curl -s "$API/channels/$ID/qr" -H "$AUTH") STATUS=$(echo "$R" | jq -r '.status') [ "$STATUS" = "connected" ] && echo "Conectado: $(echo "$R" | jq -r '.channel.phone')" && break QR=$(echo "$R" | jq -r '.qr_code // empty') [ -n "$QR" ] && echo "${QR#*,}" | base64 -d > qr.png && echo "status: $STATUS (abra qr.png)" sleep 4 done ``` * JavaScript toptalking-conexao.ts ```ts // Node 18+ (fetch nativo). Roda no SEU servidor: a chave nunca vai para o navegador. const API = 'https://platform.toptalking.com.br/v1'; export type Conexao = { object: 'channel_connection'; channel: { id: string; name: string; phone: string | null; status: string }; status: 'connected' | 'connecting' | 'disconnected'; qr_code: string | null; // "data:image/png;base64,..." → direto no qr_code_generated_at: string | null; pairing_code: string | null; }; async function api(path: string, init: RequestInit = {}): Promise { const res = await fetch(`${API}${path}`, { ...init, headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', ...init.headers, }, }); const body = await res.json(); if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`); return body as T; } /** Passo 1: cria o canal. O primeiro QR Code pode vir null nos primeiros segundos. */ export function criarConexao(nome: string, idempotencyKey = crypto.randomUUID()) { return api('/channels', { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey }, body: JSON.stringify({ name: nome }), }); } /** Passo 3: lê o QR Code atual e o status (não gera um pareamento novo). */ export function consultarConexao(channelId: string) { return api(`/channels/${channelId}/qr`); } /** Gera um QR Code novo para um canal desconectado (ou que parou de parear). */ export function gerarNovoQr(channelId: string) { return api(`/channels/${channelId}/connect`, { method: 'POST', body: '{}' }); } /** Passos 2 a 4: repete a consulta a cada 4 s até conectar (desiste depois de 3 min). */ export async function aguardarConexao( channelId: string, aoMudarQr: (qrCode: string) => void, { intervaloMs = 4000, limiteMs = 3 * 60_000 } = {}, ): Promise { const fim = Date.now() + limiteMs; let ultimoQr: string | null = null; while (Date.now() < fim) { let conexao = await consultarConexao(channelId); if (conexao.status === 'connected') return conexao; // pronto! if (conexao.status === 'disconnected' && !conexao.qr_code) { conexao = await gerarNovoQr(channelId); // o pareamento parou: gera outro } if (conexao.qr_code && conexao.qr_code !== ultimoQr) { ultimoQr = conexao.qr_code; aoMudarQr(conexao.qr_code); // QR Code novo: atualize a tela } await new Promise((r) => setTimeout(r, intervaloMs)); } throw new Error('O QR Code não foi lido a tempo. Tente de novo.'); } // Uso num script: // const { channel } = await criarConexao('Pós-venda'); // const pronto = await aguardarConexao(channel.id, (qr) => console.log('QR novo:', qr.slice(0, 40))); // console.log('Conectado:', pronto.channel.phone); ``` * Python conectar_whatsapp.py ```python import base64 import os import time import requests API = "https://platform.toptalking.com.br/v1" HEADERS = {"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"} def salvar_qr(qr_code: str, arquivo: str = "qr.png") -> None: # qr_code = "data:image/png;base64,iVBORw0..." -> grava o PNG with open(arquivo, "wb") as f: f.write(base64.b64decode(qr_code.split(",", 1)[1])) # Passo 1: cria o canal res = requests.post( f"{API}/channels", headers={**HEADERS, "Idempotency-Key": "conexao-pos-venda-001"}, json={"name": "Pós-venda"}, timeout=30, ) res.raise_for_status() conexao = res.json() channel_id = conexao["channel"]["id"] # Passos 2 a 4: a cada 4 s, atualiza o QR Code até conectar (no máximo 3 min) ultimo_qr = None limite = time.time() + 180 while time.time() < limite: if conexao["status"] == "connected": print("Conectado:", conexao["channel"]["phone"]) break if conexao["status"] == "disconnected" and not conexao["qr_code"]: # o pareamento parou: gera outro QR Code conexao = requests.post( f"{API}/channels/{channel_id}/connect", headers=HEADERS, json={}, timeout=30 ).json() if conexao["qr_code"] and conexao["qr_code"] != ultimo_qr: ultimo_qr = conexao["qr_code"] salvar_qr(ultimo_qr) print("QR Code novo salvo em qr.png: leia com o celular.") time.sleep(4) res = requests.get(f"{API}/channels/{channel_id}/qr", headers=HEADERS, timeout=30) res.raise_for_status() conexao = res.json() else: print("O QR Code não foi lido a tempo. Tente de novo.") ``` #### Mostrando o QR Code numa tela Para o usuário conectar pelo **seu** sistema, exponha duas rotas no seu backend (que chamam a TopTalking com a chave) e faça a tela consultar **essas rotas** — nunca a API da TopTalking direto do navegador. server.ts (Express 5) ```ts import express from 'express'; import { criarConexao, consultarConexao, type Conexao } from './toptalking-conexao'; const app = express(); app.use(express.json()); // Devolva ao navegador só o necessário (nunca a chave). const resumo = (c: Conexao) => ({ id: c.channel.id, status: c.status, qrCode: c.qr_code, telefone: c.channel.phone, }); // Proteja estas rotas com o login do seu sistema. app.post('/api/whatsapp/conexoes', async (req, res) => { const conexao = await criarConexao(req.body.nome ?? 'WhatsApp'); res.status(201).json(resumo(conexao)); }); app.get('/api/whatsapp/conexoes/:id', async (req, res) => { res.json(resumo(await consultarConexao(req.params.id))); }); app.listen(3000); ``` * React ConectarWhatsApp.tsx ```tsx import { useEffect, useState } from 'react'; type Conexao = { id: string; status: string; qrCode: string | null; telefone: string | null }; export function ConectarWhatsApp() { const [conexao, setConexao] = useState(null); async function iniciar() { const res = await fetch('/api/whatsapp/conexoes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nome: 'Pós-venda' }), }); setConexao(await res.json()); } // Enquanto não conecta, consulta o SEU backend a cada 4 s. useEffect(() => { if (!conexao || conexao.status === 'connected') return; const timer = setInterval(async () => { const res = await fetch(`/api/whatsapp/conexoes/${conexao.id}`); setConexao(await res.json()); }, 4000); return () => clearInterval(timer); }, [conexao?.id, conexao?.status]); if (!conexao) return ; if (conexao.status === 'connected') return

WhatsApp {conexao.telefone} conectado!

; return (
{conexao.qrCode ? ( QR Code para conectar o WhatsApp ) : (

Gerando o QR Code…

)}

No celular: WhatsApp → Aparelhos conectados → Conectar um aparelho.

); } ``` * HTML + JavaScript conectar.html ```html

``` ## Conectar com código de pareamento Sem câmera à mão (ex.: o celular é o mesmo aparelho que mostra a tela)? Peça um **código de pareamento** de 8 caracteres em vez do QR Code. 1. **Tenha um canal que ainda não está conectado.** Crie com [`POST /channels`](#criar-canal) (ignore o QR Code da resposta) ou use um canal `disconnected`. 2. **Peça o código** com [`POST /channels/{channel_id}/connect`](#gerar-qr-code-ou-c%C3%B3digo-de-pareamento), enviando em `phone` o número do **WhatsApp que vai ser conectado** (o do celular da empresa, com DDI e DDD — não o de um cliente). A resposta pode levar **até \~20 segundos**: use um timeout de pelo menos 30 s. ```bash curl https://platform.toptalking.com.br/v1/channels/742539b3-2c69-4acc-988f-fcd836e565a9/connect \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone": "5511955554444" }' ``` ```json { "object": "channel_connection", "channel": { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda", "phone": null, "status": "connecting", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" }, "status": "connecting", "qr_code": null, "qr_code_generated_at": null, "pairing_code": "WZYE-H1YY" } ``` 3. **Digite o código no celular:** WhatsApp → **Aparelhos conectados → Conectar um aparelho → Conectar com número de telefone** e digite `WZYE-H1YY`. 4. **Espere o `connected`.** Consulte [`GET /channels/{channel_id}/qr`](#consultar-o-qr-code) a cada 3–5 s (o `status` vira `connected`) ou assine [`channel.status_updated`](/webhooks/eventos/#channelstatus_updated). Se o código não funcionar, peça outro chamando o `/connect` de novo. ## Reconectar um WhatsApp desconectado Quando um canal fica `disconnected` (o celular saiu de “Aparelhos conectados”, trocou de aparelho, ficou muito tempo sem internet…), **não crie um canal novo**: gere um novo QR Code para o mesmo canal. Assim as conversas continuam no mesmo lugar. 1. Chame [`POST /channels/{channel_id}/connect`](#gerar-qr-code-ou-c%C3%B3digo-de-pareamento) sem corpo (ou com `{ "phone": "..." }` para receber um código de pareamento). 2. Mostre o `qr_code` e siga os passos 3 e 4 de [Conectar um WhatsApp por QR Code](#conectar-um-whatsapp-por-qr-code). Se o canal já estiver conectado, a API responde `409 invalid_state`: para trocar o número, [desconecte](#desconectar-canal) antes. ## O objeto canal ```json { "id": "d781e759-4047-4c35-bfcd-eae8c666369a", "object": "channel", "type": "whatsapp", "name": "Vendas", "phone": "5511988887777", "status": "connected", "is_primary": true, "created_at": "2026-03-02T12:10:44Z" } ``` | Campo | Tipo | Descrição | | ------------ | ---------------- | ---------------------------------------------------------------------------------------------------- | | `id` | UUID | ID do canal. Use em `channel_id` para escolher por qual número enviar. | | `object` | string | Sempre `channel`. | | `type` | string | `whatsapp` (número conectado por QR Code) ou `webchat` (Chat do site). | | `name` | string | Nome do canal no painel (ex.: “Vendas”). | | `phone` | string ou `null` | Número do WhatsApp, só dígitos com DDI. `null` no Chat do site e em canais que ainda não conectaram. | | `status` | string | `connected`, `connecting` ou `disconnected`. Veja [Status da conexão](#status-da-conex%C3%A3o). | | `is_primary` | booleano | Se é o WhatsApp principal — o usado por padrão em `POST /messages`. | | `created_at` | data | Quando o canal foi criado. | ## O objeto conexão Devolvido por [`POST /channels`](#criar-canal), [`POST /channels/{channel_id}/connect`](#gerar-qr-code-ou-c%C3%B3digo-de-pareamento) e [`GET /channels/{channel_id}/qr`](#consultar-o-qr-code). Mostra o estado do pareamento de um canal de WhatsApp. ```json { "object": "channel_connection", "channel": { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda", "phone": null, "status": "connecting", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" }, "status": "connecting", "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARQAAAEUCAYAAADqcMl5AAAAAklEQVR4AewaftIAAA...", "qr_code_generated_at": "2026-11-11T15:20:31Z", "pairing_code": null } ``` | Campo | Tipo | Descrição | | ---------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `object` | string | Sempre `channel_connection`. | | `channel` | objeto | O [objeto canal](#o-objeto-canal) atualizado. | | `status` | string | `connected` (o celular leu o código e o canal já envia mensagens), `connecting` ou `disconnected`. | | `qr_code` | string ou `null` | Imagem PNG em data URL (`data:image/png;base64,...`), pronta para o `src` de uma imagem. `null` quando o canal está conectado, quando não há pareamento em andamento, quando o QR Code ainda está sendo gerado ou quando você pediu código de pareamento. | | `qr_code_generated_at` | data ou `null` | Quando o QR Code atual foi gerado. Mudou? Troque a imagem. | | `pairing_code` | string ou `null` | Código de pareamento (8 caracteres, ex.: `WZYE-H1YY`). Só vem na resposta de `POST /channels/{channel_id}/connect` com `phone`. | ## Listar canais GET`/v1/channels`Testar Devolve todos os canais da organização — WhatsApp e Chat do site, conectados ou não. Use para descobrir o `channel_id` de cada número e para **monitorar se algum WhatsApp caiu**. Não tem parâmetros. A lista vem inteira numa página (`has_more: false`). * curl ```bash curl https://platform.toptalking.com.br/v1/channels \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/channels', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: canais } = await res.json(); const caidos = canais.filter((c) => c.type === 'whatsapp' && c.status !== 'connected'); if (caidos.length) console.warn('WhatsApp sem conexão:', caidos.map((c) => c.name)); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/channels", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() canais = res.json()["data"] caidos = [c["name"] for c in canais if c["type"] == "whatsapp" and c["status"] != "connected"] ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "d781e759-4047-4c35-bfcd-eae8c666369a", "object": "channel", "type": "whatsapp", "name": "Vendas", "phone": "5511988887777", "status": "connected", "is_primary": true, "created_at": "2026-03-02T12:10:44Z" }, { "id": "9d8adaf6-a08d-4027-b07c-5df1e89e117b", "object": "channel", "type": "whatsapp", "name": "Suporte", "phone": "5511977776666", "status": "disconnected", "is_primary": false, "created_at": "2026-05-18T09:31:02Z" }, { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda", "phone": null, "status": "connecting", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" }, { "id": "26facac8-3d9a-41b3-bfee-88851aba96ec", "object": "channel", "type": "webchat", "name": "Chat do site", "phone": null, "status": "connected", "is_primary": false, "created_at": "2026-06-20T17:45:10Z" } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## Criar canal POST`/v1/channels`Testar Cria um canal de WhatsApp e **já inicia o pareamento**: a resposta traz o primeiro QR Code. É o primeiro passo para [conectar um WhatsApp por QR Code](#conectar-um-whatsapp-por-qr-code). Depois, consulte [`GET /channels/{channel_id}/qr`](#consultar-o-qr-code) até `status` ser `connected`. Cada canal criado conta no **limite de conexões do plano** — veja [Limite de conexões do plano](#limite-de-conex%C3%B5es-do-plano). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ----------------- | --------- | ------ | ----------------- | ---------------------------------------------------------------------------------------------------- | | `name` | corpo | string | não | Nome do canal no painel (até 80 caracteres), ex.: “Pós-venda”. O corpo inteiro é opcional. | | `Idempotency-Key` | cabeçalho | string | não (recomendado) | Evita criar dois canais se a requisição for repetida. Veja [Idempotência](/comecando/idempotencia/). | * curl ```bash curl https://platform.toptalking.com.br/v1/channels \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: conexao-pos-venda-001" \ -d '{ "name": "Pós-venda" }' ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/channels', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'conexao-pos-venda-001', }, body: JSON.stringify({ name: 'Pós-venda' }), }); const conexao = await res.json(); if (!res.ok) throw new Error(`${conexao.error.code}: ${conexao.error.message}`); console.log(conexao.channel.id, conexao.qr_code ? 'QR Code pronto' : 'gerando QR Code…'); ``` * Python ```python import os import requests res = requests.post( "https://platform.toptalking.com.br/v1/channels", headers={ "Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}", "Idempotency-Key": "conexao-pos-venda-001", }, json={"name": "Pós-venda"}, timeout=30, ) conexao = res.json() if not res.ok: raise RuntimeError(f"{conexao['error']['code']}: {conexao['error']['message']}") channel_id = conexao["channel"]["id"] ``` #### Resposta `201` O [objeto conexão](#o-objeto-conex%C3%A3o), com o canal em `connecting`: ```json { "object": "channel_connection", "channel": { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda", "phone": null, "status": "connecting", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" }, "status": "connecting", "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARQAAAEUCAYAAADqcMl5AAAAAklEQVR4AewaftIAAA...", "qr_code_generated_at": "2026-11-11T15:20:02Z", "pairing_code": null } ``` `qr_code` pode vir `null` se o QR Code ainda estiver sendo gerado: continue consultando `GET /channels/{channel_id}/qr`. #### Erros | HTTP | `code` | Quando acontece | | ---- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `invalid_json`, `invalid_field` | Corpo inválido, `name` com mais de 80 caracteres ou campo desconhecido. | | 403 | `read_only_key` | Chave somente leitura. | | 422 | `limit_reached` | O plano já tem o máximo de conexões de WhatsApp. | | 502 | `provider_error`, `internal_error` | O WhatsApp não respondeu ao criar a conexão. Tente de novo com a mesma `Idempotency-Key`. | | 504 | `timeout` | O lado do WhatsApp demorou demais. Tente de novo com a mesma `Idempotency-Key`: se a primeira tiver dado certo, você recebe o canal já criado. | 422 limit_reached ```json { "error": { "type": "invalid_request_error", "code": "limit_reached", "message": "Seu plano permite 3 conexões de WhatsApp. Remova uma ou faça upgrade no painel.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Buscar canal GET`/v1/channels/{channel_id}`Testar Um canal pelo ID. Útil para conferir o `status` de um número específico antes de um envio importante. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | ------------ | | `channel_id` | caminho | UUID | sim | ID do canal. | * curl ```bash curl https://platform.toptalking.com.br/v1/channels/d781e759-4047-4c35-bfcd-eae8c666369a \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const channelId = 'd781e759-4047-4c35-bfcd-eae8c666369a'; const res = await fetch(`https://platform.toptalking.com.br/v1/channels/${channelId}`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const canal = await res.json(); if (canal.status !== 'connected') console.warn(`O canal ${canal.name} está ${canal.status}.`); ``` * Python ```python import os import requests channel_id = "d781e759-4047-4c35-bfcd-eae8c666369a" res = requests.get( f"https://platform.toptalking.com.br/v1/channels/{channel_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() canal = res.json() ``` #### Resposta `200` ```json { "id": "d781e759-4047-4c35-bfcd-eae8c666369a", "object": "channel", "type": "whatsapp", "name": "Vendas", "phone": "5511988887777", "status": "connected", "is_primary": true, "created_at": "2026-03-02T12:10:44Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ----------------------------------------------------------- | | 404 | `resource_not_found` | O canal não existe, foi removido ou é de outra organização. | ## Consultar o QR Code GET`/v1/channels/{channel_id}/qr`Testar Devolve o **QR Code mais recente** do pareamento em andamento e o status da conexão, para você mostrar na tela e saber quando parar. É a rota que você chama **a cada 3–5 segundos** durante a conexão. Ela **não gera** um pareamento novo (para isso, use [`POST /channels/{channel_id}/connect`](#gerar-qr-code-ou-c%C3%B3digo-de-pareamento)). Quando o celular lê o código, `status` vira `connected` e `qr_code` volta `null`. Só funciona para canais `whatsapp`. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | ------------ | | `channel_id` | caminho | UUID | sim | ID do canal. | * curl ```bash curl https://platform.toptalking.com.br/v1/channels/742539b3-2c69-4acc-988f-fcd836e565a9/qr \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const channelId = '742539b3-2c69-4acc-988f-fcd836e565a9'; const res = await fetch(`https://platform.toptalking.com.br/v1/channels/${channelId}/qr`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const conexao = await res.json(); if (conexao.status === 'connected') console.log('Conectado:', conexao.channel.phone); else if (conexao.qr_code) mostrarNaTela(conexao.qr_code); // sua função ``` * Python ```python import os import requests channel_id = "742539b3-2c69-4acc-988f-fcd836e565a9" res = requests.get( f"https://platform.toptalking.com.br/v1/channels/{channel_id}/qr", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() conexao = res.json() print(conexao["status"], "QR Code disponível" if conexao["qr_code"] else "sem QR Code") ``` #### Resposta `200` Enquanto espera a leitura: ```json { "object": "channel_connection", "channel": { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda", "phone": null, "status": "connecting", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" }, "status": "connecting", "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARQAAAEUCAYAAADqcMl5AAAAAklEQVR4AewaftIAAA...", "qr_code_generated_at": "2026-11-11T15:20:31Z", "pairing_code": null } ``` Depois que o celular leu o código, a mesma chamada devolve `status: "connected"`, `qr_code: null` e o número em `channel.phone` (veja o passo 4 de [Conectar um WhatsApp por QR Code](#conectar-um-whatsapp-por-qr-code)). | `status` | `qr_code` | O que fazer | | -------------- | ---------- | ----------------------------------------------------------------------------------- | | `connecting` | preenchido | Mostre (ou troque) a imagem e consulte de novo em 3–5 s. | | `connecting` | `null` | O QR Code está sendo gerado (ou você pediu código de pareamento). Consulte de novo. | | `connected` | `null` | Pronto. Pare de consultar. | | `disconnected` | `null` | Não há pareamento em andamento. Chame `POST /channels/{channel_id}/connect`. | #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ----------------------------------------------------------- | | 400 | `invalid_field` | O canal é o Chat do site (`webchat`), que não usa QR Code. | | 404 | `resource_not_found` | O canal não existe, foi removido ou é de outra organização. | ## Gerar QR Code ou código de pareamento POST`/v1/channels/{channel_id}/connect`Testar Inicia um **novo pareamento** num canal de WhatsApp que **não está conectado** e devolve o QR Code. Use para: * **reconectar** um canal `disconnected`; * gerar outro QR Code quando o anterior não foi lido a tempo; * conectar **sem câmera**: envie `phone` e receba um `pairing_code` para digitar no celular (veja [Conectar com código de pareamento](#conectar-com-c%C3%B3digo-de-pareamento)). O QR Code vale cerca de 20 a 40 segundos e é renovado sozinho: depois desta chamada, consulte [`GET /channels/{channel_id}/qr`](#consultar-o-qr-code) a cada poucos segundos até `status` virar `connected`. Com `phone`, a resposta pode demorar **até \~20 segundos**. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `channel_id` | caminho | UUID | sim | ID do canal de WhatsApp. | | `phone` | corpo | string | não | Número do WhatsApp **que vai ser conectado**, com DDI e DDD (ex.: `5511955554444`). Com ele, a resposta traz `pairing_code` em vez de `qr_code`. Sem corpo (ou `{}`), vem o QR Code. | * curl ```bash # QR Code curl -X POST https://platform.toptalking.com.br/v1/channels/9d8adaf6-a08d-4027-b07c-5df1e89e117b/connect \ -H "Authorization: Bearer $TOPTALKING_API_KEY" # Código de pareamento (sem câmera) curl https://platform.toptalking.com.br/v1/channels/9d8adaf6-a08d-4027-b07c-5df1e89e117b/connect \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone": "5511977776666" }' ``` * JavaScript ```js async function gerarPareamento(channelId, phone) { const res = await fetch(`https://platform.toptalking.com.br/v1/channels/${channelId}/connect`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(phone ? { phone } : {}), signal: AbortSignal.timeout(30_000), // com phone pode levar ~20 s }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); return body; // body.qr_code ou body.pairing_code } const { pairing_code } = await gerarPareamento('9d8adaf6-a08d-4027-b07c-5df1e89e117b', '5511977776666'); console.log('Digite no celular:', pairing_code); // "WZYE-H1YY" ``` * Python ```python import os import requests channel_id = "9d8adaf6-a08d-4027-b07c-5df1e89e117b" res = requests.post( f"https://platform.toptalking.com.br/v1/channels/{channel_id}/connect", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={"phone": "5511977776666"}, # ou json={} para receber o QR Code timeout=30, # com phone pode levar ~20 s ) conexao = res.json() if not res.ok: raise RuntimeError(f"{conexao['error']['code']}: {conexao['error']['message']}") print("Digite no celular:", conexao["pairing_code"]) ``` #### Resposta `200` Sem `phone` (QR Code): ```json { "object": "channel_connection", "channel": { "id": "9d8adaf6-a08d-4027-b07c-5df1e89e117b", "object": "channel", "type": "whatsapp", "name": "Suporte", "phone": "5511977776666", "status": "connecting", "is_primary": false, "created_at": "2026-05-18T09:31:02Z" }, "status": "connecting", "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARQAAAEUCAYAAADqcMl5AAAAAklEQVR4AewaftIAAA...", "qr_code_generated_at": "2026-11-11T15:42:08Z", "pairing_code": null } ``` Com `phone` (código de pareamento): ```json { "object": "channel_connection", "channel": { "id": "9d8adaf6-a08d-4027-b07c-5df1e89e117b", "object": "channel", "type": "whatsapp", "name": "Suporte", "phone": "5511977776666", "status": "connecting", "is_primary": false, "created_at": "2026-05-18T09:31:02Z" }, "status": "connecting", "qr_code": null, "qr_code_generated_at": null, "pairing_code": "WZYE-H1YY" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------------------------------------------------------------------------------- | | 400 | `invalid_field` | O canal é o Chat do site (`webchat`), que não usa QR Code, ou há campo desconhecido no corpo. | | 400 | `invalid_phone` | `phone` não parece um número válido. | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O canal não existe ou é de outra organização. | | 409 | `invalid_state` | O canal **já está conectado**. Para trocar o número, desconecte antes. | | 502 | `provider_error` | O WhatsApp não devolveu o QR Code ou o código de pareamento. Confira o número e tente de novo. | | 504 | `timeout` | O lado do WhatsApp demorou demais para responder. Tente de novo em instantes. | 409 invalid_state ```json { "error": { "type": "conflict_error", "code": "invalid_state", "message": "Este WhatsApp já está conectado. Para trocar o número, desconecte antes.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Renomear canal PATCH`/v1/channels/{channel_id}`Testar Muda o nome do canal no painel (ex.: de “WhatsApp” para “Suporte”). Não afeta a conexão nem as conversas. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ------ | ----------- | ------------------------------ | | `channel_id` | caminho | UUID | sim | ID do canal. | | `name` | corpo | string | sim | Novo nome (até 80 caracteres). | * curl ```bash curl -X PATCH https://platform.toptalking.com.br/v1/channels/742539b3-2c69-4acc-988f-fcd836e565a9 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Pós-venda e trocas" }' ``` * JavaScript ```js const channelId = '742539b3-2c69-4acc-988f-fcd836e565a9'; const res = await fetch(`https://platform.toptalking.com.br/v1/channels/${channelId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'Pós-venda e trocas' }), }); const canal = await res.json(); ``` * Python ```python import os import requests channel_id = "742539b3-2c69-4acc-988f-fcd836e565a9" res = requests.patch( f"https://platform.toptalking.com.br/v1/channels/{channel_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={"name": "Pós-venda e trocas"}, timeout=30, ) res.raise_for_status() canal = res.json() ``` #### Resposta `200` ```json { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda e trocas", "phone": "5511955554444", "status": "connected", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | ------------------------------- | ----------------------------------------------------------------------- | | 400 | `missing_field` | Faltou `name`. | | 400 | `invalid_json`, `invalid_field` | Corpo inválido, `name` com mais de 80 caracteres ou campo desconhecido. | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O canal não existe ou é de outra organização. | ## Desconectar canal POST`/v1/channels/{channel_id}/disconnect`Testar Encerra a sessão do WhatsApp neste canal — o mesmo que **Desconectar** no painel (o aparelho some de “Aparelhos conectados” no celular). O canal continua existindo, com `status: "disconnected"`, e **as conversas continuam no painel**. Para usar o número de novo (ou conectar outro número no mesmo canal), chame [`POST /channels/{channel_id}/connect`](#gerar-qr-code-ou-c%C3%B3digo-de-pareamento). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | ------------------------ | | `channel_id` | caminho | UUID | sim | ID do canal de WhatsApp. | * curl ```bash curl -X POST https://platform.toptalking.com.br/v1/channels/742539b3-2c69-4acc-988f-fcd836e565a9/disconnect \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const channelId = '742539b3-2c69-4acc-988f-fcd836e565a9'; const res = await fetch( `https://platform.toptalking.com.br/v1/channels/${channelId}/disconnect`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` } }, ); const canal = await res.json(); // canal.status === "disconnected" ``` * Python ```python import os import requests channel_id = "742539b3-2c69-4acc-988f-fcd836e565a9" res = requests.post( f"https://platform.toptalking.com.br/v1/channels/{channel_id}/disconnect", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() canal = res.json() ``` #### Resposta `200` O [objeto canal](#o-objeto-canal), já desconectado: ```json { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda e trocas", "phone": "5511955554444", "status": "disconnected", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | --------------------------------------------- | | 400 | `invalid_field` | O canal é o Chat do site (`webchat`). | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O canal não existe ou é de outra organização. | ## Remover canal DELETE`/v1/channels/{channel_id}`Testar Desconecta o WhatsApp e **remove o canal da organização**, liberando uma conexão do plano. As conversas que já existiam continuam no painel. Não dá para desfazer: para usar o número de novo, [crie outro canal](#criar-canal). O Chat do site (`webchat`) só pode ser removido no painel. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | ------------------------ | | `channel_id` | caminho | UUID | sim | ID do canal de WhatsApp. | * curl ```bash curl -X DELETE https://platform.toptalking.com.br/v1/channels/742539b3-2c69-4acc-988f-fcd836e565a9 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const channelId = '742539b3-2c69-4acc-988f-fcd836e565a9'; const res = await fetch(`https://platform.toptalking.com.br/v1/channels/${channelId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { deleted } = await res.json(); // true ``` * Python ```python import os import requests channel_id = "742539b3-2c69-4acc-988f-fcd836e565a9" res = requests.delete( f"https://platform.toptalking.com.br/v1/channels/{channel_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() ``` #### Resposta `200` ```json { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "deleted": true } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | -------------------------------------------------------------- | | 400 | `invalid_field` | O canal é o Chat do site (`webchat`): remova pelo painel. | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O canal não existe, já foi removido ou é de outra organização. | 400 invalid_field ```json { "error": { "type": "invalid_request_error", "code": "invalid_field", "message": "O Chat do site é gerenciado no painel (Configurações → Conexões).", "param": "channel_id", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Limite de conexões do plano Cada plano da TopTalking permite um número de **conexões de WhatsApp** (canais `whatsapp`). Canais `disconnected` também contam — só [remover](#remover-canal) libera a vaga. O Chat do site não conta. Ao passar do limite, `POST /channels` responde `422 limit_reached`: ```json { "error": { "type": "invalid_request_error", "code": "limit_reached", "message": "Seu plano permite 3 conexões de WhatsApp. Remova uma ou faça upgrade no painel.", "request_id": "req_6f1d2c3b4a5e" } } ``` O que fazer: reconecte um canal existente com [`POST /channels/{channel_id}/connect`](#gerar-qr-code-ou-c%C3%B3digo-de-pareamento) em vez de criar outro, remova um canal que não usa mais ou faça upgrade do plano no painel. [Enviar mensagens](/referencia/mensagens/#enviar-mensagem)POST /messages, com channel_id para escolher o número. [Evento channel.status_updated](/webhooks/eventos/#channelstatus_updated)Seja avisado quando um WhatsApp conecta ou cai. # Contatos > Contatos (conversas) na API da TopTalking - listar e filtrar (GET /contacts), criar ou atualizar pelo telefone (POST /contacts), atualizar, mover no Kanban, atribuir responsável, fixar e escolher o agente de IA (PATCH /contacts/{contact_id}), marcar como lida e ler o histórico. Na TopTalking, um **contato é uma conversa** da caixa de entrada: um por número de telefone e canal. O contato guarda os dados da pessoa (nome, telefone, e-mail) e o estado do atendimento (etapa do Kanban, responsável, agente de IA, fixada, finalizada, arquivada). [Grupos](/referencia/grupos/) também são conversas: aparecem aqui com `is_group: true`, e o `id` do grupo funciona em todos os endpoints desta página. ## O objeto contato ```json { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 2, "last_message_at": "2026-11-11T14:12:40Z", "last_message_preview": "Vocês entregam no sábado?", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:12:40Z" } ``` | Campo | Tipo | Descrição | | ---------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------- | | `id` | UUID | ID do contato (da conversa). | | `object` | string | Sempre `contact`. | | `name` | string ou `null` | Nome do contato (ou do grupo). | | `phone` | string ou `null` | Telefone só com dígitos, com DDI (ex.: `5511999998888`). `null` em grupos e em conversas do Chat do site sem telefone. | | `email` | string ou `null` | E-mail. | | `channel_id` | UUID ou `null` | [Canal](/referencia/canais/) da conversa. | | `channel_type` | string | `whatsapp` ou `webchat` (Chat do site). | | `is_group` | booleano | Grupo de WhatsApp. Grupos não têm IA, mas recebem mensagens pela API (veja [Grupos](/referencia/grupos/)). | | `stage` | objeto ou `null` | Etapa atual no Kanban (`id`, `name`, `is_final`), ou `null` se não está em nenhuma. | | `ai_enabled` | booleano | Se a IA responde esta conversa. | | `ai_agent_id` | UUID ou `null` | [Agente de IA](/referencia/agentes-ia/) da conversa. `null` = o agente padrão. | | `assigned_to` | UUID ou `null` | [Membro](/referencia/equipe/) responsável pela conversa. `null` = sem responsável. | | `pinned` | booleano | Conversa fixada no topo da caixa de entrada. | | `unread_count` | inteiro | Mensagens não lidas no painel. | | `last_message_at` | data ou `null` | Data da última mensagem. | | `last_message_preview` | string ou `null` | Prévia da última mensagem. | | `resolved_at` | data ou `null` | Quando a conversa foi finalizada (`null` = em aberto). | | `archived_at` | data ou `null` | Quando a conversa foi arquivada (`null` = ativa). | | `source` | string | Origem do contato: `whatsapp`, `webchat`, `api`, `agenda`, `lead`… | | `created_at` | data | Criação. | | `updated_at` | data | Última alteração. | ## Listar contatos GET`/v1/contacts`Testar Lista as conversas ordenadas pela **última mensagem** (mais recentes primeiro). Use para buscar um contato, montar a sua caixa de entrada, ler um quadro do Kanban ou a fila de um atendente. Todos os filtros são opcionais e podem ser combinados. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------- | ----- | -------- | ----------- | ----------------------------------------------------------------------------------- | | `search` | query | string | não | Busca parcial por nome ou telefone (até 100 caracteres). | | `phone` | query | string | não | Telefone exato. Aceita formatado (`+55 11 99999-8888`); é normalizado para dígitos. | | `stage_id` | query | UUID | não | Só contatos nesta etapa do Kanban. | | `channel_id` | query | UUID | não | Só contatos deste canal. | | `archived` | query | booleano | não | `true` só arquivados, `false` só ativos. Sem o parâmetro, todos. | | `is_group` | query | booleano | não | `false` só conversas individuais, `true` só grupos. Sem o parâmetro, todos. | | `assigned_to` | query | string | não | ID do membro responsável, ou `none` para conversas **sem responsável**. | | `limit` | query | inteiro | não | Itens por página, 1 a 100 (padrão 20). | | `cursor` | query | string | não | `next_cursor` da página anterior. Veja [Paginação](/comecando/paginacao/). | * curl ```bash # Conversas individuais ativas na etapa "Negociação" curl "https://platform.toptalking.com.br/v1/contacts?stage_id=9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11&archived=false&is_group=false&limit=50" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" # Procurar um contato pelo telefone curl "https://platform.toptalking.com.br/v1/contacts?phone=5511999998888" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" # Conversas sem responsável curl "https://platform.toptalking.com.br/v1/contacts?assigned_to=none&archived=false" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const url = new URL('https://platform.toptalking.com.br/v1/contacts'); url.searchParams.set('search', 'maria'); url.searchParams.set('archived', 'false'); url.searchParams.set('is_group', 'false'); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: contatos, has_more, next_cursor } = await res.json(); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/contacts", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"search": "maria", "archived": "false", "is_group": "false"}, timeout=30, ) res.raise_for_status() pagina = res.json() contatos = pagina["data"] ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 2, "last_message_at": "2026-11-11T14:12:40Z", "last_message_preview": "Vocês entregam no sábado?", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:12:40Z" } ], "has_more": true, "next_cursor": "eyJ0IjoiMjAyNi0xMS0xMVQxNDoxMjo0MFoifQ" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | ------------------------------------- | -------------------------------------------------------------------- | | 401 | `missing_api_key`, `invalid_api_key`… | Problema com a chave. Veja [Autenticação](/comecando/autenticacao/). | ## Criar ou atualizar contato POST`/v1/contacts`Testar Cria o contato pelo telefone. **Se já existir um contato com esse telefone na organização, os campos enviados são atualizados** e a resposta é `200` em vez de `201` — então dá para chamar sem verificar antes (“upsert”). Ideal para formulários de lead e sincronização com o seu CRM. O contato criado aparece na caixa de entrada do painel. Para já colocá-lo no funil, envie `stage_id`. Como há um contato por número **e canal**, quando o telefone existe em mais de um canal é atualizada a conversa do `channel_id` informado ou, sem ele, a que teve mensagem mais recente. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ----------------- | --------- | ------ | ----------- | ------------------------------------------------------------------------------------------------ | | `phone` | corpo | string | sim | Telefone com DDI e DDD. Formatação é ignorada; números brasileiros sem `55` recebem o `55`. | | `name` | corpo | string | não | Nome (até 120 caracteres). | | `email` | corpo | string | não | E-mail. | | `stage_id` | corpo | UUID | não | Já coloca o card nesta etapa do Kanban. Veja [`GET /stages`](/referencia/kanban/#listar-etapas). | | `channel_id` | corpo | UUID | não | Canal de WhatsApp da conversa. Padrão: o WhatsApp principal. | | `Idempotency-Key` | cabeçalho | string | não | Para retentativas seguras. Veja [Idempotência](/comecando/idempotencia/). | * curl ```bash curl https://platform.toptalking.com.br/v1/contacts \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: lead-5511999998888" \ -d '{ "phone": "+55 11 99999-8888", "name": "Maria Souza", "email": "maria@exemplo.com", "stage_id": "e63d8231-3992-457e-a8b0-d1be2fa591d4" }' ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/contacts', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'lead-5511999998888', }, body: JSON.stringify({ phone: '+55 11 99999-8888', name: 'Maria Souza', email: 'maria@exemplo.com', stage_id: 'e63d8231-3992-457e-a8b0-d1be2fa591d4', // "Novo lead" }), }); const contato = await res.json(); console.log(res.status === 201 ? 'Contato criado' : 'Contato atualizado', contato.id); ``` * Python ```python import os import requests res = requests.post( "https://platform.toptalking.com.br/v1/contacts", headers={ "Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}", "Idempotency-Key": "lead-5511999998888", }, json={ "phone": "+55 11 99999-8888", "name": "Maria Souza", "email": "maria@exemplo.com", "stage_id": "e63d8231-3992-457e-a8b0-d1be2fa591d4", # "Novo lead" }, timeout=30, ) contato = res.json() print("Contato criado" if res.status_code == 201 else "Contato atualizado", contato["id"]) ``` #### Resposta `201` (criado) ou `200` (já existia) ```json { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "e63d8231-3992-457e-a8b0-d1be2fa591d4", "name": "Novo lead", "is_final": false }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": null, "pinned": false, "unread_count": 0, "last_message_at": null, "last_message_preview": null, "resolved_at": null, "archived_at": null, "source": "api", "created_at": "2026-11-11T14:20:05Z", "updated_at": "2026-11-11T14:20:05Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | ---------------------- | ------------------------------------------------------------------------------------------------------ | | 400 | `missing_field` | Faltou `phone`. | | 400 | `invalid_phone` | Telefone inválido. | | 400 | `invalid_field` | Algum campo inválido (ex.: `email`, `stage_id` que não é etapa da organização ou `name` longo demais). | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | `channel_id` não encontrado. | | 409 | `idempotency_conflict` | `Idempotency-Key` reutilizada com outro corpo. | 400 invalid_phone ```json { "error": { "type": "invalid_request_error", "code": "invalid_phone", "message": "Telefone inválido. Envie com DDI e DDD, ex.: 5511999998888.", "param": "phone", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Buscar contato GET`/v1/contacts/{contact_id}`Testar Um contato pelo ID — por exemplo, o `contact_id` que chegou num webhook ou numa mensagem. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | ---------------------------- | | `contact_id` | caminho | UUID | sim | ID do contato (ou do grupo). | * curl ```bash curl https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const contactId = '3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60'; const res = await fetch(`https://platform.toptalking.com.br/v1/contacts/${contactId}`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const contato = await res.json(); ``` * Python ```python import os import requests contact_id = "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60" res = requests.get( f"https://platform.toptalking.com.br/v1/contacts/{contact_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() contato = res.json() ``` #### Resposta `200` O [objeto contato](#o-objeto-contato): ```json { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 2, "last_message_at": "2026-11-11T14:12:40Z", "last_message_preview": "Vocês entregam no sábado?", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:12:40Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------------------------------------------------------------- | | 404 | `resource_not_found` | O contato não existe (ex.: foi apagado no painel) ou é de outra organização. | ## Atualizar contato (e mover no Kanban) PATCH`/v1/contacts/{contact_id}`Testar Envie **só os campos que quer mudar**. É com este endpoint que você: * **move o card no Kanban** (`stage_id`); * **atribui a conversa** a um atendente (`assigned_to`) — veja [Equipe](/referencia/equipe/#atribuir-uma-conversa); * **liga ou desliga a IA** naquela conversa (`ai_enabled`) ou **troca o agente** (`ai_agent_id`) — veja [Agentes de IA](/referencia/agentes-ia/#agente-e-ia-por-conversa); * **fixa** a conversa no topo (`pinned`); * **finaliza ou reabre** a conversa (`resolved`); * **arquiva ou desarquiva** (`archived`); * corrige nome e e-mail. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------- | ------- | ---------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `contact_id` | caminho | UUID | sim | ID do contato (ou do grupo). | | `name` | corpo | string | não | Nome (até 120 caracteres). | | `email` | corpo | string ou `null` | não | E-mail. `null` apaga. | | `stage_id` | corpo | UUID ou `null` | não | Move o card para esta etapa. `null` tira o card de qualquer etapa. Mover para a etapa final (`is_final: true`) **também finaliza a conversa**, igual ao painel. | | `assigned_to` | corpo | UUID ou `null` | não | Membro responsável (veja [`GET /members`](/referencia/equipe/#listar-membros)). `null` libera a conversa. | | `ai_enabled` | corpo | booleano | não | `true` liga e `false` desliga a IA nesta conversa. | | `ai_agent_id` | corpo | UUID ou `null` | não | Agente de IA desta conversa (veja [`GET /ai-agents`](/referencia/agentes-ia/#listar-agentes-de-ia)). `null` volta para o agente padrão. | | `pinned` | corpo | booleano | não | `true` fixa a conversa no topo da caixa de entrada; `false` desafixa. | | `resolved` | corpo | booleano | não | `true` finaliza a conversa; `false` reabre. | | `archived` | corpo | booleano | não | `true` arquiva; `false` desarquiva. | * curl ```bash # Mover para a etapa "Negociação" curl -X PATCH https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "stage_id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11" }' # Um humano vai assumir: desligar a IA, atribuir à Carla e fixar curl -X PATCH https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ai_enabled": false, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": true }' ``` * JavaScript ```js async function atualizarContato(contactId, campos) { const res = await fetch(`https://platform.toptalking.com.br/v1/contacts/${contactId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(campos), }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); return body; } // Mover no Kanban await atualizarContato('3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60', { stage_id: '9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11', }); // Passar para o agente de IA "Vendas" await atualizarContato('3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60', { ai_agent_id: '52da47ff-662a-4292-9961-27a95fc5ddd8', }); // Finalizar a conversa await atualizarContato('3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60', { resolved: true }); ``` * Python ```python import os import requests def atualizar_contato(contact_id: str, campos: dict) -> dict: res = requests.patch( f"https://platform.toptalking.com.br/v1/contacts/{contact_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json=campos, timeout=30, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") return body # Um humano vai assumir: desligar a IA, atribuir à Carla e fixar atualizar_contato( "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", {"ai_enabled": False, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": True}, ) ``` #### Resposta `200` O contato atualizado (aqui, depois do segundo exemplo em curl): ```json { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "ai_enabled": false, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": true, "unread_count": 2, "last_message_at": "2026-11-11T14:12:40Z", "last_message_preview": "Vocês entregam no sábado?", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:35:21Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `missing_field` | O corpo não tem nenhum campo para atualizar. | | 400 | `invalid_json`, `invalid_field` | Corpo inválido ou campo com valor inválido: `stage_id` que não é etapa da organização, `assigned_to` que não é membro, `ai_agent_id` que não é agente da organização… (`param` diz qual). | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O contato não existe. | 400 invalid_field ```json { "error": { "type": "invalid_request_error", "code": "invalid_field", "message": "Este usuário não é membro da organização. Veja GET /v1/members.", "param": "assigned_to", "request_id": "req_6f1d2c3b4a5e" } } ``` Cada alteração de nome, e-mail, etapa, responsável, IA, arquivamento ou finalização dispara o webhook [`contact.updated`](/webhooks/eventos/#contactupdated) e, se a etapa mudou, [`contact.stage_changed`](/webhooks/eventos/#contactstage_changed). ## Marcar conversa como lida POST`/v1/contacts/{contact_id}/read`Testar Zera o contador de não lidas (`unread_count`) da conversa no painel. Use quando o seu sistema (ou a sua IA) já tratou as mensagens novas, para a equipe não abrir a conversa à toa. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | ---------------------------- | | `contact_id` | caminho | UUID | sim | ID do contato (ou do grupo). | * curl ```bash curl -X POST https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60/read \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const contactId = '3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60'; const res = await fetch(`https://platform.toptalking.com.br/v1/contacts/${contactId}/read`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const contato = await res.json(); // contato.unread_count === 0 ``` * Python ```python import os import requests contact_id = "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60" res = requests.post( f"https://platform.toptalking.com.br/v1/contacts/{contact_id}/read", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() ``` #### Resposta `200` O contato, com `unread_count: 0`: ```json { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 0, "last_message_at": "2026-11-11T14:12:40Z", "last_message_preview": "Vocês entregam no sábado?", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:13:02Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------- | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O contato não existe. | ## Histórico da conversa GET`/v1/contacts/{contact_id}/messages`Testar Mensagens da conversa, **das mais recentes para as mais antigas**, com [paginação](/comecando/paginacao/). Use para mostrar a conversa no seu sistema ou dar contexto a uma IA. Funciona também para grupos (com o `id` do grupo). Mensagens apagadas com `scope=me` não aparecem. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ------- | ----------- | -------------------------------------- | | `contact_id` | caminho | UUID | sim | ID do contato (ou do grupo). | | `limit` | query | inteiro | não | Itens por página, 1 a 100 (padrão 20). | | `cursor` | query | string | não | `next_cursor` da página anterior. | * curl ```bash curl "https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60/messages?limit=50" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const contactId = '3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60'; const res = await fetch( `https://platform.toptalking.com.br/v1/contacts/${contactId}/messages?limit=50`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` } }, ); const { data: mensagens } = await res.json(); // Para exibir em ordem cronológica: const conversa = [...mensagens].reverse(); ``` * Python ```python import os import requests contact_id = "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60" res = requests.get( f"https://platform.toptalking.com.br/v1/contacts/{contact_id}/messages", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"limit": 50}, timeout=30, ) res.raise_for_status() conversa = list(reversed(res.json()["data"])) # ordem cronológica ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "inbound", "type": "text", "text": "Vocês entregam no sábado?", "media": null, "status": "delivered", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:12:40Z", "edited_at": null }, { "id": "a9d91e32-912f-4866-b018-e7c4cfe7f45b", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Oi, Maria! Recebemos o seu pedido #1234 e ele já está sendo separado. 😊", "media": null, "status": "read", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:05:02Z", "edited_at": null } ], "has_more": true, "next_cursor": "eyJ0IjoiMjAyNi0xMS0xMVQxNDowNTowMloifQ" } ``` O formato de cada item é o [objeto mensagem](/referencia/mensagens/#o-objeto-mensagem). #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | --------------------- | | 404 | `resource_not_found` | O contato não existe. | # Mensagens > Mensagens de WhatsApp pela API da TopTalking - enviar texto, imagem, vídeo, áudio, nota de voz ou documento (POST /messages), também para grupos; acompanhar o status de entrega; editar (PATCH), apagar (DELETE) e reagir a mensagens. Tudo o que você faz com mensagens pela API aparece no chat do painel como se a equipe tivesse feito: o envio, a edição, a reação e a mensagem apagada. | Ação | Endpoint | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | Enviar texto e/ou mídia (para contatos e [grupos](/referencia/grupos/)) | [`POST /messages`](#enviar-mensagem) | | Consultar uma mensagem e o status de entrega | [`GET /messages/{message_id}`](#buscar-mensagem) | | Editar o texto de uma mensagem enviada (até 15 min) | [`PATCH /messages/{message_id}`](#editar-mensagem) | | Apagar para todos (até 48 h) ou só no painel | [`DELETE /messages/{message_id}`](#apagar-mensagem) | | Reagir com um emoji | [`POST /messages/{message_id}/reaction`](#reagir-a-uma-mensagem) | | Ler o histórico de uma conversa | [`GET /contacts/{contact_id}/messages`](/referencia/contatos/#hist%C3%B3rico-da-conversa) | ## O objeto mensagem ```json { "id": "8d66a526-230c-481a-86e5-be09b6bbb952", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "document", "text": "Segue o boleto.", "media": { "url": "https://exemplo.com/boleto-1234.pdf", "mime_type": "application/pdf", "filename": "boleto-1234.pdf" }, "status": "delivered", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:40:31Z", "edited_at": null } ``` | Campo | Tipo | Descrição | | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `id` | UUID | ID da mensagem. | | `object` | string | Sempre `message`. | | `contact_id` | UUID ou `null` | Contato (conversa) da mensagem. Em grupos, é o `id` do grupo. | | `channel_id` | UUID ou `null` | Canal por onde a mensagem passou. | | `direction` | string | `inbound` (o contato mandou) ou `outbound` (a empresa mandou: equipe, IA, campanha, Agenda ou API). | | `type` | string | Valores comuns: `text`, `image`, `video`, `audio`, `document` e `sticker`. Trate valores desconhecidos como mídia genérica. | | `text` | string ou `null` | Texto ou legenda. Mídia sem legenda pode trazer um rótulo como “📷 Foto”. Mensagem apagada para todos vira “Mensagem apagada”. | | `media` | objeto ou `null` | Arquivo anexado: `url`, `mime_type` e `filename`. | | `status` | string | `queued`, `sent`, `delivered`, `read` ou `failed`. | | `error` | string ou `null` | Motivo da falha quando `status` é `failed`. | | `reply_to_id` | UUID ou `null` | Mensagem citada (quando é uma resposta). | | `reaction` | string ou `null` | Reação enviada por nós a esta mensagem (ex.: `👍`). Veja [Reagir a uma mensagem](#reagir-a-uma-mensagem). | | `sender_name` | string ou `null` | Em grupos, quem mandou. | | `transcript` | string ou `null` | Transcrição de áudio, quando houver. | | `created_at` | data | Quando a mensagem foi criada. | | `edited_at` | data ou `null` | Quando foi editada, se foi. | ### Status de entrega As mensagens enviadas (`outbound`) passam por: ```text queued → sent → delivered → read ↘ failed ``` | Status | Significado | | ----------- | -------------------------------------------------------------- | | `queued` | Na fila para sair. | | `sent` | Saiu da TopTalking para o WhatsApp. | | `delivered` | Chegou no celular do contato. | | `read` | O contato leu (se ele não desativou a confirmação de leitura). | | `failed` | Não foi possível entregar; veja `error`. | Para acompanhar sem ficar consultando, use o webhook [`message.status_updated`](/webhooks/eventos/#messagestatus_updated). ## Enviar mensagem POST`/v1/messages`Testar Envia texto e/ou mídia pelo WhatsApp (ou responde no Chat do site, se a conversa for de lá). A mensagem aparece no chat do painel como qualquer outra. É o endpoint para avisos transacionais (pedido, boleto, lembrete) e para responder conversas. **Destino:** informe `to` (telefone) **ou** `contact_id`. Se o telefone ainda não for contato, **o contato é criado** e a conversa aparece na caixa de entrada do painel. Para **grupos**, use o `id` do grupo em `contact_id` (veja [Grupos](/referencia/grupos/#enviar-mensagem-para-o-grupo)). **Canal usado**, nesta ordem: 1. o `channel_id` enviado, se houver; 2. o canal da conversa, se estiver conectado; 3. o WhatsApp principal conectado. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ----------------- | --------- | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `to` | corpo | string | `to` ou `contact_id` | Telefone com DDI e DDD. Formatação é ignorada; números brasileiros sem `55` recebem o `55`. | | `contact_id` | corpo | UUID | `to` ou `contact_id` | Conversa de destino. Também aceita o `id` de um grupo (veja [`GET /groups`](/referencia/grupos/#listar-grupos)). | | `text` | corpo | string | `text` e/ou `media` | Até 4096 caracteres. Aceita a formatação do WhatsApp: `*negrito*`, `_itálico_`, `~tachado~`. Com `media`, vira a legenda. | | `media` | corpo | objeto | `text` e/ou `media` | Arquivo a enviar (veja abaixo). | | `channel_id` | corpo | UUID | não | Força o envio por este canal. | | `reply_to_id` | corpo | UUID | não | Responde (cita) esta mensagem da conversa. | | `contact_name` | corpo | string | não | Nome usado se o contato ainda não existir. | | `Idempotency-Key` | cabeçalho | string | não (recomendado) | Impede que uma retentativa mande a mensagem duas vezes. Veja [Idempotência](/comecando/idempotencia/). | **Objeto `media`:** | Campo | Tipo | Obrigatório | Descrição | | ------------ | -------- | ----------- | --------------------------------------------------------------------------- | | `url` | string | sim | URL **pública HTTPS** do arquivo (acessível sem login). | | `type` | string | sim | `image`, `video`, `audio` ou `document`. | | `filename` | string | não | Nome exibido (documentos). Padrão: o nome na URL. | | `mime_type` | string | não | Tipo do arquivo (ex.: `application/pdf`). Padrão: deduzido pela extensão. | | `voice_note` | booleano | não | Com `type: "audio"`, envia como nota de voz (sem legenda). Padrão: `false`. | Não use para envios em massa Este endpoint usa o WhatsApp conectado por QR Code: disparos para muitos contatos podem fazer o WhatsApp banir o número. Para promoções e avisos em massa, use [Campanhas](/referencia/campanhas/) (WhatsApp Oficial). Leia [Boas práticas no WhatsApp](/comecando/boas-praticas-whatsapp/). * curl ```bash # Texto para um telefone curl https://platform.toptalking.com.br/v1/messages \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-1234-saiu" \ -d '{ "to": "5511999998888", "text": "Olá, Maria! Seu pedido *#1234* saiu para entrega." }' # Documento com legenda para um contato existente curl https://platform.toptalking.com.br/v1/messages \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: boleto-1234" \ -d '{ "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "text": "Segue o boleto.", "media": { "url": "https://exemplo.com/boleto-1234.pdf", "type": "document", "filename": "boleto-1234.pdf" } }' ``` * JavaScript ```js async function enviarMensagem(corpo, idempotencyKey) { const res = await fetch('https://platform.toptalking.com.br/v1/messages', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify(corpo), }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); return body; } // Texto await enviarMensagem( { to: '5511999998888', text: 'Olá, Maria! Seu pedido *#1234* saiu para entrega.' }, 'pedido-1234-saiu', ); // Imagem com legenda await enviarMensagem( { to: '5511999998888', text: 'Olha como ficou o seu pedido 😍', media: { url: 'https://exemplo.com/fotos/pedido-1234.jpg', type: 'image' }, }, 'pedido-1234-foto', ); // Nota de voz await enviarMensagem( { contact_id: '3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60', media: { url: 'https://exemplo.com/audios/boas-vindas.ogg', type: 'audio', voice_note: true }, }, 'boas-vindas-audio-3f1c2b9a', ); // Responder citando uma mensagem recebida await enviarMensagem( { contact_id: '3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60', reply_to_id: 'b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba', text: 'Entregamos sim! Sábado das 8h às 14h.', }, 'resposta-b7fa4c19', ); ``` * Python ```python import os import requests def enviar_mensagem(corpo: dict, idempotency_key: str) -> dict: res = requests.post( "https://platform.toptalking.com.br/v1/messages", headers={ "Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}", "Idempotency-Key": idempotency_key, }, json=corpo, timeout=30, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") return body # Texto enviar_mensagem( {"to": "5511999998888", "text": "Olá, Maria! Seu pedido *#1234* saiu para entrega."}, "pedido-1234-saiu", ) # Documento com legenda enviar_mensagem( { "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "text": "Segue o boleto.", "media": { "url": "https://exemplo.com/boleto-1234.pdf", "type": "document", "filename": "boleto-1234.pdf", }, }, "boleto-1234", ) ``` #### Resposta `201` A mensagem criada: ```json { "id": "03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Olá, Maria! Seu pedido *#1234* saiu para entrega.", "media": null, "status": "sent", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:30:12Z", "edited_at": null } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `missing_field` | Faltou `to`/`contact_id` ou `text`/`media`. | | 400 | `invalid_phone` | Telefone em `to` inválido. | | 400 | `invalid_field` | Campo inválido: texto acima de 4096 caracteres, `media.url` sem HTTPS, `media.type` desconhecido… | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | `contact_id`, `channel_id` ou `reply_to_id` não encontrado. | | 409 | `idempotency_conflict` | `Idempotency-Key` reutilizada com outro corpo. | | 422 | `channel_unavailable` | Nenhum WhatsApp conectado (ou o canal escolhido está desconectado). [Reconecte o canal](/referencia/canais/#reconectar-um-whatsapp-desconectado). | | 502 | `provider_error` | O WhatsApp recusou o envio. A mensagem fica registrada com status `failed`. | 422 channel_unavailable ```json { "error": { "type": "channel_error", "code": "channel_unavailable", "message": "WhatsApp desconectado. Reconecte no painel.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Buscar mensagem GET`/v1/messages/{message_id}`Testar Busca uma mensagem pelo ID. Use para conferir o status de entrega (`sent` → `delivered` → `read`) de um envio importante. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | --------------- | | `message_id` | caminho | UUID | sim | ID da mensagem. | * curl ```bash curl https://platform.toptalking.com.br/v1/messages/03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const messageId = '03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e'; const res = await fetch(`https://platform.toptalking.com.br/v1/messages/${messageId}`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const mensagem = await res.json(); console.log(mensagem.status); // "read" ``` * Python ```python import os import requests message_id = "03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e" res = requests.get( f"https://platform.toptalking.com.br/v1/messages/{message_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() print(res.json()["status"]) ``` #### Resposta `200` ```json { "id": "03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Olá, Maria! Seu pedido *#1234* saiu para entrega.", "media": null, "status": "read", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:30:12Z", "edited_at": null } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ------------------------------------------------ | | 404 | `resource_not_found` | A mensagem não existe ou é de outra organização. | ## Editar mensagem PATCH`/v1/messages/{message_id}`Testar Corrige o texto de uma mensagem de texto **enviada pela empresa** (`outbound`) em até **15 minutos** depois do envio — o mesmo limite do WhatsApp. O contato vê a mensagem marcada como editada, e o texto novo aparece no painel. Só mensagens de texto: mídias (inclusive legendas) não podem ser editadas. Mensagens recebidas (`inbound`) também não. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ------ | ----------- | --------------------------------- | | `message_id` | caminho | UUID | sim | ID da mensagem enviada. | | `text` | corpo | string | sim | Novo texto (até 4096 caracteres). | * curl ```bash curl -X PATCH https://platform.toptalking.com.br/v1/messages/03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Olá, Maria! Seu pedido *#1234* sai amanhã cedo." }' ``` * JavaScript ```js const messageId = '03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e'; const res = await fetch(`https://platform.toptalking.com.br/v1/messages/${messageId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ text: 'Olá, Maria! Seu pedido *#1234* sai amanhã cedo.' }), }); const body = await res.json(); if (res.status === 409) console.log('Não dá mais para editar:', body.error.message); ``` * Python ```python import os import requests message_id = "03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e" res = requests.patch( f"https://platform.toptalking.com.br/v1/messages/{message_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={"text": "Olá, Maria! Seu pedido *#1234* sai amanhã cedo."}, timeout=30, ) mensagem = res.json() ``` #### Resposta `200` A mensagem com o texto novo e `edited_at` preenchido: ```json { "id": "03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Olá, Maria! Seu pedido *#1234* sai amanhã cedo.", "media": null, "status": "delivered", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:30:12Z", "edited_at": "2026-11-11T14:38:05Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | --------------------- | ---------------------------------------------------------------------------------- | | 400 | `missing_field` | Faltou `text`. | | 400 | `invalid_field` | A mensagem é de mídia (não pode ser editada) ou o texto passou de 4096 caracteres. | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | A mensagem não existe. | | 409 | `invalid_state` | A mensagem foi recebida (`inbound`) ou já passaram 15 minutos do envio. | | 422 | `channel_unavailable` | O WhatsApp da conversa está desconectado. | | 502 | `provider_error` | O WhatsApp recusou a edição. | 409 invalid_state ```json { "error": { "type": "conflict_error", "code": "invalid_state", "message": "Só é possível editar mensagens enviadas há no máximo 15 minutos.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Apagar mensagem DELETE`/v1/messages/{message_id}`Testar Apaga uma mensagem de um destes dois jeitos: | `scope` | O que acontece | Quando dá | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | `everyone` (padrão) | **Apaga para todos** no WhatsApp. No painel e na API, o texto vira “Mensagem apagada”. | Só mensagens **enviadas** (`outbound`), em até **48 horas**. | | `me` | **Só esconde** a mensagem: ela some do painel e de [`GET /contacts/{contact_id}/messages`](/referencia/contatos/#hist%C3%B3rico-da-conversa). O contato continua vendo. | Qualquer mensagem, a qualquer momento. | Mesmo escondida com `scope=me`, a mensagem continua acessível por [`GET /messages/{message_id}`](#buscar-mensagem). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ------ | ----------- | ---------------------------- | | `message_id` | caminho | UUID | sim | ID da mensagem. | | `scope` | query | string | não | `everyone` (padrão) ou `me`. | * curl ```bash # Apagar para todos curl -X DELETE https://platform.toptalking.com.br/v1/messages/0dd803ad-1be8-4d87-8cf8-3b3d0e94de93 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" # Só esconder do painel curl -X DELETE "https://platform.toptalking.com.br/v1/messages/0dd803ad-1be8-4d87-8cf8-3b3d0e94de93?scope=me" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js async function apagarMensagem(messageId, scope = 'everyone') { const res = await fetch( `https://platform.toptalking.com.br/v1/messages/${messageId}?scope=${scope}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` } }, ); const body = await res.json(); if (res.status === 409 && scope === 'everyone') { return apagarMensagem(messageId, 'me'); // passou de 48 h ou é recebida: só esconde } if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); return body; } await apagarMensagem('0dd803ad-1be8-4d87-8cf8-3b3d0e94de93'); ``` * Python ```python import os import requests message_id = "0dd803ad-1be8-4d87-8cf8-3b3d0e94de93" res = requests.delete( f"https://platform.toptalking.com.br/v1/messages/{message_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"scope": "everyone"}, # ou "me" timeout=30, ) mensagem = res.json() ``` #### Resposta `200` A mensagem apagada (com `scope=everyone`): ```json { "id": "0dd803ad-1be8-4d87-8cf8-3b3d0e94de93", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Mensagem apagada", "media": null, "status": "delivered", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:41:00Z", "edited_at": null } ``` Com `scope=me`, a resposta é a mensagem como estava (o texto não muda): ela só deixa de aparecer no painel e no histórico. #### Erros | HTTP | `code` | Quando acontece | | ---- | --------------------- | -------------------------------------------------------------------------------------------------- | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | A mensagem não existe. | | 409 | `invalid_state` | Com `scope=everyone`: a mensagem foi recebida (`inbound`) ou já passaram 48 horas. Use `scope=me`. | | 422 | `channel_unavailable` | Com `scope=everyone`: o WhatsApp da conversa está desconectado. | | 502 | `provider_error` | O WhatsApp recusou apagar a mensagem. | 409 invalid_state ```json { "error": { "type": "conflict_error", "code": "invalid_state", "message": "Só é possível apagar para todos mensagens enviadas há no máximo 48 horas. Para esconder do painel, use scope=me.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Reagir a uma mensagem POST`/v1/messages/{message_id}/reaction`Testar Envia uma reação com emoji a uma mensagem da conversa — recebida ou enviada —, como o “segurar e reagir” do WhatsApp. Útil para confirmar que viu um pedido sem mandar uma mensagem nova. Enviar `emoji` vazio (`""`) **remove** a reação. Não disponível para conversas do Chat do site (`webchat`). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ------ | ----------- | ------------------------------------------------- | | `message_id` | caminho | UUID | sim | ID da mensagem que vai receber a reação. | | `emoji` | corpo | string | sim | **Um** emoji (ex.: `"👍"`). `""` remove a reação. | * curl ```bash curl https://platform.toptalking.com.br/v1/messages/b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba/reaction \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "emoji": "👍" }' ``` * JavaScript ```js async function reagir(messageId, emoji) { const res = await fetch(`https://platform.toptalking.com.br/v1/messages/${messageId}/reaction`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ emoji }), // '' remove }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); return body; } await reagir('b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba', '👍'); ``` * Python ```python import os import requests message_id = "b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba" res = requests.post( f"https://platform.toptalking.com.br/v1/messages/{message_id}/reaction", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={"emoji": "👍"}, # "" remove a reação timeout=30, ) res.raise_for_status() mensagem = res.json() ``` #### Resposta `200` A mensagem com a reação em `reaction`: ```json { "id": "b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "inbound", "type": "text", "text": "Vocês entregam no sábado?", "media": null, "status": "delivered", "error": null, "reply_to_id": null, "reaction": "👍", "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:12:40Z", "edited_at": null } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | --------------------- | ---------------------------------------------------------------------------------------------------------- | | 400 | `missing_field` | Faltou `emoji`. | | 400 | `invalid_field` | Mais de um emoji, ou a conversa é do Chat do site. | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | A mensagem não existe. | | 409 | `invalid_state` | A mensagem ainda não tem identificador no WhatsApp (ex.: ainda está `queued`). Tente de novo em instantes. | | 422 | `channel_unavailable` | O WhatsApp da conversa está desconectado. | | 502 | `provider_error` | O WhatsApp recusou a reação. | 400 invalid_field ```json { "error": { "type": "invalid_request_error", "code": "invalid_field", "message": "Reações não estão disponíveis no Chat do site.", "request_id": "req_6f1d2c3b4a5e" } } ``` # Grupos > Grupos do WhatsApp na API da TopTalking - listar grupos (GET /groups), ver participantes (GET /groups/{group_id}/participants), ler o histórico e enviar mensagens para o grupo usando o id do grupo como contact_id. Os grupos do WhatsApp em que o número conectado participa aparecem no painel como conversas. Na API, **um grupo também é uma conversa**: o `id` do grupo é um `contact_id`. Com ele você: * lê o histórico com [`GET /contacts/{contact_id}/messages`](#ler-as-mensagens-do-grupo); * envia mensagens com [`POST /messages`](#enviar-mensagem-para-o-grupo) e `{ "contact_id": "" }`; * busca, arquiva ou atribui a conversa com os endpoints de [Contatos](/referencia/contatos/) (o contato do grupo tem `is_group: true`). Grupos não são atendidos pelos agentes de IA. ## O objeto grupo ```json { "id": "6fa72198-41f5-4c7f-8fc5-5c84c28e7e00", "object": "group", "name": "Equipe de Vendas", "jid": "120363012345678901@g.us", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "unread_count": 3, "last_message_at": "2026-11-11T13:58:10Z", "last_message_preview": "Fechei mais 2 pedidos hoje 🎉", "archived_at": null, "created_at": "2026-04-01T12:00:00Z" } ``` | Campo | Tipo | Descrição | | ---------------------- | ---------------- | ---------------------------------------------------------------------------------------------------- | | `id` | UUID | ID do grupo. **É o `contact_id` da conversa do grupo**: use para ler o histórico e enviar mensagens. | | `object` | string | Sempre `group`. | | `name` | string ou `null` | Nome do grupo no WhatsApp. | | `jid` | string ou `null` | Identificador do grupo no WhatsApp (termina em `@g.us`). | | `channel_id` | UUID ou `null` | Canal (número de WhatsApp) que participa do grupo. | | `unread_count` | inteiro | Mensagens não lidas no painel. | | `last_message_at` | data ou `null` | Data da última mensagem. | | `last_message_preview` | string ou `null` | Prévia da última mensagem. | | `archived_at` | data ou `null` | Quando a conversa do grupo foi arquivada (`null` = ativa). | | `created_at` | data | Quando o grupo apareceu na TopTalking. | ## Listar grupos GET`/v1/groups`Testar Lista os grupos que aparecem no painel, **dos mais recentes para os mais antigos**, com [paginação](/comecando/paginacao/). Use para descobrir o `id` de um grupo pelo nome antes de ler ou enviar mensagens. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ----- | ------- | ----------- | -------------------------------------------- | | `search` | query | string | não | Parte do nome do grupo (até 100 caracteres). | | `channel_id` | query | UUID | não | Só grupos deste canal. | | `limit` | query | inteiro | não | Itens por página, 1 a 100 (padrão 20). | | `cursor` | query | string | não | `next_cursor` da página anterior. | * curl ```bash curl "https://platform.toptalking.com.br/v1/groups?search=vendas" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const url = new URL('https://platform.toptalking.com.br/v1/groups'); url.searchParams.set('search', 'vendas'); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: grupos } = await res.json(); const grupoId = grupos[0]?.id; // use como contact_id ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/groups", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"search": "vendas"}, timeout=30, ) res.raise_for_status() grupos = res.json()["data"] ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "6fa72198-41f5-4c7f-8fc5-5c84c28e7e00", "object": "group", "name": "Equipe de Vendas", "jid": "120363012345678901@g.us", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "unread_count": 3, "last_message_at": "2026-11-11T13:58:10Z", "last_message_preview": "Fechei mais 2 pedidos hoje 🎉", "archived_at": null, "created_at": "2026-04-01T12:00:00Z" }, { "id": "8255ed43-3d44-4909-bc51-f7a9e810f214", "object": "group", "name": "Fornecedores - Vendas", "jid": "120363029876543210@g.us", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "unread_count": 0, "last_message_at": "2026-11-08T17:22:05Z", "last_message_preview": "O lote de tênis chega na terça.", "archived_at": null, "created_at": "2026-05-12T09:40:00Z" } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## Participantes do grupo GET`/v1/groups/{group_id}/participants`Testar Busca **na hora, no WhatsApp conectado**, quem está no grupo e quem é administrador. Por isso precisa do canal do grupo **conectado** — se ele estiver desconectado, a resposta é `422 channel_unavailable`. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ---------- | ------- | ---- | ----------- | ----------------------------------- | | `group_id` | caminho | UUID | sim | ID do grupo (o mesmo `contact_id`). | A lista vem inteira numa página (`has_more: false`). * curl ```bash curl https://platform.toptalking.com.br/v1/groups/6fa72198-41f5-4c7f-8fc5-5c84c28e7e00/participants \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const groupId = '6fa72198-41f5-4c7f-8fc5-5c84c28e7e00'; const res = await fetch( `https://platform.toptalking.com.br/v1/groups/${groupId}/participants`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` } }, ); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); const admins = body.data.filter((p) => p.role !== 'member'); ``` * Python ```python import os import requests group_id = "6fa72198-41f5-4c7f-8fc5-5c84c28e7e00" res = requests.get( f"https://platform.toptalking.com.br/v1/groups/{group_id}/participants", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() participantes = res.json()["data"] ``` #### Resposta `200` ```json { "object": "list", "data": [ { "object": "group_participant", "phone": "5511988887777", "name": "Loja Aurora", "role": "superadmin", "avatar_url": "https://arquivos.exemplo.com/avatars/loja-aurora.jpg" }, { "object": "group_participant", "phone": "5511991112222", "name": "Carla Mendes", "role": "admin", "avatar_url": null }, { "object": "group_participant", "phone": "5521987654321", "name": "João Pereira", "role": "member", "avatar_url": null } ], "has_more": false, "next_cursor": null } ``` | Campo | Tipo | Descrição | | ------------ | ---------------- | -------------------------------------------------------------------------------------- | | `object` | string | Sempre `group_participant`. | | `phone` | string ou `null` | Telefone do participante, só dígitos com DDI. | | `name` | string ou `null` | Nome do participante, quando conhecido. | | `role` | string | `member` (participante), `admin` (administrador) ou `superadmin` (quem criou o grupo). | | `avatar_url` | string ou `null` | Foto de perfil, quando disponível. | #### Erros | HTTP | `code` | Quando acontece | | ---- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | 404 | `resource_not_found` | O grupo não existe ou é de outra organização. | | 422 | `channel_unavailable` | O WhatsApp do grupo está desconectado. [Reconecte o canal](/referencia/canais/#reconectar-um-whatsapp-desconectado) e tente de novo. | | 502 | `provider_error` | O WhatsApp não respondeu à consulta. Tente de novo em instantes. | 422 channel_unavailable ```json { "error": { "type": "channel_error", "code": "channel_unavailable", "message": "WhatsApp desconectado. Reconecte no painel.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Ler as mensagens do grupo Use o `id` do grupo em [`GET /contacts/{contact_id}/messages`](/referencia/contatos/#hist%C3%B3rico-da-conversa). Em grupos, `sender_name` diz **quem mandou** cada mensagem recebida. GET`/v1/contacts/{contact_id}/messages`Testar * curl ```bash curl "https://platform.toptalking.com.br/v1/contacts/6fa72198-41f5-4c7f-8fc5-5c84c28e7e00/messages?limit=20" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const groupId = '6fa72198-41f5-4c7f-8fc5-5c84c28e7e00'; const res = await fetch( `https://platform.toptalking.com.br/v1/contacts/${groupId}/messages?limit=20`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` } }, ); const { data: mensagens } = await res.json(); for (const m of mensagens) console.log(`${m.sender_name ?? 'Nós'}: ${m.text}`); ``` * Python ```python import os import requests group_id = "6fa72198-41f5-4c7f-8fc5-5c84c28e7e00" res = requests.get( f"https://platform.toptalking.com.br/v1/contacts/{group_id}/messages", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"limit": 20}, timeout=30, ) res.raise_for_status() for m in res.json()["data"]: print(f"{m['sender_name'] or 'Nós'}: {m['text']}") ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "cb42b3fb-9e83-46aa-b7f6-f3aca950abf5", "object": "message", "contact_id": "6fa72198-41f5-4c7f-8fc5-5c84c28e7e00", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "inbound", "type": "text", "text": "Fechei mais 2 pedidos hoje 🎉", "media": null, "status": "delivered", "error": null, "reply_to_id": null, "reaction": null, "sender_name": "Carla Mendes", "transcript": null, "created_at": "2026-11-11T13:58:10Z", "edited_at": null } ], "has_more": true, "next_cursor": "eyJ0IjoiMjAyNi0xMS0xMVQxMzo1ODoxMFoifQ" } ``` ## Enviar mensagem para o grupo Use [`POST /messages`](/referencia/mensagens/#enviar-mensagem) com o `id` do grupo em `contact_id` (`to` é só para telefones). Texto, mídia e respostas citando (`reply_to_id`) funcionam como numa conversa individual. POST`/v1/messages`Testar * curl ```bash curl https://platform.toptalking.com.br/v1/messages \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: meta-diaria-2026-11-11" \ -d '{ "contact_id": "6fa72198-41f5-4c7f-8fc5-5c84c28e7e00", "text": "Bom dia, time! Meta de hoje: 15 pedidos. Bora! 💪" }' ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/messages', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'meta-diaria-2026-11-11', }, body: JSON.stringify({ contact_id: '6fa72198-41f5-4c7f-8fc5-5c84c28e7e00', // id do grupo text: 'Bom dia, time! Meta de hoje: 15 pedidos. Bora! 💪', }), }); const mensagem = await res.json(); ``` * Python ```python import os import requests res = requests.post( "https://platform.toptalking.com.br/v1/messages", headers={ "Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}", "Idempotency-Key": "meta-diaria-2026-11-11", }, json={ "contact_id": "6fa72198-41f5-4c7f-8fc5-5c84c28e7e00", # id do grupo "text": "Bom dia, time! Meta de hoje: 15 pedidos. Bora! 💪", }, timeout=30, ) res.raise_for_status() mensagem = res.json() ``` #### Resposta `201` ```json { "id": "37e1fdb6-6cd4-4345-bbaa-58216b232122", "object": "message", "contact_id": "6fa72198-41f5-4c7f-8fc5-5c84c28e7e00", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Bom dia, time! Meta de hoje: 15 pedidos. Bora! 💪", "media": null, "status": "sent", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T11:00:03Z", "edited_at": null } ``` Os erros são os de [`POST /messages`](/referencia/mensagens/#enviar-mensagem). O mais comum em grupos é `422 channel_unavailable`, quando o WhatsApp do grupo está desconectado. Grupos também têm regras Mensagens automáticas em grupos grandes, ou em grupos de pessoas que não esperam receber, podem levar a denúncias e ao banimento do número. Leia [Boas práticas no WhatsApp](/comecando/boas-praticas-whatsapp/). ## Receber mensagens de grupos Mensagens recebidas em grupos chegam no webhook [`message.received`](/webhooks/eventos/#messagereceived) como as demais: `data.contact.is_group` é `true`, `data.contact.id` é o `id` do grupo e `data.message.sender_name` diz quem mandou. Para listar só conversas de grupo (ou só individuais) junto com os demais filtros de contato, use [`GET /contacts?is_group=true`](/referencia/contatos/#listar-contatos). # Kanban > Etapas do Kanban (pipeline) na API da TopTalking - listar colunas com GET /stages, mover cards com PATCH /contacts/{contact_id} e stage_id, tirar da etapa e finalizar conversas na etapa final. O **Kanban** da TopTalking organiza as conversas em etapas (colunas), como um funil de vendas ou de atendimento. Cada contato está em **no máximo uma etapa**. As etapas são configuradas no painel em Configurações → Etapas do Kanban. A etapa com `is_final: true` (normalmente “Finalizado”) é especial: **mover um card para ela finaliza a conversa**, igual ao painel. ## O objeto etapa ```json { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "object": "stage", "name": "Negociação", "color": "#8b5cf6", "position": 2, "is_final": false } ``` | Campo | Tipo | Descrição | | ---------- | -------- | -------------------------------------------------------------------------- | | `id` | UUID | ID da etapa. Use em `stage_id`. | | `object` | string | Sempre `stage`. | | `name` | string | Nome da coluna. | | `color` | string | Cor da coluna no painel (hexadecimal). | | `position` | inteiro | Ordem da coluna (0 = primeira). | | `is_final` | booleano | Etapa terminal (“Finalizado”). Mover um card para ela finaliza a conversa. | Nos contatos e nos webhooks, a etapa aparece resumida (`id`, `name`, `is_final`). ## Listar etapas GET`/v1/stages`Testar Devolve as etapas **na ordem das colunas**. Use para descobrir o `id` de cada etapa antes de mover cards ou filtrar contatos. Não tem parâmetros. A lista vem inteira numa página (`has_more: false`). * curl ```bash curl https://platform.toptalking.com.br/v1/stages \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/stages', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: etapas } = await res.json(); const porNome = Object.fromEntries(etapas.map((e) => [e.name, e.id])); // porNome['Negociação'] → "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11" ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/stages", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() por_nome = {e["name"]: e["id"] for e in res.json()["data"]} # por_nome["Negociação"] -> "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11" ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "e63d8231-3992-457e-a8b0-d1be2fa591d4", "object": "stage", "name": "Novo lead", "color": "#3b82f6", "position": 0, "is_final": false }, { "id": "5f861a0a-9667-4215-b498-3be9da314b60", "object": "stage", "name": "Em atendimento", "color": "#f59e0b", "position": 1, "is_final": false }, { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "object": "stage", "name": "Negociação", "color": "#8b5cf6", "position": 2, "is_final": false }, { "id": "3c671a88-07fb-4ccc-80db-184be834360c", "object": "stage", "name": "Finalizado", "color": "#22c55e", "position": 3, "is_final": true } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## Mover um card Para mover um contato de etapa, use [`PATCH /contacts/{contact_id}`](/referencia/contatos/#atualizar-contato-e-mover-no-kanban) com `stage_id`: PATCH`/v1/contacts/{contact_id}`Testar | Corpo | Efeito | | --------------------------------------- | -------------------------------------------------- | | `{ "stage_id": "" }` | Move o card para a etapa. | | `{ "stage_id": "" }` | Move para a etapa final **e finaliza a conversa**. | | `{ "stage_id": null }` | Tira o card de qualquer etapa. | * curl ```bash curl -X PATCH https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "stage_id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11" }' ``` * JavaScript ```js async function moverCard(contactId, stageId) { const res = await fetch(`https://platform.toptalking.com.br/v1/contacts/${contactId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ stage_id: stageId }), // null tira de qualquer etapa }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); return body; // contato atualizado, com body.stage } await moverCard('3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60', '9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11'); ``` * Python ```python import os import requests def mover_card(contact_id: str, stage_id: str | None) -> dict: res = requests.patch( f"https://platform.toptalking.com.br/v1/contacts/{contact_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={"stage_id": stage_id}, # None tira de qualquer etapa timeout=30, ) body = res.json() if not res.ok: raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}") return body # contato atualizado, com body["stage"] mover_card("3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11") ``` A resposta `200` é o [contato atualizado](/referencia/contatos/#o-objeto-contato), já com o novo `stage`. Um `stage_id` que não é etapa da organização responde `400 invalid_field` (`param: "stage_id"`); um contato inexistente, `404 resource_not_found`. ## Ler um quadro inteiro Para montar o seu próprio Kanban, busque as etapas e depois os contatos de cada uma com o filtro `stage_id`: ```js const API = 'https://platform.toptalking.com.br/v1'; const headers = { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }; const { data: etapas } = await (await fetch(`${API}/stages`, { headers })).json(); const quadro = []; for (const etapa of etapas) { const url = `${API}/contacts?stage_id=${etapa.id}&archived=false&limit=100`; const { data: cards } = await (await fetch(url, { headers })).json(); quadro.push({ ...etapa, cards }); } ``` ## Ser avisado quando um card muda de etapa Assine o webhook [`contact.stage_changed`](/webhooks/eventos/#contactstage_changed). Ele chega toda vez que um card muda de coluna — pelo painel, pela IA ou pela API — com a etapa nova e a anterior. # Mensagens agendadas > Agende mensagens de WhatsApp pela API da TopTalking (POST /scheduled-messages) com variáveis {{nome}} e {{telefone}} e anexos, liste, consulte e cancele - as mesmas mensagens da Agenda do painel. Mensagens agendadas são as mesmas da **Agenda** do painel: o que você agenda pela API aparece lá, e a equipe pode ver e acompanhar. O despachante verifica a fila **a cada minuto** e envia as mensagens que chegaram no horário. Use para lembretes de consulta, cobranças, follow-ups e renovações. * `send_at` precisa estar **no futuro** e em **até 365 dias**. * O texto aceita as variáveis `{{nome}}` e `{{telefone}}`, preenchidas com os dados do contato na hora do envio. * Até **10 anexos**; o texto vira a legenda do primeiro anexo que aceita legenda. * Não disponível para conversas do **Chat do site** (`webchat`) nem para grupos. ## O objeto mensagem agendada ```json { "id": "603916c7-604c-4c56-8a58-f3f76886634b", "object": "scheduled_message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "to": "5511999998888", "title": "Lembrete de consulta", "text": "Oi {{nome}}, passando para lembrar da sua consulta amanhã às 9h.", "media": [], "send_at": "2026-12-01T15:00:00Z", "status": "pending", "sent_at": null, "message_id": null, "error": null, "created_at": "2026-11-11T14:50:00Z", "updated_at": "2026-11-11T14:50:00Z" } ``` | Campo | Tipo | Descrição | | ------------ | ---------------- | -------------------------------------------------------- | | `id` | UUID | ID da mensagem agendada. | | `object` | string | Sempre `scheduled_message`. | | `contact_id` | UUID ou `null` | Contato de destino. | | `to` | string ou `null` | Telefone de destino. | | `title` | string | Título na Agenda (padrão: “Mensagem via API”). | | `text` | string ou `null` | Texto, com as variáveis ainda não substituídas. | | `media` | lista | Anexos: cada um com `url`, `mime_type` e `filename`. | | `send_at` | data | Quando deve ser enviada. | | `status` | string | `pending`, `processing`, `sent`, `failed` ou `canceled`. | | `sent_at` | data ou `null` | Quando foi enviada. | | `message_id` | UUID ou `null` | A [mensagem](/referencia/mensagens/) gerada no envio. | | `error` | string ou `null` | Motivo da falha, quando `status` é `failed`. | | `created_at` | data | Criação. | | `updated_at` | data | Última alteração. | | Status | Significado | | ------------ | ------------------------------------------------------------- | | `pending` | Esperando o horário. É o único status que pode ser cancelado. | | `processing` | Sendo enviada agora. | | `sent` | Enviada (veja `sent_at` e `message_id`). | | `failed` | Não foi possível enviar (veja `error`). | | `canceled` | Cancelada antes do envio. | ## Agendar mensagem POST`/v1/scheduled-messages`Testar Programa uma mensagem para sair numa data e hora. Ela aparece na Agenda do painel e é enviada no horário pelo WhatsApp da conversa. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ----------------- | --------- | ------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `to` | corpo | string | `to` ou `contact_id` | Telefone com DDI e DDD. | | `contact_id` | corpo | UUID | `to` ou `contact_id` | Contato de destino. | | `send_at` | corpo | data | sim | ISO 8601 **com fuso** (ex.: `2026-12-01T12:00:00-03:00`). No futuro e em até 365 dias. | | `text` | corpo | string | `text` e/ou `media` | Até 4096 caracteres. Aceita `{{nome}}` e `{{telefone}}`. | | `media` | corpo | lista | `text` e/ou `media` | Até 10 anexos, cada um com `url` (HTTPS pública), `type` (`image`, `video`, `audio`, `document`) e, opcionalmente, `filename`, `mime_type` e `voice_note`. O texto vira legenda do primeiro anexo que aceita legenda. | | `title` | corpo | string | não | Título na Agenda (até 120 caracteres). Padrão: “Mensagem via API”. | | `Idempotency-Key` | cabeçalho | string | não (recomendado) | Evita agendar duas vezes numa retentativa. | Sempre informe o fuso `2026-12-01T12:00:00-03:00` é meio-dia em Brasília. Sem o fuso, a data é ambígua e a API recusa. Na resposta, `send_at` pode vir convertido para UTC (`2026-12-01T15:00:00Z`) — é o mesmo instante. * curl ```bash curl https://platform.toptalking.com.br/v1/scheduled-messages \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: consulta-987-lembrete" \ -d '{ "to": "5511999998888", "send_at": "2026-12-01T12:00:00-03:00", "title": "Lembrete de consulta", "text": "Oi {{nome}}, passando para lembrar da sua consulta amanhã às 9h." }' ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/scheduled-messages', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'consulta-987-lembrete-mapa', }, body: JSON.stringify({ contact_id: '3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60', send_at: '2026-12-01T12:00:00-03:00', title: 'Lembrete de consulta', text: 'Oi {{nome}}, passando para lembrar da sua consulta amanhã às 9h. Segue o endereço:', media: [{ url: 'https://exemplo.com/mapa-clinica.png', type: 'image' }], }), }); const agendada = await res.json(); console.log(agendada.id, agendada.status); // "pending" ``` * Python ```python import os import requests res = requests.post( "https://platform.toptalking.com.br/v1/scheduled-messages", headers={ "Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}", "Idempotency-Key": "consulta-987-lembrete", }, json={ "to": "5511999998888", "send_at": "2026-12-01T12:00:00-03:00", "title": "Lembrete de consulta", "text": "Oi {{nome}}, passando para lembrar da sua consulta amanhã às 9h.", }, timeout=30, ) agendada = res.json() if not res.ok: raise RuntimeError(f"{agendada['error']['code']}: {agendada['error']['message']}") ``` #### Resposta `201` ```json { "id": "603916c7-604c-4c56-8a58-f3f76886634b", "object": "scheduled_message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "to": "5511999998888", "title": "Lembrete de consulta", "text": "Oi {{nome}}, passando para lembrar da sua consulta amanhã às 9h.", "media": [], "send_at": "2026-12-01T15:00:00Z", "status": "pending", "sent_at": null, "message_id": null, "error": null, "created_at": "2026-11-11T14:50:00Z", "updated_at": "2026-11-11T14:50:00Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `missing_field` | Faltou `send_at`, `to`/`contact_id` ou `text`/`media`. | | 400 | `invalid_field` | Campo inválido: `send_at` sem fuso, no passado ou a mais de 365 dias, mais de 10 anexos, texto longo demais, conversa do Chat do site ou de grupo… | | 400 | `invalid_phone` | Telefone inválido. | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | `contact_id` não encontrado. | | 409 | `idempotency_conflict` | `Idempotency-Key` reutilizada com outro corpo. | 400 invalid_field ```json { "error": { "type": "invalid_request_error", "code": "invalid_field", "message": "`send_at` precisa estar no futuro.", "param": "send_at", "request_id": "req_6f1d2c3b4a5e" } } ``` Quando a mensagem for enviada (ou falhar), você recebe os webhooks [`scheduled_message.sent`](/webhooks/eventos/#scheduled_messagesent) ou [`scheduled_message.failed`](/webhooks/eventos/#scheduled_messagefailed). ## Listar mensagens agendadas GET`/v1/scheduled-messages`Testar As mensagens agendadas, ordenadas pelo horário de envio (as mais próximas primeiro). Use para mostrar a agenda de um cliente ou conferir o que ainda vai sair. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ----- | ------- | ----------- | -------------------------------------------------------- | | `status` | query | string | não | `pending`, `processing`, `sent`, `failed` ou `canceled`. | | `contact_id` | query | UUID | não | Só as deste contato. | | `limit` | query | inteiro | não | Itens por página, 1 a 100 (padrão 20). | | `cursor` | query | string | não | `next_cursor` da página anterior. | * curl ```bash curl "https://platform.toptalking.com.br/v1/scheduled-messages?status=pending&limit=50" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch( 'https://platform.toptalking.com.br/v1/scheduled-messages?status=pending&limit=50', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` } }, ); const { data: pendentes } = await res.json(); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/scheduled-messages", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"status": "pending", "limit": 50}, timeout=30, ) res.raise_for_status() pendentes = res.json()["data"] ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "603916c7-604c-4c56-8a58-f3f76886634b", "object": "scheduled_message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "to": "5511999998888", "title": "Lembrete de consulta", "text": "Oi {{nome}}, passando para lembrar da sua consulta amanhã às 9h.", "media": [], "send_at": "2026-12-01T15:00:00Z", "status": "pending", "sent_at": null, "message_id": null, "error": null, "created_at": "2026-11-11T14:50:00Z", "updated_at": "2026-11-11T14:50:00Z" } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## Buscar mensagem agendada GET`/v1/scheduled-messages/{scheduled_message_id}`Testar Uma mensagem agendada pelo ID. Use para saber se já saiu (`status`, `sent_at`) e qual mensagem ela gerou (`message_id`). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ---------------------- | ------- | ---- | ----------- | ------------------------ | | `scheduled_message_id` | caminho | UUID | sim | ID da mensagem agendada. | * curl ```bash curl https://platform.toptalking.com.br/v1/scheduled-messages/d3440bed-1a6f-4470-9d89-cec62fe2fc67 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const id = 'd3440bed-1a6f-4470-9d89-cec62fe2fc67'; const res = await fetch(`https://platform.toptalking.com.br/v1/scheduled-messages/${id}`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const agendada = await res.json(); ``` * Python ```python import os import requests scheduled_id = "d3440bed-1a6f-4470-9d89-cec62fe2fc67" res = requests.get( f"https://platform.toptalking.com.br/v1/scheduled-messages/{scheduled_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() agendada = res.json() ``` #### Resposta `200` Exemplo de uma mensagem que já foi enviada: ```json { "id": "d3440bed-1a6f-4470-9d89-cec62fe2fc67", "object": "scheduled_message", "contact_id": "721bfe94-e549-4354-b90c-a8d679e9b271", "to": "5521987654321", "title": "Renovação do plano", "text": "Oi {{nome}}! Seu plano vence na sexta. Quer que a gente renove?", "media": [], "send_at": "2026-11-10T13:00:00Z", "status": "sent", "sent_at": "2026-11-10T13:00:41Z", "message_id": "8e395744-cc4c-4eae-9ccd-06c8dde834e0", "error": null, "created_at": "2026-11-03T18:22:10Z", "updated_at": "2026-11-10T13:00:41Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ------------------------------- | | 404 | `resource_not_found` | A mensagem agendada não existe. | ## Cancelar mensagem agendada DELETE`/v1/scheduled-messages/{scheduled_message_id}`Testar Cancela uma mensagem que **ainda está `pending`** — ela some da fila e fica como cancelada na Agenda. A resposta é o item com `status: "canceled"`. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ---------------------- | ------- | ---- | ----------- | ------------------------ | | `scheduled_message_id` | caminho | UUID | sim | ID da mensagem agendada. | * curl ```bash curl -X DELETE https://platform.toptalking.com.br/v1/scheduled-messages/603916c7-604c-4c56-8a58-f3f76886634b \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const id = '603916c7-604c-4c56-8a58-f3f76886634b'; const res = await fetch(`https://platform.toptalking.com.br/v1/scheduled-messages/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const body = await res.json(); if (res.status === 409) console.log('Não dá mais para cancelar:', body.error.message); ``` * Python ```python import os import requests scheduled_id = "603916c7-604c-4c56-8a58-f3f76886634b" res = requests.delete( f"https://platform.toptalking.com.br/v1/scheduled-messages/{scheduled_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) body = res.json() if res.status_code == 409: print("Não dá mais para cancelar:", body["error"]["message"]) ``` #### Resposta `200` ```json { "id": "603916c7-604c-4c56-8a58-f3f76886634b", "object": "scheduled_message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "to": "5511999998888", "title": "Lembrete de consulta", "text": "Oi {{nome}}, passando para lembrar da sua consulta amanhã às 9h.", "media": [], "send_at": "2026-12-01T15:00:00Z", "status": "canceled", "sent_at": null, "message_id": null, "error": null, "created_at": "2026-11-11T14:50:00Z", "updated_at": "2026-11-11T15:02:13Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------------------------------------------------------------------------------------- | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | A mensagem agendada não existe. | | 409 | `invalid_state` | A mensagem não está mais `pending` (já foi enviada, está sendo enviada, falhou ou já foi cancelada). | 409 invalid_state ```json { "error": { "type": "conflict_error", "code": "invalid_state", "message": "Só é possível cancelar mensagens pendentes (status atual: sent).", "request_id": "req_6f1d2c3b4a5e" } } ``` # Equipe > Membros da organização na API da TopTalking (GET /members e GET /members/{member_id}) e como atribuir uma conversa a um atendente com PATCH /contacts/{contact_id} e assigned_to. Os **membros** são as pessoas da equipe que usam o painel: donos, administradores e atendentes. Pela API você lista a equipe e usa o `id` de cada membro para **atribuir conversas** (`assigned_to`) e para filtrar as conversas de cada atendente. Convidar, remover e mudar o papel de membros é feito no painel. ## O objeto membro ```json { "id": "4083deb8-f96e-4093-8598-ebdd4dee786b", "object": "member", "name": "Carla Mendes", "email": "carla@lojaaurora.com.br", "role": "agent", "avatar_url": "https://arquivos.exemplo.com/avatars/carla.jpg", "joined_at": "2026-03-10T13:00:00Z" } ``` | Campo | Tipo | Descrição | | ------------ | ---------------- | ------------------------------------------------------------------------ | | `id` | UUID | ID do usuário. **Use em `assigned_to`** para atribuir conversas. | | `object` | string | Sempre `member`. | | `name` | string ou `null` | Nome da pessoa. | | `email` | string ou `null` | E-mail de login. | | `role` | string | `owner` (dono da conta), `admin` (administrador) ou `agent` (atendente). | | `avatar_url` | string ou `null` | Foto de perfil. | | `joined_at` | data | Quando entrou na organização. | ## Listar membros GET`/v1/members`Testar Todos os membros da organização. Use para montar um seletor de responsável ou para descobrir o `id` de um atendente pelo nome ou e-mail. Não tem parâmetros. A lista vem inteira numa página (`has_more: false`). * curl ```bash curl https://platform.toptalking.com.br/v1/members \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/members', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: equipe } = await res.json(); const carla = equipe.find((m) => m.email === 'carla@lojaaurora.com.br'); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/members", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() equipe = res.json()["data"] carla = next(m for m in equipe if m["email"] == "carla@lojaaurora.com.br") ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "4ce0bb29-e6f6-4a78-8d63-747e73e00071", "object": "member", "name": "Rafael Costa", "email": "rafael@lojaaurora.com.br", "role": "owner", "avatar_url": "https://arquivos.exemplo.com/avatars/rafael.jpg", "joined_at": "2026-02-10T12:00:00Z" }, { "id": "384d33f5-953e-4d07-a85e-69e67e1bf89c", "object": "member", "name": "Bruno Alves", "email": "bruno@lojaaurora.com.br", "role": "admin", "avatar_url": null, "joined_at": "2026-02-15T18:30:00Z" }, { "id": "4083deb8-f96e-4093-8598-ebdd4dee786b", "object": "member", "name": "Carla Mendes", "email": "carla@lojaaurora.com.br", "role": "agent", "avatar_url": "https://arquivos.exemplo.com/avatars/carla.jpg", "joined_at": "2026-03-10T13:00:00Z" } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## Buscar membro GET`/v1/members/{member_id}`Testar Um membro pelo ID. Útil para mostrar o nome do responsável de uma conversa a partir do `assigned_to` do contato. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ----------- | ------- | ---- | ----------- | ---------------------------------------------- | | `member_id` | caminho | UUID | sim | ID do membro (o mesmo valor de `assigned_to`). | * curl ```bash curl https://platform.toptalking.com.br/v1/members/4083deb8-f96e-4093-8598-ebdd4dee786b \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const memberId = '4083deb8-f96e-4093-8598-ebdd4dee786b'; const res = await fetch(`https://platform.toptalking.com.br/v1/members/${memberId}`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const membro = await res.json(); console.log(`Responsável: ${membro.name}`); ``` * Python ```python import os import requests member_id = "4083deb8-f96e-4093-8598-ebdd4dee786b" res = requests.get( f"https://platform.toptalking.com.br/v1/members/{member_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() membro = res.json() ``` #### Resposta `200` ```json { "id": "4083deb8-f96e-4093-8598-ebdd4dee786b", "object": "member", "name": "Carla Mendes", "email": "carla@lojaaurora.com.br", "role": "agent", "avatar_url": "https://arquivos.exemplo.com/avatars/carla.jpg", "joined_at": "2026-03-10T13:00:00Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | --------------------------------------------------------------------------- | | 404 | `resource_not_found` | Não há membro com esse ID nesta organização (ex.: a pessoa saiu da equipe). | ## Atribuir uma conversa Para definir o **responsável** por uma conversa, envie o `id` do membro em `assigned_to` com [`PATCH /contacts/{contact_id}`](/referencia/contatos/#atualizar-contato-e-mover-no-kanban). A conversa passa a aparecer para ele no painel como “minha”. PATCH`/v1/contacts/{contact_id}`Testar | Corpo | Efeito | | ------------------------------------- | ----------------------------------------- | | `{ "assigned_to": "" }` | Atribui a conversa a este membro. | | `{ "assigned_to": null }` | Libera a conversa (fica sem responsável). | * curl ```bash curl -X PATCH https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b" }' ``` * JavaScript ```js async function atribuir(contactId, memberId) { const res = await fetch(`https://platform.toptalking.com.br/v1/contacts/${contactId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ assigned_to: memberId }), // null libera a conversa }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); return body; } await atribuir('3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60', '4083deb8-f96e-4093-8598-ebdd4dee786b'); ``` * Python ```python import os import requests contact_id = "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60" res = requests.patch( f"https://platform.toptalking.com.br/v1/contacts/{contact_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={"assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b"}, # None libera timeout=30, ) res.raise_for_status() contato = res.json() ``` A resposta `200` é o [contato atualizado](/referencia/contatos/#o-objeto-contato), com o novo `assigned_to`. A atribuição fica registrada no histórico de atribuições da conversa no painel e dispara o webhook [`contact.updated`](/webhooks/eventos/#contactupdated) com `assigned_to` em `changed_fields`. | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------------------------------------------------------- | | 400 | `invalid_field` | `assigned_to` não é um membro da organização (`param: "assigned_to"`). | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O contato não existe. | 400 invalid_field ```json { "error": { "type": "invalid_request_error", "code": "invalid_field", "message": "Este usuário não é membro da organização. Veja GET /v1/members.", "param": "assigned_to", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Conversas de cada atendente Filtre [`GET /contacts`](/referencia/contatos/#listar-contatos) por `assigned_to`: * `?assigned_to=` — as conversas de um atendente; * `?assigned_to=none` — as conversas **sem responsável** (ótimo para distribuir a fila). - curl ```bash # Conversas ativas sem responsável curl "https://platform.toptalking.com.br/v1/contacts?assigned_to=none&archived=false&is_group=false" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` - JavaScript ```js // Distribui as conversas sem responsável entre os atendentes (rodízio simples) const API = 'https://platform.toptalking.com.br/v1'; const headers = { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }; const { data: equipe } = await (await fetch(`${API}/members`, { headers })).json(); const atendentes = equipe.filter((m) => m.role === 'agent'); const url = `${API}/contacts?assigned_to=none&archived=false&is_group=false&limit=50`; const { data: fila } = await (await fetch(url, { headers })).json(); for (const [i, contato] of fila.entries()) { const responsavel = atendentes[i % atendentes.length]; await fetch(`${API}/contacts/${contato.id}`, { method: 'PATCH', headers, body: JSON.stringify({ assigned_to: responsavel.id }), }); } ``` - Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/contacts", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"assigned_to": "none", "archived": "false", "is_group": "false"}, timeout=30, ) res.raise_for_status() sem_responsavel = res.json()["data"] ``` # Agentes de IA > Agentes de IA da TopTalking pela API - listar (GET /ai-agents), buscar, ligar e desligar (PATCH /ai-agents/{agent_id}) e escolher o agente ou desligar a IA em uma conversa com PATCH /contacts/{contact_id}. Os **agentes de IA** respondem as conversas do WhatsApp e do Chat do site sozinhos, seguindo as instruções, FAQs e a base de conhecimento configuradas no painel (Agentes de IA). Pela API você: * lista os agentes e vê qual é o **padrão**; * **liga ou desliga** um agente; * escolhe **qual agente** atende uma conversa, ou **desliga a IA** só naquela conversa (para um humano assumir). Criar agentes e editar instruções, FAQs e base de conhecimento é feito no painel. ## O objeto agente de IA ```json { "id": "523fc881-026e-4e04-89e2-8a99c28607c9", "object": "ai_agent", "name": "Aurora", "description": "Atendimento geral: dúvidas sobre pedidos, entregas e trocas.", "active": true, "is_default": true, "tone": "profissional_caloroso", "strict_mode": false, "created_at": "2026-03-05T14:00:00Z", "updated_at": "2026-11-02T10:15:00Z" } ``` | Campo | Tipo | Descrição | | ------------- | ---------------- | ----------------------------------------------------------------------------------------- | | `id` | UUID | ID do agente. Use em `ai_agent_id` de uma conversa. | | `object` | string | Sempre `ai_agent`. | | `name` | string | Nome do agente. | | `description` | string ou `null` | Descrição livre (para que serve o agente). | | `active` | booleano | Se o agente está respondendo. | | `is_default` | booleano | Agente **padrão**: atende as conversas que não têm agente definido (`ai_agent_id: null`). | | `tone` | string | Tom de voz configurado no painel (ex.: `profissional_caloroso`). | | `strict_mode` | booleano | Modo restrito: só responde com base nas FAQs, na base de conhecimento e nas instruções. | | `created_at` | data | Criação. | | `updated_at` | data | Última alteração. | ## Quem responde uma conversa A IA responde uma conversa quando **as três condições** são verdadeiras: 1. a conversa está com a IA ligada (`ai_enabled: true` no [contato](/referencia/contatos/#o-objeto-contato)); 2. o agente da conversa — o de `ai_agent_id` ou, se for `null`, o agente padrão — está `active: true`; 3. a IA da organização está ligada (ou seja, o agente **padrão** está ativo). Grupos não são atendidos pela IA. ## Listar agentes de IA GET`/v1/ai-agents`Testar Todos os agentes da organização. Use para descobrir o `id` do agente certo antes de atribuí-lo a uma conversa. Não tem parâmetros. A lista vem inteira numa página (`has_more: false`). * curl ```bash curl https://platform.toptalking.com.br/v1/ai-agents \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/ai-agents', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: agentes } = await res.json(); const padrao = agentes.find((a) => a.is_default); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/ai-agents", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() agentes = res.json()["data"] padrao = next(a for a in agentes if a["is_default"]) ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "523fc881-026e-4e04-89e2-8a99c28607c9", "object": "ai_agent", "name": "Aurora", "description": "Atendimento geral: dúvidas sobre pedidos, entregas e trocas.", "active": true, "is_default": true, "tone": "profissional_caloroso", "strict_mode": false, "created_at": "2026-03-05T14:00:00Z", "updated_at": "2026-11-02T10:15:00Z" }, { "id": "52da47ff-662a-4292-9961-27a95fc5ddd8", "object": "ai_agent", "name": "Vendas", "description": "Qualifica leads e apresenta produtos e promoções.", "active": true, "is_default": false, "tone": "descontraido", "strict_mode": true, "created_at": "2026-06-18T16:30:00Z", "updated_at": "2026-10-21T09:05:00Z" } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## Buscar agente de IA GET`/v1/ai-agents/{agent_id}`Testar Um agente pelo ID — por exemplo, para mostrar qual agente atende uma conversa a partir do `ai_agent_id` do contato. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ---------- | ------- | ---- | ----------- | ------------- | | `agent_id` | caminho | UUID | sim | ID do agente. | * curl ```bash curl https://platform.toptalking.com.br/v1/ai-agents/52da47ff-662a-4292-9961-27a95fc5ddd8 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const agentId = '52da47ff-662a-4292-9961-27a95fc5ddd8'; const res = await fetch(`https://platform.toptalking.com.br/v1/ai-agents/${agentId}`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const agente = await res.json(); ``` * Python ```python import os import requests agent_id = "52da47ff-662a-4292-9961-27a95fc5ddd8" res = requests.get( f"https://platform.toptalking.com.br/v1/ai-agents/{agent_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() agente = res.json() ``` #### Resposta `200` ```json { "id": "52da47ff-662a-4292-9961-27a95fc5ddd8", "object": "ai_agent", "name": "Vendas", "description": "Qualifica leads e apresenta produtos e promoções.", "active": true, "is_default": false, "tone": "descontraido", "strict_mode": true, "created_at": "2026-06-18T16:30:00Z", "updated_at": "2026-10-21T09:05:00Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------------------------------- | | 404 | `resource_not_found` | O agente não existe ou é de outra organização. | ## Ligar ou desligar agente de IA PATCH`/v1/ai-agents/{agent_id}`Testar Liga (`active: true`) ou desliga (`active: false`) um agente. Um agente desligado para de responder as conversas dele. Desligar o agente padrão desliga a IA da organização inteira O agente com `is_default: true` funciona como o interruptor geral da IA (o mesmo do painel). Com ele desligado, **nenhuma conversa é respondida pela IA**, nem as de outros agentes. Para tirar a IA de **uma** conversa só, use [`ai_enabled: false` no contato](#agente-e-ia-por-conversa). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ---------- | ------- | -------- | ----------- | ----------------------------- | | `agent_id` | caminho | UUID | sim | ID do agente. | | `active` | corpo | booleano | sim | `true` liga; `false` desliga. | * curl ```bash # Desligar o agente "Vendas" (o agente padrão continua respondendo) curl -X PATCH https://platform.toptalking.com.br/v1/ai-agents/52da47ff-662a-4292-9961-27a95fc5ddd8 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "active": false }' ``` * JavaScript ```js async function ligarAgente(agentId, active) { const res = await fetch(`https://platform.toptalking.com.br/v1/ai-agents/${agentId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ active }), }); const body = await res.json(); if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`); return body; } await ligarAgente('52da47ff-662a-4292-9961-27a95fc5ddd8', false); ``` * Python ```python import os import requests agent_id = "52da47ff-662a-4292-9961-27a95fc5ddd8" res = requests.patch( f"https://platform.toptalking.com.br/v1/ai-agents/{agent_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={"active": False}, timeout=30, ) res.raise_for_status() agente = res.json() ``` #### Resposta `200` O agente atualizado: ```json { "id": "52da47ff-662a-4292-9961-27a95fc5ddd8", "object": "ai_agent", "name": "Vendas", "description": "Qualifica leads e apresenta produtos e promoções.", "active": false, "is_default": false, "tone": "descontraido", "strict_mode": true, "created_at": "2026-06-18T16:30:00Z", "updated_at": "2026-11-11T16:45:12Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | ------------------------------- | ------------------------------------------------ | | 400 | `missing_field` | Faltou `active`. | | 400 | `invalid_json`, `invalid_field` | Corpo inválido ou `active` não é `true`/`false`. | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O agente não existe ou é de outra organização. | 400 missing_field ```json { "error": { "type": "invalid_request_error", "code": "missing_field", "message": "Informe `active` (true ou false).", "param": "active", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Agente e IA por conversa O agente e a IA de **cada conversa** são campos do contato. Use [`PATCH /contacts/{contact_id}`](/referencia/contatos/#atualizar-contato-e-mover-no-kanban): PATCH`/v1/contacts/{contact_id}`Testar | Corpo | Efeito | | ------------------------------------- | ------------------------------------------------------------ | | `{ "ai_agent_id": "" }` | Esta conversa passa a ser atendida por este agente. | | `{ "ai_agent_id": null }` | Volta para o agente padrão. | | `{ "ai_enabled": false }` | Desliga a IA **só nesta conversa** (para um humano assumir). | | `{ "ai_enabled": true }` | Religa a IA nesta conversa. | * curl ```bash # Passar a conversa para o agente "Vendas" curl -X PATCH https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ai_agent_id": "52da47ff-662a-4292-9961-27a95fc5ddd8" }' # Um humano vai assumir: desligar a IA e atribuir à Carla curl -X PATCH https://platform.toptalking.com.br/v1/contacts/3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ai_enabled": false, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b" }' ``` * JavaScript ```js const contactId = '3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60'; const res = await fetch(`https://platform.toptalking.com.br/v1/contacts/${contactId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, // Um humano vai assumir: desliga a IA e atribui à Carla body: JSON.stringify({ ai_enabled: false, assigned_to: '4083deb8-f96e-4093-8598-ebdd4dee786b' }), }); const contato = await res.json(); ``` * Python ```python import os import requests contact_id = "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60" res = requests.patch( f"https://platform.toptalking.com.br/v1/contacts/{contact_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={"ai_agent_id": "52da47ff-662a-4292-9961-27a95fc5ddd8"}, # None = agente padrão timeout=30, ) res.raise_for_status() contato = res.json() ``` A resposta `200` é o [contato atualizado](/referencia/contatos/#o-objeto-contato). Um `ai_agent_id` que não é agente da organização responde `400 invalid_field` (`param: "ai_agent_id"`). # Campanhas > Campanhas pelo WhatsApp Oficial (API da Meta) na API da TopTalking - listar templates prontos (GET /templates), simular o preço (POST /campaigns/quote), criar a campanha para aprovação (POST /campaigns), acompanhar, ver destinatários, pausar e retomar. **Campanhas** são disparos para muitos contatos de uma vez pelo **WhatsApp Oficial (API da Meta)**, usando **templates aprovados**. São o caminho certo para promoções, novidades e avisos em massa — ao contrário de [`POST /messages`](/referencia/mensagens/#enviar-mensagem), que usa o WhatsApp conectado por QR Code e serve para mensagens individuais. Toda campanha passa pela **revisão da equipe da TopTalking** antes de sair. Aprovada, ela começa a enviar sozinha. ## Antes de começar | Requisito | Onde resolver | | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | Um número do **WhatsApp Oficial** conectado. Ele é diferente dos [canais](/referencia/canais/) conectados por QR Code e não aparece em `GET /channels`. | No painel, em **Campanhas**. Sem ele, `POST /campaigns` responde `422 channel_unavailable`. | | Pelo menos um **template pronto** (`ready: true`): aprovado pela TopTalking e pela Meta. | No painel, em **Campanhas → Templates**. Pela API você só lista os templates. | | **Opt-in** dos destinatários: só envie para quem aceitou receber mensagens da empresa. | Na sua base. Veja [Boas práticas no WhatsApp](/comecando/boas-praticas-whatsapp/). | ## Ciclo de vida ```text POST /campaigns │ ▼ ┌──────────────────┐ a revisão pediu ajustes ┌───────────────────┐ │ pending_approval │ ──────────────────────► │ changes_requested │ └──────────────────┘ └───────────────────┘ │ aprovada pela TopTalking ▼ ┌──────────┐ em ~1 min ┌─────────┐ terminou ┌───────────┐ │ approved │ ────────► │ running │ ───────► │ completed │ └──────────┘ └─────────┘ └───────────┘ │ │ ▲ │ POST .../pause │ │ POST .../resume ▼ ▼ │ ┌──────────────────────────┐ │ │ paused │ ──┘ └──────────────────────────┘ failed: o envio não pôde continuar ``` | `status` | Significado | | ------------------- | ---------------------------------------------------------------------------------------------------------- | | `pending_approval` | Criada e aguardando a revisão da TopTalking. É assim que toda campanha criada pela API nasce. | | `changes_requested` | A revisão pediu ajustes: leia `review_notes`, corrija e crie uma nova campanha. | | `approved` | Aprovada. **Começa a enviar sozinha em até \~1 minuto.** | | `running` | Enviando. Acompanhe em `totals`. | | `paused` | Pausada por você (ou pelo painel). Retome com [`POST /campaigns/{campaign_id}/resume`](#retomar-campanha). | | `completed` | Todos os destinatários foram processados. | | `failed` | O envio não pôde continuar. | | `draft` | Rascunho criado no painel e ainda não enviado para revisão. | Para ser avisado de cada mudança, assine o webhook [`campaign.status_updated`](/webhooks/eventos/#campaignstatus_updated). ## Regras * **Template pronto:** `template.name` (e `template.language`, padrão `pt_BR`) precisa ser de um template com `ready: true`. Template inexistente → `404 resource_not_found`; existente mas não pronto → `400 invalid_field` (`param: "template"`). * **Variáveis na ordem:** `variables` de cada destinatário preenche `{{1}}`, `{{2}}`… do corpo do template, **na ordem**, e precisa ter exatamente `variables_count` itens. Números são convertidos em texto. * **Botão de link com variável:** se o template tem um botão com URL variável (ex.: `https://site.com/checkout/{{1}}`), o valor do link vai **no fim** de `variables`, depois das variáveis do corpo. Mande só a parte que entra no lugar de `{{1}}` (ex.: `86`). Ele já está contado em `variables_count`. * **Até 5.000 destinatários por requisição** (corpo de até 1 MB). Para listas maiores, crie várias campanhas. * **Telefones repetidos** na mesma requisição recebem a mensagem uma vez só. * **`skip_already_sent: true`** pula quem já recebeu alguma campanha da organização. Se isso deixar a lista vazia, a resposta é `400 invalid_field` (`param: "recipients"`). * **Preço congelado:** o preço por mensagem (`pricing.unit_price`) é o da tabela da sua organização **no momento da criação** e não muda depois. `pricing.estimated_total` = preço × destinatários; `pricing.charged_total` = preço × mensagens já enviadas. Simule antes com [`POST /campaigns/quote`](#simular-pre%C3%A7o). * **Pausar e retomar:** só dá para pausar campanhas `approved` ou `running`, e retomar campanhas `paused`. Fora disso → `409 invalid_state`. ## Passo a passo 1. **Escolha um template pronto** com [`GET /templates?ready=true`](#listar-templates). Anote `name`, `language` e `variables_count` (quantos valores cada destinatário precisa). ```bash curl "https://platform.toptalking.com.br/v1/templates?ready=true" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` 2. **Simule o preço** com [`POST /campaigns/quote`](#simular-pre%C3%A7o), informando o template e quantos destinatários você terá. Nada é criado. ```bash curl https://platform.toptalking.com.br/v1/campaigns/quote \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template": { "name": "promo_black_friday", "language": "pt_BR" }, "recipients_count": 1500 }' ``` 3. **Crie a campanha** com [`POST /campaigns`](#criar-campanha). Envie `Idempotency-Key` para uma retentativa não criar outra campanha igual. A resposta `201` vem com `status: "pending_approval"`. ```bash curl https://platform.toptalking.com.br/v1/campaigns \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: black-friday-2026-lote-1" \ -d '{ "name": "Black Friday 2026", "template": { "name": "promo_black_friday", "language": "pt_BR" }, "recipients": [ { "phone": "5511999998888", "name": "Maria Souza", "variables": ["Maria", "30%"] }, { "phone": "5521987654321", "name": "João Pereira", "variables": ["João", "30%"] }, { "phone": "5531991234567", "name": "Ana Lima", "variables": ["Ana", "30%"] } ], "skip_already_sent": false }' ``` 4. **Acompanhe o status.** Assine o webhook [`campaign.status_updated`](/webhooks/eventos/#campaignstatus_updated) ou consulte [`GET /campaigns/{campaign_id}`](#buscar-campanha) de tempos em tempos (a cada 1–5 minutos é suficiente). Se vier `changes_requested`, leia `review_notes`. 5. **Veja quem recebeu** com [`GET /campaigns/{campaign_id}/recipients`](#destinat%C3%A1rios-da-campanha) — filtre por `status=failed` para ver as falhas e o motivo em `error`. ### Exemplo completo * JavaScript campanha.mjs ```js // Node 18+. Rode no seu servidor: node campanha.mjs const API = 'https://platform.toptalking.com.br/v1'; const headers = { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }; async function api(path, init = {}) { const res = await fetch(`${API}${path}`, { ...init, headers: { ...headers, ...init.headers } }); const body = await res.json(); if (!res.ok) throw new Error(`${res.status} ${body.error.code} (${body.error.param ?? '-'}): ${body.error.message}`); return body; } const clientes = [ { phone: '5511999998888', nome: 'Maria Souza' }, { phone: '5521987654321', nome: 'João Pereira' }, { phone: '5531991234567', nome: 'Ana Lima' }, ]; // 1. Template pronto const { data: templates } = await api('/templates?ready=true'); const template = templates.find((t) => t.name === 'promo_black_friday'); if (!template) throw new Error('Template não está pronto: confira no painel, em Campanhas → Templates.'); // 2. Preço const orcamento = await api('/campaigns/quote', { method: 'POST', body: JSON.stringify({ template: { name: template.name, language: template.language }, recipients_count: clientes.length, }), }); console.log(`Custo estimado: ${orcamento.currency} ${orcamento.total}`); // 3. Criar (as variables têm exatamente template.variables_count itens) const campanha = await api('/campaigns', { method: 'POST', headers: { 'Idempotency-Key': 'black-friday-2026-lote-1' }, body: JSON.stringify({ name: 'Black Friday 2026', template: { name: template.name, language: template.language }, recipients: clientes.map((c) => ({ phone: c.phone, name: c.nome, variables: [c.nome.split(' ')[0], '30%'], })), skip_already_sent: false, }), }); console.log('Campanha criada:', campanha.id, campanha.status); // pending_approval // 4. Acompanhar (ou use o webhook campaign.status_updated) const FINAIS = ['completed', 'failed', 'changes_requested']; let atual = campanha; while (!FINAIS.includes(atual.status)) { await new Promise((r) => setTimeout(r, 60_000)); // 1 minuto atual = await api(`/campaigns/${campanha.id}`); console.log(atual.status, atual.totals); } if (atual.status === 'changes_requested') console.log('Ajustes pedidos:', atual.review_notes); // 5. Falhas const { data: falhas } = await api(`/campaigns/${campanha.id}/recipients?status=failed&limit=100`); for (const f of falhas) console.log(f.phone, f.error); ``` * Python campanha.py ```python import os import time import requests API = "https://platform.toptalking.com.br/v1" HEADERS = {"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"} def api(method, path, **kwargs): res = requests.request(method, f"{API}{path}", headers={**HEADERS, **kwargs.pop("headers", {})}, timeout=60, **kwargs) body = res.json() if not res.ok: e = body["error"] raise RuntimeError(f"{res.status_code} {e['code']} ({e.get('param')}): {e['message']}") return body clientes = [ {"phone": "5511999998888", "nome": "Maria Souza"}, {"phone": "5521987654321", "nome": "João Pereira"}, {"phone": "5531991234567", "nome": "Ana Lima"}, ] # 1. Template pronto templates = api("GET", "/templates", params={"ready": "true"})["data"] template = next(t for t in templates if t["name"] == "promo_black_friday") ref = {"name": template["name"], "language": template["language"]} # 2. Preço orcamento = api("POST", "/campaigns/quote", json={"template": ref, "recipients_count": len(clientes)}) print("Custo estimado:", orcamento["currency"], orcamento["total"]) # 3. Criar campanha = api( "POST", "/campaigns", headers={"Idempotency-Key": "black-friday-2026-lote-1"}, json={ "name": "Black Friday 2026", "template": ref, "recipients": [ {"phone": c["phone"], "name": c["nome"], "variables": [c["nome"].split()[0], "30%"]} for c in clientes ], "skip_already_sent": False, }, ) print("Campanha criada:", campanha["id"], campanha["status"]) # 4. Acompanhar (ou use o webhook campaign.status_updated) while campanha["status"] not in ("completed", "failed", "changes_requested"): time.sleep(60) campanha = api("GET", f"/campaigns/{campanha['id']}") print(campanha["status"], campanha["totals"]) # 5. Falhas falhas = api("GET", f"/campaigns/{campanha['id']}/recipients", params={"status": "failed", "limit": 100}) for f in falhas["data"]: print(f["phone"], f["error"]) ``` ## O objeto template ```json { "id": "875269ea-ba67-436d-81b4-0d209df54520", "object": "template", "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING", "status": "APPROVED", "review_status": "approved", "ready": true, "body": "Oi {{1}}! A Black Friday da Loja Aurora chegou: {{2}} de desconto em todo o site até domingo. Responda SAIR para não receber mais.", "variables_count": 2, "header_type": "IMAGE", "rejected_reason": null, "created_at": "2026-10-20T14:00:00Z" } ``` | Campo | Tipo | Descrição | | ----------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- | | `id` | UUID | ID do template. | | `object` | string | Sempre `template`. | | `name` | string | Nome do template. É o que você envia em `template.name`. | | `language` | string | Idioma (ex.: `pt_BR`). É o que você envia em `template.language`. | | `category` | string ou `null` | Categoria na Meta: `MARKETING`, `UTILITY` ou `AUTHENTICATION`. O preço por mensagem depende dela. | | `status` | string ou `null` | Status na Meta: `APPROVED`, `PENDING`, `REJECTED`, `PAUSED` ou `DISABLED`. | | `review_status` | string | Revisão interna da TopTalking: `approved`, `pending` ou `rejected`. | | `ready` | booleano | **`true` = pode ser usado em campanhas** (aprovado pela TopTalking e pela Meta). | | `body` | string ou `null` | Texto do corpo, com as variáveis `{{1}}`, `{{2}}`… | | `variables_count` | inteiro | Quantos itens cada destinatário precisa em `variables`: as variáveis do corpo mais 1 por botão de link com variável. | | `header_type` | string ou `null` | Tipo do cabeçalho: `TEXT`, `IMAGE`, `VIDEO`, `DOCUMENT` ou `null` (sem cabeçalho). | | `rejected_reason` | string ou `null` | Motivo da rejeição, quando houver. | | `created_at` | data | Criação. | ## Listar templates GET`/v1/templates`Testar Os templates de mensagem do WhatsApp Oficial da organização. Só os com `ready: true` podem ser usados em campanhas. Os templates são **criados e enviados para aprovação no painel**, em Campanhas → Templates. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | --------- | ----- | -------- | ----------- | ------------------------------------------------ | | `ready` | query | booleano | não | `true` devolve só os templates prontos para uso. | A lista vem inteira numa página (`has_more: false`). * curl ```bash curl https://platform.toptalking.com.br/v1/templates \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/templates?ready=true', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: prontos } = await res.json(); for (const t of prontos) console.log(t.name, t.language, `${t.variables_count} variáveis`); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/templates", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"ready": "true"}, timeout=30, ) res.raise_for_status() prontos = res.json()["data"] ``` #### Resposta `200` Sem filtro, vêm também os templates que ainda não estão prontos: ```json { "object": "list", "data": [ { "id": "875269ea-ba67-436d-81b4-0d209df54520", "object": "template", "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING", "status": "APPROVED", "review_status": "approved", "ready": true, "body": "Oi {{1}}! A Black Friday da Loja Aurora chegou: {{2}} de desconto em todo o site até domingo. Responda SAIR para não receber mais.", "variables_count": 2, "header_type": "IMAGE", "rejected_reason": null, "created_at": "2026-10-20T14:00:00Z" }, { "id": "3acdaef8-6224-43e8-aa29-9de336a75b7b", "object": "template", "name": "lembrete_pagamento", "language": "pt_BR", "category": "UTILITY", "status": "PENDING", "review_status": "approved", "ready": false, "body": "Olá {{1}}, o boleto do pedido {{2}} vence em {{3}}. Precisa de ajuda? É só responder esta mensagem.", "variables_count": 3, "header_type": null, "rejected_reason": null, "created_at": "2026-11-10T19:12:00Z" } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## O objeto campanha ```json { "id": "f2143187-f4f8-43b0-9c03-57a26c575f41", "object": "campaign", "name": "Black Friday 2026", "status": "running", "review_notes": null, "template": { "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING" }, "totals": { "recipients": 3, "pending": 1, "sent": 2, "delivered": 2, "read": 1, "failed": 0 }, "pricing": { "unit_price": 0.35, "currency": "BRL", "estimated_total": 1.05, "charged_total": 0.7 }, "created_at": "2026-11-11T16:00:00Z", "submitted_at": "2026-11-11T16:00:00Z", "reviewed_at": "2026-11-11T16:20:00Z", "started_at": "2026-11-11T16:20:41Z", "completed_at": null } ``` | Campo | Tipo | Descrição | | ------------------------- | ---------------- | -------------------------------------------------------------------------------- | | `id` | UUID | ID da campanha. | | `object` | string | Sempre `campaign`. | | `name` | string | Nome da campanha. | | `status` | string | Veja o [ciclo de vida](#ciclo-de-vida). | | `review_notes` | string ou `null` | Observação da revisão da TopTalking (ex.: o que ajustar em `changes_requested`). | | `template` | objeto | `name`, `language` e `category` do template usado. | | `totals.recipients` | inteiro | Total de destinatários (= `pending` + `sent` + `failed`). | | `totals.pending` | inteiro | Ainda não enviados. | | `totals.sent` | inteiro | Enviados (**inclui** os entregues e os lidos). | | `totals.delivered` | inteiro | Entregues no celular (**inclui** os lidos). | | `totals.read` | inteiro | Lidos. | | `totals.failed` | inteiro | Falharam. | | `pricing.unit_price` | número | Preço por mensagem, **congelado na criação**. | | `pricing.currency` | string | Moeda (`BRL`). | | `pricing.estimated_total` | número | `unit_price` × destinatários. | | `pricing.charged_total` | número | `unit_price` × mensagens já enviadas (arredondado em 2 casas). | | `created_at` | data | Criação. | | `submitted_at` | data ou `null` | Quando foi enviada para revisão. | | `reviewed_at` | data ou `null` | Quando a TopTalking revisou. | | `started_at` | data ou `null` | Quando o envio começou. | | `completed_at` | data ou `null` | Quando terminou. | ## Simular preço POST`/v1/campaigns/quote`Testar Calcula quanto uma campanha vai custar, pela tabela de preço da sua organização, **sem criar nada**. Use antes de criar, para mostrar o valor a quem vai aprovar o disparo. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------------- | ----- | ------- | ----------- | ------------------------------------ | | `template.name` | corpo | string | sim | Nome do template. | | `template.language` | corpo | string | não | Idioma do template. Padrão: `pt_BR`. | | `recipients_count` | corpo | inteiro | sim | Quantos destinatários (1 a 100.000). | * curl ```bash curl https://platform.toptalking.com.br/v1/campaigns/quote \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template": { "name": "promo_black_friday", "language": "pt_BR" }, "recipients_count": 1500 }' ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/campaigns/quote', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ template: { name: 'promo_black_friday', language: 'pt_BR' }, recipients_count: 1500, }), }); const orcamento = await res.json(); console.log(`${orcamento.recipients} mensagens × R$ ${orcamento.unit_price} = R$ ${orcamento.total}`); ``` * Python ```python import os import requests res = requests.post( "https://platform.toptalking.com.br/v1/campaigns/quote", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={ "template": {"name": "promo_black_friday", "language": "pt_BR"}, "recipients_count": 1500, }, timeout=30, ) res.raise_for_status() orcamento = res.json() ``` #### Resposta `200` ```json { "object": "campaign_quote", "category": "MARKETING", "recipients": 1500, "unit_price": 0.35, "currency": "BRL", "total": 525 } ``` | Campo | Tipo | Descrição | | ------------ | ---------------- | --------------------------------------- | | `object` | string | Sempre `campaign_quote`. | | `category` | string ou `null` | Categoria do template (define o preço). | | `recipients` | inteiro | Quantidade simulada. | | `unit_price` | número | Preço por mensagem hoje. | | `currency` | string | Moeda (`BRL`). | | `total` | número | `unit_price` × `recipients`. | Esta é a única rota `POST` liberada para chaves **somente leitura**: ela só calcula, não cria nada. #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------------------- | --------------------------------------------------------------------------------- | | 400 | `missing_field`, `invalid_field` | Faltou `template` ou `recipients_count`, ou `recipients_count` fora de 1–100.000. | | 404 | `resource_not_found` | Não existe template com esse `name` e `language`. | ## Criar campanha POST`/v1/campaigns`Testar Cria um disparo pelo WhatsApp Oficial com um template pronto e **envia para a revisão da TopTalking**. A campanha nasce `pending_approval`; ao ser aprovada, o envio começa sozinho (`approved` → `running` → `completed`). O preço por mensagem fica congelado neste momento. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------------------ | --------- | ---------------- | ----------------------------- | -------------------------------------------------------------------------------------- | | `name` | corpo | string | sim | Nome da campanha (até 120 caracteres). | | `template.name` | corpo | string | sim | Nome de um template com `ready: true`. | | `template.language` | corpo | string | não | Idioma do template. Padrão: `pt_BR`. | | `recipients` | corpo | lista | sim | De 1 a 5.000 destinatários. | | `recipients[].phone` | corpo | string | sim | Telefone com DDI e DDD (ex.: `5511999998888`). | | `recipients[].name` | corpo | string | não | Nome do destinatário. | | `recipients[].variables` | corpo | lista de strings | se o template tiver variáveis | Valores de `{{1}}`, `{{2}}`… na ordem. Precisa ter exatamente `variables_count` itens. | | `skip_already_sent` | corpo | booleano | não | `true` pula quem já recebeu alguma campanha desta organização. Padrão: `false`. | | `Idempotency-Key` | cabeçalho | string | não (recomendado) | Evita criar a mesma campanha duas vezes numa retentativa. | * curl ```bash curl https://platform.toptalking.com.br/v1/campaigns \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: black-friday-2026-lote-1" \ -d '{ "name": "Black Friday 2026", "template": { "name": "promo_black_friday", "language": "pt_BR" }, "recipients": [ { "phone": "5511999998888", "name": "Maria Souza", "variables": ["Maria", "30%"] }, { "phone": "5521987654321", "name": "João Pereira", "variables": ["João", "30%"] }, { "phone": "5531991234567", "name": "Ana Lima", "variables": ["Ana", "30%"] } ], "skip_already_sent": false }' ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/campaigns', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'black-friday-2026-lote-1', }, body: JSON.stringify({ name: 'Black Friday 2026', template: { name: 'promo_black_friday', language: 'pt_BR' }, recipients: [ { phone: '5511999998888', name: 'Maria Souza', variables: ['Maria', '30%'] }, { phone: '5521987654321', name: 'João Pereira', variables: ['João', '30%'] }, { phone: '5531991234567', name: 'Ana Lima', variables: ['Ana', '30%'] }, ], skip_already_sent: false, }), }); const campanha = await res.json(); if (!res.ok) throw new Error(`${campanha.error.code} (${campanha.error.param}): ${campanha.error.message}`); console.log(campanha.id, campanha.status); // "pending_approval" ``` * Python ```python import os import requests res = requests.post( "https://platform.toptalking.com.br/v1/campaigns", headers={ "Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}", "Idempotency-Key": "black-friday-2026-lote-1", }, json={ "name": "Black Friday 2026", "template": {"name": "promo_black_friday", "language": "pt_BR"}, "recipients": [ {"phone": "5511999998888", "name": "Maria Souza", "variables": ["Maria", "30%"]}, {"phone": "5521987654321", "name": "João Pereira", "variables": ["João", "30%"]}, {"phone": "5531991234567", "name": "Ana Lima", "variables": ["Ana", "30%"]}, ], "skip_already_sent": False, }, timeout=60, ) campanha = res.json() if not res.ok: erro = campanha["error"] raise RuntimeError(f"{erro['code']} ({erro.get('param')}): {erro['message']}") ``` #### Resposta `201` ```json { "id": "f2143187-f4f8-43b0-9c03-57a26c575f41", "object": "campaign", "name": "Black Friday 2026", "status": "pending_approval", "review_notes": null, "template": { "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING" }, "totals": { "recipients": 3, "pending": 3, "sent": 0, "delivered": 0, "read": 0, "failed": 0 }, "pricing": { "unit_price": 0.35, "currency": "BRL", "estimated_total": 1.05, "charged_total": 0 }, "created_at": "2026-11-11T16:00:00Z", "submitted_at": "2026-11-11T16:00:00Z", "reviewed_at": null, "started_at": null, "completed_at": null } ``` #### Erros | HTTP | `code` | `param` | Quando acontece | | ---- | ---------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | | 400 | `missing_field` | `name`, `template` ou `recipients` | Faltou um campo obrigatório. | | 400 | `invalid_field` | `template` | O template existe, mas não está pronto (`ready: false`). | | 400 | `invalid_field` | `recipients[3].variables` | A quantidade de `variables` desse destinatário é diferente de `variables_count`. | | 400 | `invalid_field` | `recipients` | Mais de 5.000 destinatários, lista vazia ou, com `skip_already_sent: true`, todos já receberam alguma campanha. | | 400 | `invalid_phone` | `recipients[n].phone` | Telefone inválido em um destinatário. | | 403 | `read_only_key` | — | Chave somente leitura. | | 404 | `resource_not_found` | `template` | Não existe template com esse `name` e `language`. | | 409 | `idempotency_conflict` | — | `Idempotency-Key` reutilizada com outro corpo. | | 422 | `channel_unavailable` | — | A organização não tem número do WhatsApp Oficial conectado. | 400 invalid_field (variáveis) ```json { "error": { "type": "invalid_request_error", "code": "invalid_field", "message": "O template \"promo_black_friday\" tem 2 variáveis; `recipients[3].variables` trouxe 1.", "param": "recipients[3].variables", "request_id": "req_6f1d2c3b4a5e" } } ``` 422 channel_unavailable ```json { "error": { "type": "channel_error", "code": "channel_unavailable", "message": "Nenhum número do WhatsApp Oficial conectado. Conecte no painel em Campanhas.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Listar campanhas GET`/v1/campaigns`Testar As campanhas da organização, **das mais recentes para as mais antigas**, com [paginação](/comecando/paginacao/). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | --------- | ----- | ------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `status` | query | string | não | Só campanhas neste status (`draft`, `pending_approval`, `approved`, `changes_requested`, `running`, `paused`, `completed` ou `failed`). | | `limit` | query | inteiro | não | Itens por página, 1 a 100 (padrão 20). | | `cursor` | query | string | não | `next_cursor` da página anterior. | * curl ```bash curl "https://platform.toptalking.com.br/v1/campaigns?limit=10" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/campaigns?status=running', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: enviando } = await res.json(); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/campaigns", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"limit": 10}, timeout=30, ) res.raise_for_status() campanhas = res.json()["data"] ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "f2143187-f4f8-43b0-9c03-57a26c575f41", "object": "campaign", "name": "Black Friday 2026", "status": "running", "review_notes": null, "template": { "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING" }, "totals": { "recipients": 3, "pending": 1, "sent": 2, "delivered": 2, "read": 1, "failed": 0 }, "pricing": { "unit_price": 0.35, "currency": "BRL", "estimated_total": 1.05, "charged_total": 0.7 }, "created_at": "2026-11-11T16:00:00Z", "submitted_at": "2026-11-11T16:00:00Z", "reviewed_at": "2026-11-11T16:20:00Z", "started_at": "2026-11-11T16:20:41Z", "completed_at": null }, { "id": "4815f77a-bd81-4e28-bded-100b6a109cad", "object": "campaign", "name": "Aviso de feriado - Finados", "status": "completed", "review_notes": null, "template": { "name": "aviso_horario_feriado", "language": "pt_BR", "category": "UTILITY" }, "totals": { "recipients": 1500, "pending": 0, "sent": 1480, "delivered": 1420, "read": 900, "failed": 20 }, "pricing": { "unit_price": 0.08, "currency": "BRL", "estimated_total": 120, "charged_total": 118.4 }, "created_at": "2026-10-30T13:10:00Z", "submitted_at": "2026-10-30T13:10:00Z", "reviewed_at": "2026-10-30T14:02:00Z", "started_at": "2026-10-30T14:02:55Z", "completed_at": "2026-10-30T14:41:18Z" } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## Buscar campanha GET`/v1/campaigns/{campaign_id}`Testar Uma campanha pelo ID, com status, contadores e preço atualizados. Use para acompanhar o envio quando não estiver usando o webhook [`campaign.status_updated`](/webhooks/eventos/#campaignstatus_updated). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------- | ------- | ---- | ----------- | --------------- | | `campaign_id` | caminho | UUID | sim | ID da campanha. | * curl ```bash curl https://platform.toptalking.com.br/v1/campaigns/f2143187-f4f8-43b0-9c03-57a26c575f41 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const campaignId = 'f2143187-f4f8-43b0-9c03-57a26c575f41'; const res = await fetch(`https://platform.toptalking.com.br/v1/campaigns/${campaignId}`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const campanha = await res.json(); const { recipients, pending } = campanha.totals; console.log(`${campanha.status}: ${recipients - pending}/${recipients} processados`); ``` * Python ```python import os import requests campaign_id = "f2143187-f4f8-43b0-9c03-57a26c575f41" res = requests.get( f"https://platform.toptalking.com.br/v1/campaigns/{campaign_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() campanha = res.json() ``` #### Resposta `200` ```json { "id": "f2143187-f4f8-43b0-9c03-57a26c575f41", "object": "campaign", "name": "Black Friday 2026", "status": "completed", "review_notes": null, "template": { "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING" }, "totals": { "recipients": 3, "pending": 0, "sent": 3, "delivered": 2, "read": 1, "failed": 0 }, "pricing": { "unit_price": 0.35, "currency": "BRL", "estimated_total": 1.05, "charged_total": 1.05 }, "created_at": "2026-11-11T16:00:00Z", "submitted_at": "2026-11-11T16:00:00Z", "reviewed_at": "2026-11-11T16:20:00Z", "started_at": "2026-11-11T16:20:41Z", "completed_at": "2026-11-11T16:21:30Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ------------------------------------------------ | | 404 | `resource_not_found` | A campanha não existe ou é de outra organização. | ## Destinatários da campanha GET`/v1/campaigns/{campaign_id}/recipients`Testar O status de envio de cada destinatário, **na ordem em que foram enviados à API**, com [paginação](/comecando/paginacao/). Filtre por `status=failed` para ver quem não recebeu e o motivo. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------- | ------- | ------- | ----------- | -------------------------------------------------------------- | | `campaign_id` | caminho | UUID | sim | ID da campanha. | | `status` | query | string | não | `pending`, `sending`, `sent`, `delivered`, `read` ou `failed`. | | `limit` | query | inteiro | não | Itens por página, 1 a 100 (padrão 20). | | `cursor` | query | string | não | `next_cursor` da página anterior. | * curl ```bash curl "https://platform.toptalking.com.br/v1/campaigns/f2143187-f4f8-43b0-9c03-57a26c575f41/recipients?limit=100" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js // Percorre todas as páginas de destinatários que falharam const API = 'https://platform.toptalking.com.br/v1'; const headers = { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }; const campaignId = 'f2143187-f4f8-43b0-9c03-57a26c575f41'; const falhas = []; let cursor = null; do { const url = new URL(`${API}/campaigns/${campaignId}/recipients`); url.searchParams.set('status', 'failed'); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('cursor', cursor); const page = await (await fetch(url, { headers })).json(); falhas.push(...page.data); cursor = page.has_more ? page.next_cursor : null; } while (cursor); ``` * Python ```python import os import requests campaign_id = "f2143187-f4f8-43b0-9c03-57a26c575f41" res = requests.get( f"https://platform.toptalking.com.br/v1/campaigns/{campaign_id}/recipients", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"limit": 100}, timeout=30, ) res.raise_for_status() for d in res.json()["data"]: print(d["phone"], d["status"], d["error"] or "") ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "434c30a3-7853-4b6f-9b21-e02da711774a", "object": "campaign_recipient", "phone": "5511999998888", "name": "Maria Souza", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "variables": ["Maria", "30%"], "status": "read", "error": null, "sent_at": "2026-11-11T16:20:43Z", "delivered_at": "2026-11-11T16:20:45Z", "read_at": "2026-11-11T16:24:10Z" }, { "id": "5c80c4da-1858-41ca-8969-746ea12fdc01", "object": "campaign_recipient", "phone": "5521987654321", "name": "João Pereira", "contact_id": "721bfe94-e549-4354-b90c-a8d679e9b271", "variables": ["João", "30%"], "status": "delivered", "error": null, "sent_at": "2026-11-11T16:20:44Z", "delivered_at": "2026-11-11T16:20:47Z", "read_at": null }, { "id": "a1138591-7dd7-45cc-8a72-9f492f087c77", "object": "campaign_recipient", "phone": "5531991234567", "name": "Ana Lima", "contact_id": "6586daec-7197-478e-b0ff-5b6659c11d26", "variables": ["Ana", "30%"], "status": "sent", "error": null, "sent_at": "2026-11-11T16:21:29Z", "delivered_at": null, "read_at": null } ], "has_more": false, "next_cursor": null } ``` | Campo | Tipo | Descrição | | -------------- | ---------------- | -------------------------------------------------------------- | | `id` | UUID | ID do destinatário na campanha. | | `object` | string | Sempre `campaign_recipient`. | | `phone` | string | Telefone, só dígitos com DDI. | | `name` | string ou `null` | Nome enviado na criação. | | `contact_id` | UUID ou `null` | Contato (conversa) correspondente no painel, quando houver. | | `variables` | lista de strings | Valores usados nas variáveis do template. | | `status` | string | `pending`, `sending`, `sent`, `delivered`, `read` ou `failed`. | | `error` | string ou `null` | Motivo da falha, quando `status` é `failed`. | | `sent_at` | data ou `null` | Quando foi enviado. | | `delivered_at` | data ou `null` | Quando chegou no celular. | | `read_at` | data ou `null` | Quando foi lido. | #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ------------------------------------------------ | | 404 | `resource_not_found` | A campanha não existe ou é de outra organização. | ## Pausar campanha POST`/v1/campaigns/{campaign_id}/pause`Testar Interrompe o envio de uma campanha `approved` ou `running`. Quem já recebeu não é afetado; os pendentes esperam até você [retomar](#retomar-campanha). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------- | ------- | ---- | ----------- | --------------- | | `campaign_id` | caminho | UUID | sim | ID da campanha. | * curl ```bash curl -X POST https://platform.toptalking.com.br/v1/campaigns/f2143187-f4f8-43b0-9c03-57a26c575f41/pause \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const campaignId = 'f2143187-f4f8-43b0-9c03-57a26c575f41'; const res = await fetch(`https://platform.toptalking.com.br/v1/campaigns/${campaignId}/pause`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const campanha = await res.json(); if (res.status === 409) console.log('Não dá para pausar agora:', campanha.error.message); ``` * Python ```python import os import requests campaign_id = "f2143187-f4f8-43b0-9c03-57a26c575f41" res = requests.post( f"https://platform.toptalking.com.br/v1/campaigns/{campaign_id}/pause", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) campanha = res.json() ``` #### Resposta `200` ```json { "id": "f2143187-f4f8-43b0-9c03-57a26c575f41", "object": "campaign", "name": "Black Friday 2026", "status": "paused", "review_notes": null, "template": { "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING" }, "totals": { "recipients": 3, "pending": 1, "sent": 2, "delivered": 2, "read": 1, "failed": 0 }, "pricing": { "unit_price": 0.35, "currency": "BRL", "estimated_total": 1.05, "charged_total": 0.7 }, "created_at": "2026-11-11T16:00:00Z", "submitted_at": "2026-11-11T16:00:00Z", "reviewed_at": "2026-11-11T16:20:00Z", "started_at": "2026-11-11T16:20:41Z", "completed_at": null } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------------------------------------------------------------------------------- | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | A campanha não existe ou é de outra organização. | | 409 | `invalid_state` | A campanha não está `approved` nem `running` (ex.: ainda em revisão, já pausada ou concluída). | 409 invalid_state ```json { "error": { "type": "conflict_error", "code": "invalid_state", "message": "Só é possível pausar campanhas aprovadas ou em envio (status atual: pending_approval).", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Retomar campanha POST`/v1/campaigns/{campaign_id}/resume`Testar Volta a enviar uma campanha `paused`, **continuando de onde parou** (só os pendentes recebem). | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------- | ------- | ---- | ----------- | --------------- | | `campaign_id` | caminho | UUID | sim | ID da campanha. | * curl ```bash curl -X POST https://platform.toptalking.com.br/v1/campaigns/f2143187-f4f8-43b0-9c03-57a26c575f41/resume \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const campaignId = 'f2143187-f4f8-43b0-9c03-57a26c575f41'; const res = await fetch(`https://platform.toptalking.com.br/v1/campaigns/${campaignId}/resume`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const campanha = await res.json(); // campanha.status === "running" ``` * Python ```python import os import requests campaign_id = "f2143187-f4f8-43b0-9c03-57a26c575f41" res = requests.post( f"https://platform.toptalking.com.br/v1/campaigns/{campaign_id}/resume", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) campanha = res.json() ``` #### Resposta `200` A campanha, de volta ao envio: ```json { "id": "f2143187-f4f8-43b0-9c03-57a26c575f41", "object": "campaign", "name": "Black Friday 2026", "status": "running", "review_notes": null, "template": { "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING" }, "totals": { "recipients": 3, "pending": 1, "sent": 2, "delivered": 2, "read": 1, "failed": 0 }, "pricing": { "unit_price": 0.35, "currency": "BRL", "estimated_total": 1.05, "charged_total": 0.7 }, "created_at": "2026-11-11T16:00:00Z", "submitted_at": "2026-11-11T16:00:00Z", "reviewed_at": "2026-11-11T16:20:00Z", "started_at": "2026-11-11T16:20:41Z", "completed_at": null } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ------------------------------------------------ | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | A campanha não existe ou é de outra organização. | | 409 | `invalid_state` | A campanha não está `paused`. | # Webhooks (gestão) > Gerencie webhooks pela API da TopTalking - listar, criar (com o secret de assinatura), buscar, atualizar, reativar, excluir, enviar evento de teste (ping) e ver o histórico de entregas. Estes endpoints gerenciam os **endereços** que recebem eventos. Para entender os eventos em si, como validar a assinatura e como funcionam as retentativas, veja a seção [Webhooks](/webhooks/visao-geral/). Você também pode fazer tudo isso no painel, em [Configurações → Desenvolvedor → Webhooks](https://toptalking.com.br/configuracoes/desenvolvedor). ## O objeto webhook ```json { "id": "f03b0773-5912-4a45-9139-c29375485843", "object": "webhook_endpoint", "url": "https://meusistema.com.br/webhooks/toptalking", "description": "CRM interno", "events": ["message.received", "contact.stage_changed"], "enabled": true, "disabled_reason": null, "last_delivery_at": "2026-11-11T14:12:41Z", "last_delivery_status": 200, "created_at": "2026-11-01T10:00:00Z", "updated_at": "2026-11-01T10:00:00Z" } ``` | Campo | Tipo | Descrição | | ---------------------- | ----------------- | ----------------------------------------------------------------------- | | `id` | UUID | ID do webhook. | | `object` | string | Sempre `webhook_endpoint`. | | `url` | string | Endereço HTTPS que recebe os eventos. | | `description` | string ou `null` | Descrição livre. | | `events` | lista | Eventos assinados, ou `["*"]` para todos. | | `enabled` | booleano | Se está recebendo eventos. | | `disabled_reason` | string ou `null` | Preenchido quando a TopTalking desativou o webhook por falhas seguidas. | | `last_delivery_at` | data ou `null` | Última tentativa de entrega. | | `last_delivery_status` | inteiro ou `null` | Status HTTP que o seu sistema respondeu na última entrega. | | `created_at` | data | Criação. | | `updated_at` | data | Última alteração. | ### Eventos disponíveis Valores aceitos em `events`: | Valor | Quando chega | | -------------------------- | ---------------------------------------------------------------------------------------- | | `*` | Todos os eventos. | | `message.received` | Um contato mandou mensagem. | | `message.sent` | Uma mensagem saiu pela conta. | | `message.status_updated` | O status de entrega de uma mensagem enviada mudou. | | `contact.created` | Um contato novo entrou na base. | | `contact.updated` | Mudou nome, e-mail, etapa, responsável, IA, arquivamento ou finalização. | | `contact.stage_changed` | O card mudou de etapa no Kanban. | | `scheduled_message.sent` | Uma mensagem agendada foi enviada. | | `scheduled_message.failed` | Uma mensagem agendada falhou. | | `channel.status_updated` | Um WhatsApp conectou, caiu ou foi desconectado. | | `campaign.status_updated` | Uma campanha mudou de status (aprovada, ajustes pedidos, enviando, pausada, concluída…). | O evento de teste `ping` não precisa ser assinado: ele sempre é entregue quando você pede um teste. Os payloads de cada evento estão em [Eventos e payloads](/webhooks/eventos/). ## Listar webhooks GET`/v1/webhooks`Testar Todos os webhooks da organização (no máximo 10). Use para conferir se o seu endereço já está cadastrado antes de criar outro. Não tem parâmetros. A lista vem inteira numa página (`has_more: false`). * curl ```bash curl https://platform.toptalking.com.br/v1/webhooks \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/webhooks', { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { data: webhooks } = await res.json(); const desativados = webhooks.filter((w) => !w.enabled); ``` * Python ```python import os import requests res = requests.get( "https://platform.toptalking.com.br/v1/webhooks", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() webhooks = res.json()["data"] ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "f03b0773-5912-4a45-9139-c29375485843", "object": "webhook_endpoint", "url": "https://meusistema.com.br/webhooks/toptalking", "description": "CRM interno", "events": ["message.received", "contact.stage_changed"], "enabled": true, "disabled_reason": null, "last_delivery_at": "2026-11-11T14:12:41Z", "last_delivery_status": 200, "created_at": "2026-11-01T10:00:00Z", "updated_at": "2026-11-01T10:00:00Z" } ], "has_more": false, "next_cursor": null } ``` #### Erros Só os gerais: `401` (chave) e `429` (limite por minuto). Veja [Erros](/comecando/erros/). ## Criar webhook POST`/v1/webhooks`Testar Cadastra um endereço do seu sistema para receber eventos. A resposta traz o **`secret`** (só desta vez), usado para [validar a assinatura](/webhooks/assinatura/). Máximo de **10 webhooks por organização**. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ----------------- | --------- | -------- | ----------- | ------------------------------------------------------------------------ | | `url` | corpo | string | sim | Endereço **HTTPS público** do seu sistema. | | `events` | corpo | lista | não | [Eventos](#eventos-dispon%C3%ADveis) a receber. Padrão: `["*"]` (todos). | | `description` | corpo | string | não | Descrição (até 200 caracteres). | | `enabled` | corpo | booleano | não | Padrão: `true`. | | `Idempotency-Key` | cabeçalho | string | não | Evita criar dois webhooks numa retentativa. | * curl ```bash curl https://platform.toptalking.com.br/v1/webhooks \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: webhook-crm-interno" \ -d '{ "url": "https://meusistema.com.br/webhooks/toptalking", "events": ["message.received", "contact.stage_changed"], "description": "CRM interno" }' ``` * JavaScript ```js const res = await fetch('https://platform.toptalking.com.br/v1/webhooks', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'webhook-crm-interno', }, body: JSON.stringify({ url: 'https://meusistema.com.br/webhooks/toptalking', events: ['message.received', 'contact.stage_changed'], description: 'CRM interno', }), }); const webhook = await res.json(); // Guarde webhook.secret AGORA (ex.: na variável TOPTALKING_WEBHOOK_SECRET). ``` * Python ```python import os import requests res = requests.post( "https://platform.toptalking.com.br/v1/webhooks", headers={ "Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}", "Idempotency-Key": "webhook-crm-interno", }, json={ "url": "https://meusistema.com.br/webhooks/toptalking", "events": ["message.received", "contact.stage_changed"], "description": "CRM interno", }, timeout=30, ) res.raise_for_status() webhook = res.json() # Guarde webhook["secret"] AGORA (ex.: na variável TOPTALKING_WEBHOOK_SECRET). ``` #### Resposta `201` O webhook criado **com o `secret`**: ```json { "id": "f03b0773-5912-4a45-9139-c29375485843", "object": "webhook_endpoint", "url": "https://meusistema.com.br/webhooks/toptalking", "description": "CRM interno", "events": ["message.received", "contact.stage_changed"], "enabled": true, "disabled_reason": null, "last_delivery_at": null, "last_delivery_status": null, "created_at": "2026-11-01T10:00:00Z", "updated_at": "2026-11-01T10:00:00Z", "secret": "whsec_d8d10826ecb93b4679de7409553d10982e864a172f6df97b" } ``` O secret só vem nesta resposta A API não devolve o `secret` de novo. Se perder, revele ou gere um novo no painel (**Revelar segredo** / **Gerar novo segredo**, no menu do webhook). #### Erros | HTTP | `code` | Quando acontece | | ---- | ---------------------- | ------------------------------------------------------------------------------------------- | | 400 | `missing_field` | Faltou `url`. | | 400 | `invalid_field` | URL sem HTTPS ou não pública (localhost, rede interna), ou evento desconhecido em `events`. | | 403 | `read_only_key` | Chave somente leitura. | | 409 | `idempotency_conflict` | `Idempotency-Key` reutilizada com outro corpo. | | 422 | `limit_reached` | A organização já tem 10 webhooks. | 422 limit_reached ```json { "error": { "type": "invalid_request_error", "code": "limit_reached", "message": "Limite de 10 webhooks por organização atingido.", "request_id": "req_6f1d2c3b4a5e" } } ``` ## Buscar webhook GET`/v1/webhooks/{webhook_id}`Testar Um webhook pelo ID, sem o `secret`. Útil para conferir `enabled`, `disabled_reason` e o resultado da última entrega. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | -------------- | | `webhook_id` | caminho | UUID | sim | ID do webhook. | * curl ```bash curl https://platform.toptalking.com.br/v1/webhooks/f03b0773-5912-4a45-9139-c29375485843 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const id = 'f03b0773-5912-4a45-9139-c29375485843'; const res = await fetch(`https://platform.toptalking.com.br/v1/webhooks/${id}`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const webhook = await res.json(); if (!webhook.enabled) console.warn('Webhook desativado:', webhook.disabled_reason); ``` * Python ```python import os import requests webhook_id = "f03b0773-5912-4a45-9139-c29375485843" res = requests.get( f"https://platform.toptalking.com.br/v1/webhooks/{webhook_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() webhook = res.json() ``` #### Resposta `200` ```json { "id": "f03b0773-5912-4a45-9139-c29375485843", "object": "webhook_endpoint", "url": "https://meusistema.com.br/webhooks/toptalking", "description": "CRM interno", "events": ["message.received", "contact.stage_changed"], "enabled": true, "disabled_reason": null, "last_delivery_at": "2026-11-11T14:12:41Z", "last_delivery_status": 200, "created_at": "2026-11-01T10:00:00Z", "updated_at": "2026-11-01T10:00:00Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | --------------------- | | 404 | `resource_not_found` | O webhook não existe. | ## Atualizar webhook PATCH`/v1/webhooks/{webhook_id}`Testar Muda o endereço, os eventos ou a descrição, ou pausa/reativa o webhook — inclusive um que a TopTalking desativou por falhas seguidas. Envie só o que quer mudar. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------- | ------- | ---------------- | ----------- | --------------------------------------------- | | `webhook_id` | caminho | UUID | sim | ID do webhook. | | `url` | corpo | string | não | Novo endereço HTTPS. | | `events` | corpo | lista | não | Nova lista de eventos (substitui a anterior). | | `description` | corpo | string ou `null` | não | Nova descrição. | | `enabled` | corpo | booleano | não | `false` pausa; `true` reativa. | * curl ```bash # Reativar e passar a receber também os eventos de conexão curl -X PATCH https://platform.toptalking.com.br/v1/webhooks/f03b0773-5912-4a45-9139-c29375485843 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "events": ["message.received", "contact.stage_changed", "channel.status_updated"] }' ``` * JavaScript ```js const id = 'f03b0773-5912-4a45-9139-c29375485843'; const res = await fetch(`https://platform.toptalking.com.br/v1/webhooks/${id}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ enabled: true, events: ['message.received', 'contact.stage_changed', 'channel.status_updated'], }), }); const webhook = await res.json(); ``` * Python ```python import os import requests webhook_id = "f03b0773-5912-4a45-9139-c29375485843" res = requests.patch( f"https://platform.toptalking.com.br/v1/webhooks/{webhook_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, json={ "enabled": True, "events": ["message.received", "contact.stage_changed", "channel.status_updated"], }, timeout=30, ) res.raise_for_status() webhook = res.json() ``` #### Resposta `200` ```json { "id": "f03b0773-5912-4a45-9139-c29375485843", "object": "webhook_endpoint", "url": "https://meusistema.com.br/webhooks/toptalking", "description": "CRM interno", "events": ["message.received", "contact.stage_changed", "channel.status_updated"], "enabled": true, "disabled_reason": null, "last_delivery_at": "2026-11-11T14:12:41Z", "last_delivery_status": 200, "created_at": "2026-11-01T10:00:00Z", "updated_at": "2026-11-11T15:05:30Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | ------------------------------- | ----------------------------------------------------- | | 400 | `missing_field` | O corpo não tem nenhum campo para atualizar. | | 400 | `invalid_json`, `invalid_field` | Corpo inválido, URL sem HTTPS ou evento desconhecido. | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O webhook não existe. | ## Excluir webhook DELETE`/v1/webhooks/{webhook_id}`Testar Remove o webhook: o endereço para de receber eventos imediatamente. Para só pausar, use [`PATCH`](#atualizar-webhook) com `enabled: false`. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | -------------- | | `webhook_id` | caminho | UUID | sim | ID do webhook. | * curl ```bash curl -X DELETE https://platform.toptalking.com.br/v1/webhooks/f03b0773-5912-4a45-9139-c29375485843 \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const id = 'f03b0773-5912-4a45-9139-c29375485843'; const res = await fetch(`https://platform.toptalking.com.br/v1/webhooks/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const { deleted } = await res.json(); // true ``` * Python ```python import os import requests webhook_id = "f03b0773-5912-4a45-9139-c29375485843" res = requests.delete( f"https://platform.toptalking.com.br/v1/webhooks/{webhook_id}", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) res.raise_for_status() ``` #### Resposta `200` ```json { "id": "f03b0773-5912-4a45-9139-c29375485843", "object": "webhook_endpoint", "deleted": true } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------- | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O webhook não existe. | ## Enviar evento de teste POST`/v1/webhooks/{webhook_id}/test`Testar Coloca na fila um evento [`ping`](/webhooks/eventos/#ping) para o endereço — **mesmo se o webhook estiver desativado**. É o mesmo que o botão **Enviar teste** do painel. Use para conferir se o seu endpoint recebe e valida a assinatura. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ---- | ----------- | -------------- | | `webhook_id` | caminho | UUID | sim | ID do webhook. | * curl ```bash curl -X POST https://platform.toptalking.com.br/v1/webhooks/f03b0773-5912-4a45-9139-c29375485843/test \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const id = 'f03b0773-5912-4a45-9139-c29375485843'; const res = await fetch(`https://platform.toptalking.com.br/v1/webhooks/${id}/test`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` }, }); const entrega = await res.json(); // status 202 ``` * Python ```python import os import requests webhook_id = "f03b0773-5912-4a45-9139-c29375485843" res = requests.post( f"https://platform.toptalking.com.br/v1/webhooks/{webhook_id}/test", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, timeout=30, ) entrega = res.json() # res.status_code == 202 ``` #### Resposta `202` A entrega criada, ainda na fila: ```json { "id": "d94d8ee3-4eb6-41f1-8c44-44bd0907d99f", "object": "webhook_delivery", "webhook_id": "f03b0773-5912-4a45-9139-c29375485843", "event_id": "252a2c18-7b60-47a6-9feb-9c8322f2ba50", "event_type": "ping", "status": "pending", "attempts": 0, "response_status": null, "error": null, "duration_ms": null, "next_attempt_at": "2026-11-11T15:10:00Z", "delivered_at": null, "created_at": "2026-11-11T15:10:00Z" } ``` #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | ---------------------- | | 403 | `read_only_key` | Chave somente leitura. | | 404 | `resource_not_found` | O webhook não existe. | ## Histórico de entregas GET`/v1/webhooks/{webhook_id}/deliveries`Testar As últimas tentativas de entrega deste webhook, com [paginação](/comecando/paginacao/). Os eventos ficam guardados por **30 dias**. Use para investigar por que o seu sistema não recebeu um evento. | Parâmetro | Onde | Tipo | Obrigatório | Descrição | | ------------ | ------- | ------- | ----------- | -------------------------------------- | | `webhook_id` | caminho | UUID | sim | ID do webhook. | | `limit` | query | inteiro | não | Itens por página, 1 a 100 (padrão 20). | | `cursor` | query | string | não | `next_cursor` da página anterior. | * curl ```bash curl "https://platform.toptalking.com.br/v1/webhooks/f03b0773-5912-4a45-9139-c29375485843/deliveries?limit=20" \ -H "Authorization: Bearer $TOPTALKING_API_KEY" ``` * JavaScript ```js const id = 'f03b0773-5912-4a45-9139-c29375485843'; const res = await fetch( `https://platform.toptalking.com.br/v1/webhooks/${id}/deliveries?limit=20`, { headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}` } }, ); const { data: entregas } = await res.json(); const comFalha = entregas.filter((e) => e.status !== 'succeeded'); ``` * Python ```python import os import requests webhook_id = "f03b0773-5912-4a45-9139-c29375485843" res = requests.get( f"https://platform.toptalking.com.br/v1/webhooks/{webhook_id}/deliveries", headers={"Authorization": f"Bearer {os.environ['TOPTALKING_API_KEY']}"}, params={"limit": 20}, timeout=30, ) res.raise_for_status() entregas = res.json()["data"] ``` #### Resposta `200` ```json { "object": "list", "data": [ { "id": "7c8f5d3c-c721-44aa-a161-3fe74f6ad736", "object": "webhook_delivery", "webhook_id": "f03b0773-5912-4a45-9139-c29375485843", "event_id": "597b7d23-208d-4b5b-82a2-fb1703a81d63", "event_type": "message.received", "status": "pending", "attempts": 2, "response_status": 500, "error": "HTTP 500", "duration_ms": 1240, "next_attempt_at": "2026-11-11T14:18:42Z", "delivered_at": null, "created_at": "2026-11-11T14:12:41Z" }, { "id": "d94d8ee3-4eb6-41f1-8c44-44bd0907d99f", "object": "webhook_delivery", "webhook_id": "f03b0773-5912-4a45-9139-c29375485843", "event_id": "252a2c18-7b60-47a6-9feb-9c8322f2ba50", "event_type": "ping", "status": "succeeded", "attempts": 1, "response_status": 200, "error": null, "duration_ms": 184, "next_attempt_at": null, "delivered_at": "2026-11-11T15:10:01Z", "created_at": "2026-11-11T15:10:00Z" } ], "has_more": false, "next_cursor": null } ``` | Campo | Tipo | Descrição | | ----------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `id` | UUID | ID da entrega (o mesmo do cabeçalho `X-TopTalking-Delivery-Id`). | | `object` | string | Sempre `webhook_delivery`. | | `webhook_id` | UUID | Webhook de destino. | | `event_id` | UUID | ID do evento (o mesmo em todas as tentativas). | | `event_type` | string | Tipo do evento (ex.: `message.received`). | | `status` | string | `pending` (na fila ou esperando nova tentativa), `processing` (enviando), `succeeded` (seu sistema respondeu 2xx) ou `failed`. | | `attempts` | inteiro | Quantas tentativas já foram feitas. | | `response_status` | inteiro ou `null` | Status HTTP que o seu sistema respondeu na última tentativa. | | `error` | string ou `null` | Motivo da falha (timeout, erro de conexão, status não 2xx…). | | `duration_ms` | inteiro ou `null` | Quanto o seu sistema demorou para responder. | | `next_attempt_at` | data ou `null` | Quando será a próxima tentativa, se houver. | | `delivered_at` | data ou `null` | Quando foi entregue com sucesso. | | `created_at` | data | Quando a entrega foi criada. | Entregas que falharam podem ser reenviadas pelo painel (botão **Reenviar**, em **Ver entregas**). Veja [Entregas e novas tentativas](/webhooks/entregas/). #### Erros | HTTP | `code` | Quando acontece | | ---- | -------------------- | --------------------- | | 404 | `resource_not_found` | O webhook não existe. | # Visão geral dos webhooks > Receba eventos da TopTalking em tempo real (mensagem recebida, status de entrega, contato criado ou alterado, mudança de etapa no Kanban, mensagem agendada, conexão do WhatsApp e status de campanha) no seu sistema por webhook HTTPS assinado. Com webhooks, **a TopTalking avisa o seu sistema** quando algo acontece na conta — sem você precisar consultar a API o tempo todo. Cada evento é um `POST` com JSON para um endereço HTTPS seu, assinado com HMAC-SHA256. Exemplos de uso: * Gravar no seu banco cada mensagem recebida (`message.received`). * Atualizar o CRM quando um card muda de etapa no Kanban (`contact.stage_changed`). * Saber quando uma mensagem foi entregue ou lida (`message.status_updated`). * Avisar a equipe quando um lembrete agendado falhar (`scheduled_message.failed`). * Saber na hora que um WhatsApp foi conectado pelo QR Code — ou que caiu (`channel.status_updated`). * Acompanhar a aprovação e o envio de uma campanha (`campaign.status_updated`). A lista completa, com exemplos de JSON, está em [Eventos e payloads](/webhooks/eventos/). ## Configurar no painel 1. Abra [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor) e vá até o cartão **Webhooks**. 2. Clique em **Novo webhook**. 3. Informe a **URL** do seu sistema (HTTPS), uma descrição e escolha os eventos — ou **Todos os eventos**. 4. Copie o **segredo de assinatura** (`whsec_...`) e guarde como variável de ambiente no seu servidor (ex.: `TOPTALKING_WEBHOOK_SECRET`). Você vai usá-lo para [verificar a assinatura](/webhooks/assinatura/). 5. No menu do webhook, clique em **Enviar teste**. O seu endpoint deve receber um evento `ping` e responder `2xx`. 6. Em **Ver entregas**, confira o status HTTP e o tempo de resposta de cada envio. Prefere automatizar? Dá para fazer tudo pela API: veja [Webhooks (gestão)](/referencia/webhooks/). ## Requisitos do endereço * **HTTPS** com certificado válido. * **Público na internet:** `localhost`, IPs de rede interna e endereços `.local` não recebem eventos. Para testar na sua máquina, use um túnel HTTPS (como ngrok ou Cloudflare Tunnel). * Até **10 webhooks** por organização. Cada um pode assinar eventos diferentes. ## Como chega um evento ```http POST /webhooks/toptalking HTTP/1.1 Host: meusistema.com.br Content-Type: application/json User-Agent: TopTalking-Webhooks/1.0 X-TopTalking-Signature: t=1794409800,v1=946dcacfdaee2f0eca7a2194d9919092cac50f554189d6651b1ce5acca155cdc X-TopTalking-Event: ping X-TopTalking-Event-Id: 252a2c18-7b60-47a6-9feb-9c8322f2ba50 X-TopTalking-Delivery-Id: d94d8ee3-4eb6-41f1-8c44-44bd0907d99f {"id":"252a2c18-7b60-47a6-9feb-9c8322f2ba50","object":"event","type":"ping","created_at":"2026-11-11T15:10:00Z","organization_id":"e044a615-e879-44bb-8510-948b10b5e93c","data":{"webhook_id":"f03b0773-5912-4a45-9139-c29375485843","message":"Evento de teste enviado pela TopTalking."}} ``` | Cabeçalho | Descrição | | -------------------------- | -------------------------------------------------------------------------------------------------------------- | | `Content-Type` | Sempre `application/json`. | | `User-Agent` | `TopTalking-Webhooks/1.0`. | | `X-TopTalking-Signature` | `t=,v1=`. Veja [Verificar a assinatura](/webhooks/assinatura/). | | `X-TopTalking-Event` | O tipo do evento (ex.: `message.received`). | | `X-TopTalking-Event-Id` | ID do evento. **É o mesmo em todas as retentativas** — use para não processar duas vezes. | | `X-TopTalking-Delivery-Id` | ID da entrega para este endereço. | ### O corpo (envelope) Todo evento tem o mesmo envelope; o conteúdo muda em `data`: ```json { "id": "597b7d23-208d-4b5b-82a2-fb1703a81d63", "object": "event", "type": "message.received", "created_at": "2026-11-11T14:12:40Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "message": { "...": "..." }, "contact": { "...": "..." } } } ``` | Campo | Descrição | | ----------------- | ------------------------------------------------------------------ | | `id` | ID do evento (igual ao cabeçalho `X-TopTalking-Event-Id`). | | `object` | Sempre `event`. | | `type` | Tipo do evento. | | `created_at` | Quando o evento aconteceu. | | `organization_id` | Organização de origem. | | `data` | Os dados do evento. Veja [Eventos e payloads](/webhooks/eventos/). | ## Como responder * Responda com **qualquer status `2xx` em até 10 segundos**. O corpo da resposta é ignorado. * **Valide a assinatura antes de tudo** e responda `401` se ela não bater. * Faça o trabalho pesado **depois de responder** (fila, job em segundo plano). Se você demorar mais de 10 segundos ou responder fora de `2xx`, a TopTalking considera falha e [tenta de novo](/webhooks/entregas/). * Seja **idempotente**: o mesmo evento pode chegar mais de uma vez. Guarde os `id` já processados. * Não dependa da **ordem** de chegada: use `created_at` e os dados do próprio evento. Esqueleto de um receptor (Node.js + Express) ```js import express from 'express'; import { verificarAssinatura } from './toptalking-assinatura.js'; // veja "Verificar a assinatura" const app = express(); // Importante: o corpo CRU (sem JSON.parse) é necessário para validar a assinatura. app.post('/webhooks/toptalking', express.raw({ type: 'application/json' }), async (req, res) => { if (!verificarAssinatura(req.body, req.get('X-TopTalking-Signature'), process.env.TOPTALKING_WEBHOOK_SECRET)) { return res.status(401).send('assinatura inválida'); } const evento = JSON.parse(req.body.toString('utf8')); // Responda rápido; processe depois. res.sendStatus(200); await filaDeEventos.adicionar(evento); // sua fila / job em segundo plano }); app.listen(3000); ``` [Eventos e payloads](/webhooks/eventos/)Todos os eventos com exemplos de JSON. [Verificar a assinatura](/webhooks/assinatura/)Código pronto para Node.js, Python e Supabase Edge Functions. [Entregas e novas tentativas](/webhooks/entregas/)Prazos, retentativas, desativação automática e histórico. [Receber webhooks no Lovable](/ia/lovable/#receber-webhooks-numa-edge-function)Edge Function que valida a assinatura e salva numa tabela. # Eventos e payloads > Todos os eventos de webhook da TopTalking com exemplos de JSON - message.received, message.sent, message.status_updated, contact.created, contact.updated, contact.stage_changed, scheduled_message.sent, scheduled_message.failed, channel.status_updated, campaign.status_updated e ping. Todo evento chega no mesmo [envelope](/webhooks/visao-geral/#o-corpo-envelope) (`id`, `object: "event"`, `type`, `created_at`, `organization_id`, `data`). Os objetos dentro de `data` têm exatamente o mesmo formato da API REST: [mensagem](/referencia/mensagens/#o-objeto-mensagem), [contato](/referencia/contatos/#o-objeto-contato), [mensagem agendada](/referencia/mensagens-agendadas/#o-objeto-mensagem-agendada), [canal](/referencia/canais/#o-objeto-canal) e [campanha](/referencia/campanhas/#o-objeto-campanha). | Evento | Quando dispara | `data` | | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------- | | [`message.received`](#messagereceived) | Um contato mandou mensagem (WhatsApp, grupo ou Chat do site). | `message`, `contact` | | [`message.sent`](#messagesent) | Uma mensagem saiu pela conta (equipe, IA, campanha, Agenda ou API). | `message`, `contact` | | [`message.status_updated`](#messagestatus_updated) | O status de entrega de uma mensagem enviada mudou. | `message`, `contact`, `previous_status` | | [`contact.created`](#contactcreated) | Um contato novo entrou na base. | `contact` | | [`contact.updated`](#contactupdated) | Mudou nome, e-mail, etapa, responsável, IA, arquivamento ou finalização. | `contact`, `changed_fields`, `previous` | | [`contact.stage_changed`](#contactstage_changed) | O card mudou de etapa no Kanban. | `contact`, `stage`, `previous_stage` | | [`scheduled_message.sent`](#scheduled_messagesent) | Uma mensagem agendada foi enviada. | `scheduled_message` | | [`scheduled_message.failed`](#scheduled_messagefailed) | Uma mensagem agendada falhou. | `scheduled_message` | | [`channel.status_updated`](#channelstatus_updated) | Um WhatsApp conectou (o QR Code foi lido), caiu ou foi desconectado. | `channel`, `previous_status` | | [`campaign.status_updated`](#campaignstatus_updated) | Uma campanha foi aprovada, teve ajustes pedidos, começou a enviar, foi pausada, concluída ou falhou. | `campaign`, `previous_status` | | [`ping`](#ping) | Teste pedido no painel (**Enviar teste**) ou pela API. | `webhook_id`, `message` | Ao criar o webhook, assine eventos específicos ou `*` para receber todos. O `ping` sempre é entregue quando você pede um teste. ## `message.received` Um contato mandou uma mensagem para a empresa — no WhatsApp, num grupo ou no Chat do site. `message.direction` é sempre `inbound`. ```json { "id": "597b7d23-208d-4b5b-82a2-fb1703a81d63", "object": "event", "type": "message.received", "created_at": "2026-11-11T14:12:40Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "message": { "id": "b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "inbound", "type": "text", "text": "Vocês entregam no sábado?", "media": null, "status": "delivered", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:12:40Z", "edited_at": null }, "contact": { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 1, "last_message_at": "2026-11-11T14:12:40Z", "last_message_preview": "Vocês entregam no sábado?", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:12:40Z" } } } ``` Um áudio recebido chega com `type: "audio"`, o arquivo em `media` e, quando disponível, a transcrição em `transcript`: ```json { "id": "3f08322f-2c8f-4633-a478-5d25a5342f66", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "inbound", "type": "audio", "text": null, "media": { "url": "https://arquivos.exemplo.com/audios/3f08322f.ogg", "mime_type": "audio/ogg", "filename": null }, "status": "delivered", "error": null, "reply_to_id": null, "reaction": null, "sender_name": null, "transcript": "Oi, queria saber se ainda tem o tênis 39 em estoque.", "created_at": "2026-11-11T14:16:02Z", "edited_at": null } ``` Em grupos, `contact.is_group` é `true` e `message.sender_name` diz quem mandou. ## `message.sent` Uma mensagem saiu pela conta — enviada pela equipe no painel, pela IA, por uma campanha, pela Agenda ou pela API. `message.direction` é sempre `outbound`. O evento pode chegar com `status: "queued"` (ainda na fila). Para saber se foi entregue, acompanhe [`message.status_updated`](#messagestatus_updated). ```json { "id": "62b273bc-c0b1-4b61-b1d4-de8d115483cd", "object": "event", "type": "message.sent", "created_at": "2026-11-11T14:30:12Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "message": { "id": "03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Entregamos sim! Sábado das 8h às 14h.", "media": null, "status": "queued", "error": null, "reply_to_id": "b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba", "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:30:12Z", "edited_at": null }, "contact": { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 0, "last_message_at": "2026-11-11T14:30:12Z", "last_message_preview": "Entregamos sim! Sábado das 8h às 14h.", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:30:12Z" } } } ``` ## `message.status_updated` O status de entrega de uma mensagem **enviada** mudou: `queued` → `sent` → `delivered` → `read`, ou `failed`. Só existe para mensagens `outbound`. `previous_status` traz o status anterior. ```json { "id": "a6d85050-d17c-4ce6-ae54-4639a7f38536", "object": "event", "type": "message.status_updated", "created_at": "2026-11-11T14:31:05Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "message": { "id": "03ab9a84-86b0-4a1c-a9b2-bbfa205cc52e", "object": "message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "direction": "outbound", "type": "text", "text": "Entregamos sim! Sábado das 8h às 14h.", "media": null, "status": "read", "error": null, "reply_to_id": "b7fa4c19-3e25-4ce9-9794-4e7f60efa4ba", "reaction": null, "sender_name": null, "transcript": null, "created_at": "2026-11-11T14:30:12Z", "edited_at": null }, "contact": { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 0, "last_message_at": "2026-11-11T14:30:12Z", "last_message_preview": "Entregamos sim! Sábado das 8h às 14h.", "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:30:12Z" }, "previous_status": "delivered" } } ``` Quando a entrega falha, `message.status` é `failed` e `message.error` explica o motivo. ## `contact.created` Um contato novo entrou na base — por uma mensagem recebida, pelo Chat do site, pelo painel, pela API ou por outra origem (veja `contact.source`). ```json { "id": "2d3339bd-e78f-4ebd-84b5-46afdb2b348d", "object": "event", "type": "contact.created", "created_at": "2026-11-11T15:01:18Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "contact": { "id": "6586daec-7197-478e-b0ff-5b6659c11d26", "object": "contact", "name": "Ana Lima", "phone": "5531991234567", "email": null, "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": null, "ai_enabled": true, "ai_agent_id": null, "assigned_to": null, "pinned": false, "unread_count": 0, "last_message_at": null, "last_message_preview": null, "resolved_at": null, "archived_at": null, "source": "whatsapp", "created_at": "2026-11-11T15:01:18Z", "updated_at": "2026-11-11T15:01:18Z" } } } ``` ## `contact.updated` Disparado quando muda um **campo de negócio** do contato: | Valor em `changed_fields` | O que mudou | | ------------------------- | ---------------------------------------------------------------------------------- | | `name` | Nome. | | `email` | E-mail. | | `stage` | Etapa do Kanban (também dispara [`contact.stage_changed`](#contactstage_changed)). | | `assigned_to` | Responsável. | | `ai_enabled` | IA ligada ou desligada na conversa. | | `archived_at` | Arquivado ou desarquivado. | | `resolved_at` | Finalizado ou reaberto. | Ele **não** dispara a cada mensagem nova (contador de não lidas, prévia da última mensagem etc.) — para isso existem os eventos de mensagem. `previous` traz os **valores anteriores só dos campos que mudaram**. Para `stage`, o valor anterior é um objeto de etapa (`id`, `name`, `is_final`) ou `null`. ```json { "id": "417bc8d0-a2e2-492b-b54a-2d1c7a982e85", "object": "event", "type": "contact.updated", "created_at": "2026-11-11T14:35:21Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "contact": { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "3c671a88-07fb-4ccc-80db-184be834360c", "name": "Finalizado", "is_final": true }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 0, "last_message_at": "2026-11-11T14:30:12Z", "last_message_preview": "Entregamos sim! Sábado das 8h às 14h.", "resolved_at": "2026-11-11T14:35:21Z", "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:35:21Z" }, "changed_fields": ["stage", "resolved_at"], "previous": { "stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false }, "resolved_at": null } } } ``` ## `contact.stage_changed` O card mudou de etapa no Kanban — pelo painel, pela IA ou pela API. `stage` é a etapa nova e `previous_stage` a anterior (qualquer uma pode ser `null`, quando o card entra ou sai do Kanban). Se `stage.is_final` for `true`, o card foi para a etapa final e a conversa foi finalizada. ```json { "id": "d11785e0-67f1-45b8-9796-ccf867b500ba", "object": "event", "type": "contact.stage_changed", "created_at": "2026-11-11T14:35:21Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "contact": { "id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "object": "contact", "name": "Maria Souza", "phone": "5511999998888", "email": "maria@exemplo.com", "channel_id": "d781e759-4047-4c35-bfcd-eae8c666369a", "channel_type": "whatsapp", "is_group": false, "stage": { "id": "3c671a88-07fb-4ccc-80db-184be834360c", "name": "Finalizado", "is_final": true }, "ai_enabled": true, "ai_agent_id": null, "assigned_to": "4083deb8-f96e-4093-8598-ebdd4dee786b", "pinned": false, "unread_count": 0, "last_message_at": "2026-11-11T14:30:12Z", "last_message_preview": "Entregamos sim! Sábado das 8h às 14h.", "resolved_at": "2026-11-11T14:35:21Z", "archived_at": null, "source": "whatsapp", "created_at": "2026-10-28T19:03:55Z", "updated_at": "2026-11-11T14:35:21Z" }, "stage": { "id": "3c671a88-07fb-4ccc-80db-184be834360c", "name": "Finalizado", "is_final": true }, "previous_stage": { "id": "9b2f5a8e-6a3b-4d0e-9a51-2a7f4f1d0c11", "name": "Negociação", "is_final": false } } } ``` ## `scheduled_message.sent` Uma [mensagem agendada](/referencia/mensagens-agendadas/) (Agenda) foi enviada. `message_id` aponta para a mensagem gerada — que também gera um [`message.sent`](#messagesent). ```json { "id": "a5ad15a7-bccf-4ff7-ac3c-64168d032258", "object": "event", "type": "scheduled_message.sent", "created_at": "2026-11-10T13:00:41Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "scheduled_message": { "id": "d3440bed-1a6f-4470-9d89-cec62fe2fc67", "object": "scheduled_message", "contact_id": "721bfe94-e549-4354-b90c-a8d679e9b271", "to": "5521987654321", "title": "Renovação do plano", "text": "Oi {{nome}}! Seu plano vence na sexta. Quer que a gente renove?", "media": [], "send_at": "2026-11-10T13:00:00Z", "status": "sent", "sent_at": "2026-11-10T13:00:41Z", "message_id": "8e395744-cc4c-4eae-9ccd-06c8dde834e0", "error": null, "created_at": "2026-11-03T18:22:10Z", "updated_at": "2026-11-10T13:00:41Z" } } } ``` ## `scheduled_message.failed` Uma mensagem agendada não pôde ser enviada. `error` explica o motivo (ex.: WhatsApp desconectado no horário do envio). ```json { "id": "d161f19b-c31d-4378-88f7-3f6c23c3e135", "object": "event", "type": "scheduled_message.failed", "created_at": "2026-11-12T12:00:38Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "scheduled_message": { "id": "603916c7-604c-4c56-8a58-f3f76886634b", "object": "scheduled_message", "contact_id": "3f1c2b9a-7d4e-4c55-8a0e-1b2c3d4e5f60", "to": "5511999998888", "title": "Lembrete de consulta", "text": "Oi {{nome}}, passando para lembrar da sua consulta amanhã às 9h.", "media": [], "send_at": "2026-11-12T12:00:00Z", "status": "failed", "sent_at": null, "message_id": null, "error": "WhatsApp desconectado.", "created_at": "2026-11-11T14:50:00Z", "updated_at": "2026-11-12T12:00:38Z" } } } ``` ## `channel.status_updated` A conexão de um WhatsApp mudou: o celular leu o QR Code (ou digitou o código de pareamento) e o canal **conectou**, a sessão **caiu** ou alguém **desconectou** o número. `data.channel` é o [canal](/referencia/canais/#o-objeto-canal) já com o status novo e `previous_status` é o status anterior (`connected`, `connecting`, `disconnected` ou `null`). É o jeito mais rápido de saber que o pareamento terminou — sem consultar `GET /channels/{channel_id}/qr` — e de avisar a equipe quando um número cai. * Só dispara para canais `whatsapp` (o Chat do site não gera este evento). * **Não** dispara quando o QR Code só é renovado durante o pareamento. ```json { "id": "d6778bf5-f7d2-40d4-ba5e-0edaafd4af41", "object": "event", "type": "channel.status_updated", "created_at": "2026-11-11T15:21:14Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "channel": { "id": "742539b3-2c69-4acc-988f-fcd836e565a9", "object": "channel", "type": "whatsapp", "name": "Pós-venda", "phone": "5511955554444", "status": "connected", "is_primary": false, "created_at": "2026-11-11T15:20:00Z" }, "previous_status": "connecting" } } ``` Quando um número cai, chega `channel.status` igual a `disconnected` e `previous_status` igual a `connected`: avise a equipe e gere um novo QR Code com [`POST /channels/{channel_id}/connect`](/referencia/canais/#reconectar-um-whatsapp-desconectado). ## `campaign.status_updated` Uma [campanha](/referencia/campanhas/) mudou de status: foi **aprovada** pela TopTalking, teve **ajustes pedidos**, **começou a enviar**, foi **pausada**, **concluída** ou **falhou**. `data.campaign` é a [campanha](/referencia/campanhas/#o-objeto-campanha) atualizada e `previous_status` o status anterior. ```json { "id": "a6624a26-d283-4db2-9585-58d26bbf213d", "object": "event", "type": "campaign.status_updated", "created_at": "2026-11-11T16:20:00Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "campaign": { "id": "f2143187-f4f8-43b0-9c03-57a26c575f41", "object": "campaign", "name": "Black Friday 2026", "status": "approved", "review_notes": null, "template": { "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING" }, "totals": { "recipients": 3, "pending": 3, "sent": 0, "delivered": 0, "read": 0, "failed": 0 }, "pricing": { "unit_price": 0.35, "currency": "BRL", "estimated_total": 1.05, "charged_total": 0 }, "created_at": "2026-11-11T16:00:00Z", "submitted_at": "2026-11-11T16:00:00Z", "reviewed_at": "2026-11-11T16:20:00Z", "started_at": null, "completed_at": null }, "previous_status": "pending_approval" } } ``` Quando a revisão pede ajustes, `campaign.status` vem `changes_requested` e `campaign.review_notes` explica o que mudar: ```json { "id": "c3d40a38-2062-4259-b999-047a297ba3aa", "object": "event", "type": "campaign.status_updated", "created_at": "2026-11-11T16:18:40Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "campaign": { "id": "1eaa64c7-7f3c-40e6-9554-754d3e4b097c", "object": "campaign", "name": "Black Friday 2026 - lista comprada", "status": "changes_requested", "review_notes": "A lista parece incluir pessoas que nunca falaram com a loja. Envie só clientes que aceitaram receber mensagens.", "template": { "name": "promo_black_friday", "language": "pt_BR", "category": "MARKETING" }, "totals": { "recipients": 1500, "pending": 1500, "sent": 0, "delivered": 0, "read": 0, "failed": 0 }, "pricing": { "unit_price": 0.35, "currency": "BRL", "estimated_total": 525, "charged_total": 0 }, "created_at": "2026-11-11T15:50:00Z", "submitted_at": "2026-11-11T15:50:00Z", "reviewed_at": "2026-11-11T16:18:40Z", "started_at": null, "completed_at": null }, "previous_status": "pending_approval" } } ``` ## `ping` Evento de teste. Enviado quando você clica em **Enviar teste** no painel ou chama [`POST /webhooks/{webhook_id}/test`](/referencia/webhooks/#enviar-evento-de-teste) — mesmo que o webhook esteja desativado e mesmo que ele não assine `*`. Responda `2xx` e ignore o conteúdo. ```json { "id": "252a2c18-7b60-47a6-9feb-9c8322f2ba50", "object": "event", "type": "ping", "created_at": "2026-11-11T15:10:00Z", "organization_id": "e044a615-e879-44bb-8510-948b10b5e93c", "data": { "webhook_id": "f03b0773-5912-4a45-9139-c29375485843", "message": "Evento de teste enviado pela TopTalking." } } ``` ## Tratando eventos no código Roteando por tipo (Node.js) ```js async function processarEvento(evento) { if (await jaProcessado(evento.id)) return; // a mesma entrega pode chegar mais de uma vez switch (evento.type) { case 'message.received': await salvarMensagem(evento.data.message, evento.data.contact); break; case 'message.status_updated': await atualizarStatus(evento.data.message.id, evento.data.message.status); break; case 'contact.stage_changed': await sincronizarEtapaNoCrm(evento.data.contact.id, evento.data.stage?.name ?? null); break; case 'channel.status_updated': if (evento.data.channel.status === 'disconnected') await avisarEquipe(evento.data.channel.name); break; case 'campaign.status_updated': await atualizarCampanha(evento.data.campaign.id, evento.data.campaign.status); break; case 'ping': break; // teste: nada a fazer default: break; // ignore tipos que você não usa (novos eventos podem surgir) } await marcarComoProcessado(evento.id); } ``` # Verificar a assinatura > Como validar o cabeçalho X-TopTalking-Signature (HMAC-SHA256 de "t.corpo" com o secret whsec_) dos webhooks da TopTalking, com código pronto para Node.js (Express), Python (Flask e FastAPI) e Deno / Supabase Edge Functions (Lovable). Qualquer pessoa que descobrir a URL do seu webhook pode mandar um `POST` para ela. A **assinatura** prova que o evento veio da TopTalking e não foi alterado no caminho. **Sempre valide antes de processar.** ## Como a assinatura é feita Cada entrega traz o cabeçalho: ```http X-TopTalking-Signature: t=1794409800,v1=946dcacfdaee2f0eca7a2194d9919092cac50f554189d6651b1ce5acca155cdc ``` * `t` — momento do envio, em Unix timestamp (segundos). * `v1` — assinatura em hexadecimal: ```text v1 = hex( HMAC_SHA256( chave = secret, mensagem = t + "." + corpo_cru ) ) ``` O **secret** é o segredo de assinatura do webhook (`whsec_...`), usado **inteiro** (com o prefixo `whsec_`) como chave. Ele aparece quando o webhook é criado; no painel dá para **Revelar segredo** de novo ou **Gerar novo segredo**. ## Passo a passo 1. Leia o **corpo cru** da requisição, exatamente como chegou (bytes ou texto), **antes** de qualquer `JSON.parse`. Reformatar o JSON muda os bytes e quebra a assinatura. 2. Separe `t` e `v1` do cabeçalho `X-TopTalking-Signature`. 3. Rejeite se `t` estiver a mais de **300 segundos** do horário atual (protege contra reenvio de eventos antigos). 4. Calcule `HMAC-SHA256(secret, ".")` em hexadecimal. 5. Compare com `v1` usando **comparação em tempo constante**. 6. Se não bater, responda `401` e não processe nada. ## Código pronto * Node.js (Express) toptalking-assinatura.js ```js import crypto from 'node:crypto'; const TOLERANCIA_SEGUNDOS = 300; // 5 minutos /** * Confere o cabeçalho X-TopTalking-Signature. * @param {Buffer|string} corpoCru corpo EXATO recebido (antes de qualquer JSON.parse) * @param {string|undefined} cabecalho valor de X-TopTalking-Signature * @param {string} secret segredo do webhook (whsec_...) */ export function verificarAssinatura(corpoCru, cabecalho, secret) { if (!cabecalho || !secret) return false; // Formato: t=,v1= let t = null; const assinaturas = []; for (const parte of cabecalho.split(',')) { const i = parte.indexOf('='); if (i === -1) continue; const chave = parte.slice(0, i).trim(); const valor = parte.slice(i + 1).trim(); if (chave === 't') t = valor; else if (chave === 'v1' && valor) assinaturas.push(valor); } if (!t || !/^\d+$/.test(t) || assinaturas.length === 0) return false; // Rejeita eventos antigos (proteção contra replay). const agora = Math.floor(Date.now() / 1000); if (Math.abs(agora - Number(t)) > TOLERANCIA_SEGUNDOS) return false; // v1 = HMAC-SHA256(chave = secret, mensagem = ".") const esperado = crypto.createHmac('sha256', secret).update(`${t}.`).update(corpoCru).digest(); // Comparação em tempo constante. return assinaturas.some((hex) => { const recebido = Buffer.from(hex, 'hex'); return recebido.length === esperado.length && crypto.timingSafeEqual(recebido, esperado); }); } ``` server.js ```js import express from 'express'; import { verificarAssinatura } from './toptalking-assinatura.js'; const app = express(); // express.raw entrega o corpo como Buffer, sem parse. Não use express.json() nesta rota. app.post('/webhooks/toptalking', express.raw({ type: 'application/json' }), (req, res) => { const ok = verificarAssinatura( req.body, req.get('X-TopTalking-Signature'), process.env.TOPTALKING_WEBHOOK_SECRET, ); if (!ok) return res.status(401).send('assinatura inválida'); const evento = JSON.parse(req.body.toString('utf8')); res.sendStatus(200); // responda rápido… setImmediate(() => processarEvento(evento)); // …e processe depois (ideal: uma fila) }); app.listen(3000); ``` Cuidado com o `express.json()` global Se o seu app usa `app.use(express.json())`, ele consome o corpo antes da sua rota. Registre a rota do webhook **antes** do `express.json()` ou exclua o caminho dela do parser. * Python (Flask) app.py ```python import hashlib import hmac import os import time from flask import Flask, abort, request app = Flask(__name__) SECRET = os.environ["TOPTALKING_WEBHOOK_SECRET"] TOLERANCIA_SEGUNDOS = 300 def verificar_assinatura(corpo: bytes, cabecalho, secret: str) -> bool: """corpo = bytes EXATOS recebidos; cabecalho = X-TopTalking-Signature.""" if not cabecalho or not secret: return False t, assinaturas = None, [] for parte in cabecalho.split(","): chave, _, valor = parte.strip().partition("=") if chave == "t": t = valor elif chave == "v1" and valor: assinaturas.append(valor) if not t or not t.isdigit() or not assinaturas: return False if abs(time.time() - int(t)) > TOLERANCIA_SEGUNDOS: return False esperado = hmac.new(secret.encode(), f"{t}.".encode() + corpo, hashlib.sha256).hexdigest() return any(hmac.compare_digest(esperado.encode(), a.encode()) for a in assinaturas) @app.post("/webhooks/toptalking") def webhook_toptalking(): corpo = request.get_data() # bytes crus if not verificar_assinatura(corpo, request.headers.get("X-TopTalking-Signature"), SECRET): abort(401) evento = request.get_json() # Enfileire o processamento e responda rápido. fila.enqueue(processar_evento, evento) return "", 200 ``` * Python (FastAPI) main.py ```python import hashlib import hmac import json import os import time from fastapi import BackgroundTasks, FastAPI, HTTPException, Request app = FastAPI() SECRET = os.environ["TOPTALKING_WEBHOOK_SECRET"] TOLERANCIA_SEGUNDOS = 300 def verificar_assinatura(corpo: bytes, cabecalho, secret: str) -> bool: if not cabecalho or not secret: return False t, assinaturas = None, [] for parte in cabecalho.split(","): chave, _, valor = parte.strip().partition("=") if chave == "t": t = valor elif chave == "v1" and valor: assinaturas.append(valor) if not t or not t.isdigit() or not assinaturas: return False if abs(time.time() - int(t)) > TOLERANCIA_SEGUNDOS: return False esperado = hmac.new(secret.encode(), f"{t}.".encode() + corpo, hashlib.sha256).hexdigest() return any(hmac.compare_digest(esperado.encode(), a.encode()) for a in assinaturas) @app.post("/webhooks/toptalking") async def webhook_toptalking(request: Request, background: BackgroundTasks): corpo = await request.body() # bytes crus, antes de qualquer parse if not verificar_assinatura(corpo, request.headers.get("x-toptalking-signature"), SECRET): raise HTTPException(status_code=401, detail="assinatura inválida") evento = json.loads(corpo) background.add_task(processar_evento, evento) # roda depois da resposta return {"ok": True} ``` * Deno / Supabase Edge Function Ideal para apps do **Lovable** e projetos Supabase. Usa só Web Crypto (sem dependências). supabase/functions/toptalking-webhook/index.ts ```ts // Recebe os webhooks da TopTalking e valida a assinatura. // Secret necessário: TOPTALKING_WEBHOOK_SECRET (o whsec_... do webhook). const SECRET = Deno.env.get('TOPTALKING_WEBHOOK_SECRET') ?? ''; const TOLERANCIA_SEGUNDOS = 300; const encoder = new TextEncoder(); function hexParaBytes(hex: string): Uint8Array | null { if (hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null; const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); return bytes; } async function verificarAssinatura(corpo: string, cabecalho: string | null, secret: string) { if (!cabecalho || !secret) return false; // Formato: t=,v1= let t: string | null = null; const assinaturas: string[] = []; for (const parte of cabecalho.split(',')) { const i = parte.indexOf('='); if (i === -1) continue; const chave = parte.slice(0, i).trim(); const valor = parte.slice(i + 1).trim(); if (chave === 't') t = valor; else if (chave === 'v1' && valor) assinaturas.push(valor); } if (!t || !/^\d+$/.test(t) || assinaturas.length === 0) return false; // Rejeita eventos com mais de 5 minutos de diferença (replay). if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCIA_SEGUNDOS) return false; const chave = await crypto.subtle.importKey( 'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'], ); const dados = encoder.encode(`${t}.${corpo}`); for (const hex of assinaturas) { const assinatura = hexParaBytes(hex); // crypto.subtle.verify já compara em tempo constante. if (assinatura && (await crypto.subtle.verify('HMAC', chave, assinatura, dados))) return true; } return false; } Deno.serve(async (req) => { if (req.method !== 'POST') return new Response('Método não permitido', { status: 405 }); const corpo = await req.text(); // corpo CRU: não use req.json() antes de validar const valido = await verificarAssinatura(corpo, req.headers.get('X-TopTalking-Signature'), SECRET); if (!valido) return new Response('assinatura inválida', { status: 401 }); const evento = JSON.parse(corpo); // Faça aqui o que precisar (ex.: salvar numa tabela). Mantenha rápido: < 10 s. console.log('Evento recebido:', evento.type, evento.id); return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' }, }); }); ``` A TopTalking não envia o token de login da Supabase, então **desligue a verificação de JWT** desta função: supabase/config.toml ```toml [functions.toptalking-webhook] verify_jwt = false ``` A URL para cadastrar no painel fica assim: `https://.supabase.co/functions/v1/toptalking-webhook`. O guia completo, com tabela e prompt pronto, está em [Lovable](/ia/lovable/#receber-webhooks-numa-edge-function). ## Teste a sua implementação Use estes valores para conferir se o seu código calcula a assinatura certa (desligue a checagem de tempo só neste teste, pois `t` é fixo): | Item | Valor | | ------------- | ------------------------------------------------------------------ | | secret | `whsec_d8d10826ecb93b4679de7409553d10982e864a172f6df97b` | | `t` | `1794409800` | | corpo cru | o JSON abaixo, exatamente assim (uma linha, sem espaços) | | `v1` esperado | `946dcacfdaee2f0eca7a2194d9919092cac50f554189d6651b1ce5acca155cdc` | ```json {"id":"252a2c18-7b60-47a6-9feb-9c8322f2ba50","object":"event","type":"ping","created_at":"2026-11-11T15:10:00Z","organization_id":"e044a615-e879-44bb-8510-948b10b5e93c","data":{"webhook_id":"f03b0773-5912-4a45-9139-c29375485843","message":"Evento de teste enviado pela TopTalking."}} ``` Depois, clique em **Enviar teste** no painel e confira em **Ver entregas** se o seu endpoint respondeu `200`. ## Problemas comuns | Sintoma | Causa provável | | ------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | A assinatura nunca bate | O corpo foi convertido em objeto e depois em texto de novo (`JSON.stringify(req.body)`). Use o corpo cru. | | Bate no teste, falha em produção | Um middleware (body parser, proxy, framework) alterou o corpo antes da sua rota. | | Falha só às vezes | Relógio do servidor fora de hora. Sincronize com NTP; a tolerância é de 300 s. | | Parou de bater depois de um tempo | Alguém clicou em **Gerar novo segredo**. Atualize a variável de ambiente. | | `401` vindo da Supabase, não do seu código | A função está com `verify_jwt` ligado. Desligue para esta função. | # Entregas e novas tentativas > Como a TopTalking entrega webhooks - prazo de 10 segundos, até 6 tentativas (1 min, 5 min, 30 min, 2 h, 6 h), deduplicação pelo id do evento, ordem não garantida, desativação automática após 3 dias de falhas, histórico e reenvio. ## Quando uma entrega é bem-sucedida A entrega conta como **bem-sucedida** quando o seu endpoint responde **qualquer status `2xx` em até 10 segundos**. O corpo da resposta é ignorado. Conta como **falha**: status fora de `2xx`, demora maior que 10 segundos ou erro de conexão (DNS, certificado, servidor fora do ar). ## Novas tentativas Se a entrega falhar, a TopTalking tenta de novo. São até **6 tentativas no total**: | Tentativa | Quando | | --------- | ----------------- | | 1ª | Imediatamente | | 2ª | 1 minuto depois | | 3ª | 5 minutos depois | | 4ª | 30 minutos depois | | 5ª | 2 horas depois | | 6ª | 6 horas depois | Depois da última falha, a entrega fica com status `failed`. Você pode reenviá-la pelo painel (veja abaixo). ## Deduplicação O **`id` do evento é o mesmo em todas as tentativas** (também no cabeçalho `X-TopTalking-Event-Id`). Como uma entrega pode chegar mais de uma vez — por exemplo, se o seu servidor processou mas demorou a responder —, guarde os IDs já processados e ignore repetições: Tabela simples de controle (PostgreSQL) ```sql create table eventos_toptalking ( id uuid primary key, -- evento.id tipo text not null, recebido_em timestamptz not null default now() ); -- No receptor: só processe se o insert funcionar. insert into eventos_toptalking (id, tipo) values ($1, $2) on conflict (id) do nothing returning id; ``` ## Ordem não garantida Eventos podem chegar **fora de ordem** (retentativas, paralelismo). Exemplos: * um `message.status_updated` com `read` pode chegar antes do de `delivered`; * dois `contact.updated` seguidos podem chegar invertidos. Para não voltar um estado no tempo, compare `created_at` do evento (ou `updated_at` do contato) com o que você já tem salvo antes de sobrescrever. Se precisar do estado mais atual com certeza, busque o recurso na API (ex.: [`GET /contacts/{contact_id}`](/referencia/contatos/#buscar-contato)). ## Desativação automática Se um endpoint **só falhar por 3 dias seguidos**, a TopTalking o **desativa automaticamente**: `enabled` vira `false` e `disabled_reason` explica o motivo. Enquanto estiver desativado, ele não recebe novos eventos. Para reativar, corrija o problema e: * no painel, reative o webhook em Configurações → Desenvolvedor → Webhooks; ou * pela API: [`PATCH /webhooks/{webhook_id}`](/referencia/webhooks/#atualizar-webhook) com `{ "enabled": true }`. Antes de reativar, use **Enviar teste** para confirmar que o endpoint voltou a responder `2xx`. ## Histórico e reenvio * **No painel:** menu do webhook → **Ver entregas**. Mostra cada tentativa com o status HTTP, o tempo de resposta e o erro. Entregas com falha têm o botão **Reenviar**. * **Pela API:** [`GET /webhooks/{webhook_id}/deliveries`](/referencia/webhooks/#hist%C3%B3rico-de-entregas). * Os eventos ficam guardados por **30 dias**. ## Boas práticas * **Responda primeiro, processe depois.** Valide a assinatura, coloque o evento numa fila ou tabela e responda `200`. Processamentos longos (chamar outra API, gerar PDF, rodar IA) devem rodar em segundo plano. * **Seja idempotente** com o `id` do evento. * **Não confie na ordem**; use `created_at`. * **Monitore** `last_delivery_status` e `disabled_reason` em [`GET /webhooks`](/referencia/webhooks/#listar-webhooks). * **Ignore tipos desconhecidos** com `2xx`: novos eventos podem ser adicionados no futuro. # Servidor MCP > Servidor MCP da TopTalking (Streamable HTTP em https://platform.toptalking.com.br/mcp) - conecte Claude, Cursor, VS Code e Windsurf à sua conta com a chave de API e veja todas as ferramentas disponíveis. O **MCP** (Model Context Protocol) é o padrão que ferramentas de IA usam para acessar sistemas externos. Com o servidor MCP da TopTalking, você pede em português para o Claude, o Cursor ou o Copilot — “quem me mandou mensagem hoje?”, “mova a Maria para Negociação”, “agende um lembrete para amanhã às 9h” — e a IA usa a sua conta para fazer. | | | | ---------------- | ------------------------------------------------------------------------------------------------ | | **Endereço** | `https://platform.toptalking.com.br/mcp` | | **Transporte** | Streamable HTTP (sem estado; respostas em JSON) | | **Autenticação** | Cabeçalho `Authorization: Bearer tt_live_...` — a mesma [chave de API](/comecando/autenticacao/) | ## Conectar o seu cliente [Claude Code](/mcp/claude-code/)Um comando no terminal. [Claude Desktop](/mcp/claude-desktop/)Via mcp-remote no claude_desktop_config.json. [Cursor](/mcp/cursor/)\~/.cursor/mcp.json com URL e cabeçalho. [VS Code e Windsurf](/mcp/vscode-windsurf/)Copilot (modo agente) e Windsurf Cascade. ## Ferramentas São **18 ferramentas**: 13 de leitura e 5 que alteram a conta. | Ferramenta | Tipo | O que faz | Parâmetros | | -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `get_account` | leitura | Organização e chave em uso. | — | | `list_channels` | leitura | Canais (WhatsApp e Chat do site) e se estão conectados. | — | | `get_channel_qr` | leitura | QR Code atual de um canal de WhatsApp **como imagem** (o assistente mostra na conversa) e o status da conexão. | `channel_id` | | `search_contacts` | leitura | Busca contatos por nome, telefone ou etapa. | `query?`, `phone?`, `stage_id?`, `limit?` | | `get_contact` | leitura | Detalhes de um contato. | `contact_id` | | `list_messages` | leitura | Histórico da conversa de um contato (ou grupo). | `contact_id`, `limit?` | | `list_groups` | leitura | Grupos do WhatsApp (o `id` do grupo é o `contact_id` da conversa). | `query?`, `limit?`, `cursor?` | | `list_stages` | leitura | Etapas do Kanban, em ordem. | — | | `list_scheduled_messages` | leitura | Mensagens agendadas. | `status?`, `contact_id?`, `limit?` | | `list_members` | leitura | Membros da equipe (para atribuir conversas). | — | | `list_ai_agents` | leitura | Agentes de IA e qual é o padrão. | — | | `list_templates` | leitura | Templates do WhatsApp Oficial para campanhas. | `ready?` | | `list_campaigns` | leitura | Campanhas, com status, contadores e preço. | `status?`, `limit?`, `cursor?` | | `upsert_contact` | escrita | Cria ou atualiza um contato pelo telefone. | `phone`, `name?`, `email?`, `stage_id?` | | `update_contact` | escrita | Atualiza um contato: nome, e-mail, **etapa do Kanban**, responsável, fixar, agente de IA, IA ligada, finalizar, arquivar. | `contact_id`, `name?`, `email?`, `stage_id?`, `assigned_to?`, `pinned?`, `ai_agent_id?`, `ai_enabled?`, `resolved?`, `archived?` | | `send_message` | envia mensagem | Envia texto e/ou mídia pelo WhatsApp. | `to?` ou `contact_id?`, `text?`, `media_url?`, `media_type?`, `filename?`, `channel_id?` | | `schedule_message` | envia mensagem | Agenda uma mensagem (aparece na Agenda). | `to?` ou `contact_id?`, `send_at`, `text`, `title?` | | `cancel_scheduled_message` | escrita | Cancela uma mensagem agendada pendente. | `scheduled_message_id` | (`?` = opcional.) As ferramentas seguem as mesmas regras da [API REST](/referencia/visao-geral/): mesmos limites, mesmos erros e tudo aparece no painel. Com uma chave **Somente leitura**, o servidor só expõe as **13 ferramentas de leitura** — a IA nem enxerga as de escrita. ### Conectar um WhatsApp pelo assistente `get_channel_qr` devolve duas partes: uma **imagem** PNG do QR Code (o Claude e outros clientes compatíveis mostram direto na conversa) e um **texto** com `status`, `channel`, `qr_code_generated_at` e `instrucoes` (o que fazer no celular). Peça de novo a cada \~20–30 segundos até o status virar `connected` — o QR Code é renovado nesse intervalo. A ferramenta **lê** o pareamento em andamento (canal `connecting`): o canal precisa ter sido criado (no painel ou com [`POST /channels`](/referencia/canais/#criar-canal)) ou ter um novo QR Code gerado com [`POST /channels/{channel_id}/connect`](/referencia/canais/#gerar-qr-code-ou-c%C3%B3digo-de-pareamento). Veja o fluxo completo em [Conectar um WhatsApp por QR Code](/referencia/canais/#conectar-um-whatsapp-por-qr-code). ## Segurança A IA age na sua conta de verdade As ferramentas mandam mensagens **reais** para os seus clientes pelo WhatsApp da empresa. Um pedido mal interpretado pode virar uma mensagem errada para a pessoa errada. * **Comece com uma chave Somente leitura** para explorar, montar relatórios e testar prompts. * **Revise cada chamada** de `send_message`, `schedule_message` e `update_contact` antes de aprovar. Não ative “aprovar tudo automaticamente” para essas ferramentas. * **Crie uma chave só para a IA**, com nome claro (ex.: “Claude do time de vendas”) e validade, e revogue quando não usar mais. * **Não cole a chave em chats** nem em arquivos que vão para o Git. Prefira variáveis de ambiente quando o cliente suportar. * Lembre-se das [boas práticas no WhatsApp](/comecando/boas-praticas-whatsapp/): nada de disparos em massa pela IA. ## Exemplos de pedidos ```text Quais conversas receberam mensagem hoje e ainda estão sem resposta? ``` ```text Mostre o funil do Kanban: quantos contatos há em cada etapa? ``` ```text Leia as últimas 20 mensagens da conversa com a Maria Souza e resuma o que ela quer. ``` ```text Mova o João Pereira para a etapa "Negociação", desligue a IA nessa conversa e atribua à Carla. ``` ```text Mostre o QR Code do canal "Pós-venda" para eu conectar o WhatsApp e me avise quando conectar. ``` ```text Quais campanhas estão em andamento e quantas mensagens já foram entregues em cada uma? ``` ```text Quem escreveu no grupo "Equipe de Vendas" hoje? Resuma as últimas 30 mensagens. ``` ```text Agende para amanhã às 9h (horário de Brasília) uma mensagem para 5511999998888: "Oi {{nome}}, passando para lembrar da sua consulta às 10h." Mostre antes de agendar. ``` ## E o claude.ai e o ChatGPT? * **claude.ai (web) e Claude Desktop — “conectores personalizados”:** exigem login por **OAuth**, que o servidor da TopTalking **ainda não suporta**. No Claude Desktop, use a [configuração com mcp-remote](/mcp/claude-desktop/). No claude.ai, use um Projeto com a documentação (veja [Claude](/ia/claude/#claudeai-projetos)). * **ChatGPT — conectores:** também exigem OAuth. Use um **GPT personalizado com Actions** importando o OpenAPI: veja [ChatGPT](/ia/chatgpt/). ## Problemas comuns | Sintoma | O que fazer | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Erro 401 / “unauthorized” | Confira o cabeçalho: `Authorization: Bearer tt_live_...` (com `Bearer` e um espaço). A chave pode ter sido revogada ou expirado. | | A IA não vê as ferramentas de envio | A chave é **Somente leitura**. Crie uma com **Acesso total** se quiser permitir escrita. | | “channel_unavailable” ao enviar | O WhatsApp está desconectado. Reconecte no painel em Configurações → Conexões ou gere um novo QR Code com [`POST /channels/{channel_id}/connect`](/referencia/canais/#reconectar-um-whatsapp-desconectado) e peça à IA para mostrá-lo com `get_channel_qr`. | | `get_channel_qr` não mostra QR Code | Não há pareamento em andamento (o canal está `disconnected` ou já `connected`). Gere um novo QR Code no painel ou com `POST /channels/{channel_id}/connect`. | | O cliente não conecta | Confirme que ele suporta MCP por **HTTP** (Streamable HTTP). Clientes que só aceitam `stdio` precisam do `mcp-remote` (veja [Claude Desktop](/mcp/claude-desktop/)). | # MCP no Claude Code > Conecte o Claude Code ao servidor MCP da TopTalking com um comando (claude mcp add --transport http), escolha o escopo e configure permissões para as ferramentas que enviam mensagens. O **Claude Code** (a ferramenta de linha de comando da Anthropic) suporta servidores MCP por HTTP nativamente. ## Instalar 1. Gere uma chave em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor). Para começar, prefira **Somente leitura**. 2. No terminal, rode (troque `tt_live_SUA_CHAVE` pela sua chave): ```bash claude mcp add --transport http toptalking https://platform.toptalking.com.br/mcp --header "Authorization: Bearer tt_live_SUA_CHAVE" ``` 3. Confira se conectou: ```bash claude mcp list ``` Dentro do Claude Code, o comando `/mcp` mostra o status e as ferramentas da `toptalking`. 4. Peça algo como: “Use a TopTalking para listar as etapas do Kanban e quantos contatos há em cada uma.” ## Escopos Por padrão o servidor fica disponível só para você, no projeto atual (`--scope local`). Outras opções: | Escopo | Comando | Onde fica | | -------------------------- | ------------------------ | ------------------------------------------ | | Só você, este projeto | `--scope local` (padrão) | Configuração local do Claude Code | | Só você, todos os projetos | `--scope user` | Configuração do seu usuário | | Todo o time, este projeto | `--scope project` | Arquivo `.mcp.json` na raiz do repositório | ```bash # Disponível em todos os seus projetos claude mcp add --transport http --scope user toptalking https://platform.toptalking.com.br/mcp --header "Authorization: Bearer tt_live_SUA_CHAVE" ``` ### Compartilhar com o time sem expor a chave O `.mcp.json` vai para o Git. **Não coloque a chave nele**: use uma variável de ambiente, que o Claude Code expande ao carregar o arquivo. .mcp.json ```json { "mcpServers": { "toptalking": { "type": "http", "url": "https://platform.toptalking.com.br/mcp", "headers": { "Authorization": "Bearer ${TOPTALKING_API_KEY}" } } } } ``` Cada pessoa define `TOPTALKING_API_KEY` na própria máquina (de preferência com a própria chave). ## Permissões O Claude Code pede confirmação antes de usar uma ferramenta MCP. Você pode liberar de vez **só as ferramentas de leitura** e continuar aprovando manualmente as que enviam mensagens: .claude/settings.json ```json { "permissions": { "allow": [ "mcp__toptalking__get_account", "mcp__toptalking__list_channels", "mcp__toptalking__search_contacts", "mcp__toptalking__get_contact", "mcp__toptalking__list_messages", "mcp__toptalking__list_stages", "mcp__toptalking__list_scheduled_messages", "mcp__toptalking__list_groups", "mcp__toptalking__list_members", "mcp__toptalking__list_ai_agents", "mcp__toptalking__list_templates", "mcp__toptalking__list_campaigns", "mcp__toptalking__get_channel_qr" ] } } ``` Cuidado Não libere automaticamente `send_message`, `schedule_message`, `update_contact`, `upsert_contact` nem `cancel_scheduled_message`. Elas agem na conta real. ## Remover ```bash claude mcp remove toptalking ``` ## Próximos passos * Quer que o Claude Code **escreva uma integração** com a API (e não só use o MCP)? Veja o guia [Claude](/ia/claude/#claude-code), com um `CLAUDE.md` pronto. * Todas as ferramentas e cuidados: [Servidor MCP](/mcp/visao-geral/). # MCP no Claude Desktop > Conecte o app Claude Desktop (Windows e macOS) ao servidor MCP da TopTalking usando o mcp-remote no claude_desktop_config.json, com o cabeçalho de autorização numa variável de ambiente. Os “conectores personalizados” do Claude Desktop exigem login por OAuth, que a TopTalking ainda não suporta. A forma de conectar é pelo arquivo de configuração, usando o [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) — uma ponte que roda na sua máquina e fala com o servidor da TopTalking por HTTP. **Requisito:** [Node.js](https://nodejs.org) 18 ou mais novo instalado (o comando `npx` precisa funcionar no terminal). ## Configurar 1. Gere uma chave em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor). Para começar, prefira **Somente leitura**. 2. No Claude Desktop, abra **Configurações → Desenvolvedor → Editar configuração**. Isso abre o arquivo `claude_desktop_config.json`: * Windows ```text %APPDATA%\Claude\claude_desktop_config.json ``` * macOS ```text ~/Library/Application Support/Claude/claude_desktop_config.json ``` 3. Cole a configuração abaixo (se o arquivo já tiver outros servidores, adicione só o bloco `toptalking` dentro de `mcpServers`) e troque `tt_live_SUA_CHAVE` pela sua chave: claude_desktop_config.json ```json { "mcpServers": { "toptalking": { "command": "npx", "args": [ "-y", "mcp-remote", "https://platform.toptalking.com.br/mcp", "--header", "Authorization:${TOPTALKING_AUTH}" ], "env": { "TOPTALKING_AUTH": "Bearer tt_live_SUA_CHAVE" } } } } ``` 4. **Feche o Claude Desktop por completo** (inclusive o ícone perto do relógio, no Windows) e abra de novo. 5. Num chat novo, clique no ícone de ferramentas: a `toptalking` deve aparecer com as ferramentas listadas. ## Por que o cabeçalho está numa variável? Repare que o argumento é `Authorization:${TOPTALKING_AUTH}` — **sem espaço** depois dos dois-pontos — e o valor `Bearer tt_live_...` fica em `env`. No Windows, argumentos com espaço (como `Authorization: Bearer tt_live_...`) costumam ser quebrados no caminho até o `npx`, e o cabeçalho chega errado. O `mcp-remote` substitui `${TOPTALKING_AUTH}` pelo valor da variável, então o espaço nunca passa pela linha de comando. Funciona igual no macOS. ## Problemas comuns | Sintoma | O que fazer | | -------------------------- | ------------------------------------------------------------------------------------------------------- | | A `toptalking` não aparece | Confira se o JSON é válido (vírgulas!) e se você fechou o app por completo antes de abrir de novo. | | Erro com `npx` | Instale o Node.js 18+ e teste `npx --version` no terminal. | | Erro 401 | Confira o valor de `TOPTALKING_AUTH`: precisa começar com `Bearer `(com espaço) seguido da chave. | | Ver os logs | No Windows: `%APPDATA%\Claude\logs`. No macOS: `~/Library/Logs/Claude`. Procure os arquivos `mcp*.log`. | Veja as ferramentas e os cuidados de segurança em [Servidor MCP](/mcp/visao-geral/). # MCP no Cursor > Conecte o Cursor ao servidor MCP da TopTalking pelo arquivo ~/.cursor/mcp.json (URL + cabeçalho Authorization) e use as ferramentas no modo Agent. O Cursor conecta direto em servidores MCP por HTTP, com cabeçalhos personalizados. ## Configurar 1. Gere uma chave em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor). Para começar, prefira **Somente leitura**. 2. Abra (ou crie) o arquivo **`~/.cursor/mcp.json`** — a configuração global, fora de qualquer repositório. No Windows, fica em `%USERPROFILE%\.cursor\mcp.json`. 3. Cole e troque `tt_live_SUA_CHAVE` pela sua chave: \~/.cursor/mcp.json ```json { "mcpServers": { "toptalking": { "url": "https://platform.toptalking.com.br/mcp", "headers": { "Authorization": "Bearer tt_live_SUA_CHAVE" } } } } ``` 4. Abra as configurações do Cursor, na seção de **MCP**, e confirme que a `toptalking` aparece ativa, com as ferramentas listadas. 5. No chat, em modo **Agent**, peça: “Use a TopTalking para listar meus canais de WhatsApp e dizer se estão conectados.” Configuração por projeto O Cursor também lê `.cursor/mcp.json` dentro do projeto. Se usar esse arquivo, **não faça commit dele com a chave** — adicione-o ao `.gitignore`. Para o seu uso pessoal, prefira o arquivo global. ## Dicas * O Cursor pede confirmação antes de rodar ferramentas. Mantenha a confirmação para `send_message`, `schedule_message` e `update_contact`. * Para o Cursor **escrever código** que integra com a API (além de usar o MCP), adicione a documentação como `@Docs` e use as regras prontas do guia [Cursor, Windsurf e Copilot](/ia/cursor/). Veja as ferramentas e os cuidados de segurança em [Servidor MCP](/mcp/visao-geral/). # MCP no VS Code e no Windsurf > Conecte o VS Code (GitHub Copilot no modo agente, com .vscode/mcp.json e chave pedida com segurança) e o Windsurf (mcp_config.json com serverUrl) ao servidor MCP da TopTalking. * VS Code (Copilot) O VS Code usa servidores MCP no **GitHub Copilot Chat, em modo agente**. Com um `inputs` do tipo senha, o VS Code **pergunta a chave na primeira vez e guarda com segurança** — o arquivo pode ir para o Git sem expor nada. 1. Gere uma chave em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor). 2. Crie o arquivo `.vscode/mcp.json` no projeto: .vscode/mcp.json ```json { "inputs": [ { "id": "toptalking-key", "type": "promptString", "password": true, "description": "Chave de API da TopTalking" } ], "servers": { "toptalking": { "type": "http", "url": "https://platform.toptalking.com.br/mcp", "headers": { "Authorization": "Bearer ${input:toptalking-key}" } } } } ``` 3. Clique em **Start** sobre o servidor no próprio arquivo (ou rode o comando **MCP: List Servers**) e cole a chave quando o VS Code pedir. 4. Abra o Copilot Chat, escolha o modo **Agent** e confira no seletor de ferramentas se as da `toptalking` estão marcadas. 5. Peça: “Liste as etapas do Kanban da TopTalking e quantos contatos há em cada uma.” * Windsurf O Windsurf (Cascade) lê os servidores MCP de um arquivo global. 1. Gere uma chave em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor). 2. Abra o arquivo `~/.codeium/windsurf/mcp_config.json` (no Windows, `%USERPROFILE%\.codeium\windsurf\mcp_config.json`) — também dá para abrir pelo painel de MCP do Cascade. 3. Cole e troque `tt_live_SUA_CHAVE` pela sua chave: \~/.codeium/windsurf/mcp_config.json ```json { "mcpServers": { "toptalking": { "serverUrl": "https://platform.toptalking.com.br/mcp", "headers": { "Authorization": "Bearer tt_live_SUA_CHAVE" } } } } ``` 4. No Cascade, atualize a lista de servidores MCP e confirme que a `toptalking` aparece com as ferramentas. 5. Peça: “Use a TopTalking para buscar o contato com telefone 5511999998888.” Veja as ferramentas e os cuidados de segurança em [Servidor MCP](/mcp/visao-geral/). Para a IA escrever código de integração, use as regras prontas em [Cursor, Windsurf e Copilot](/ia/cursor/). # Integrar com IA > Como usar a API da TopTalking com Lovable, Claude, Cursor, ChatGPT, Bolt, v0, Replit, n8n, Make e Zapier - llms.txt, OpenAPI, MCP, as regras de ouro e um bloco de contexto pronto para colar em qualquer IA. Você não precisa ser programador para integrar com a TopTalking. As ferramentas de IA (Lovable, Claude, Cursor, ChatGPT…) escrevem o código por você — desde que recebam **a documentação certa** e **as regras certas**. Esta seção entrega as duas coisas, com prompts prontos para copiar e colar. ## Como as IAs leem esta documentação | Recurso | Endereço | Para quê | | ------------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------- | | **llms-full.txt** | `https://platform.toptalking.com.br/llms-full.txt` | A documentação **inteira** em texto puro. É o que você entrega para a IA escrever código. | | **llms.txt** | `https://platform.toptalking.com.br/llms.txt` | Índice curto com as regras essenciais e links para as outras versões. | | **llms-small.txt** | `https://platform.toptalking.com.br/llms-small.txt` | Versão compacta, para IAs com pouco contexto. | | **openapi.yaml** | `https://platform.toptalking.com.br/openapi.yaml` | O contrato da API (OpenAPI 3.1). Ideal para ChatGPT Actions, Postman e geradores de código. | | **Servidor MCP** | `https://platform.toptalking.com.br/mcp` | Para a IA **usar** a sua conta (consultar, enviar), em vez de escrever código. | Mais detalhes em [llms.txt](/recursos/llms-txt/) e [OpenAPI](/recursos/openapi/). ## Escolha o seu caminho | Você usa… | Quer… | Caminho | | ------------------------- | ---------------------------------- | -------------------------------------------------------------------- | | Lovable | Um app com WhatsApp | [Guia do Lovable](/ia/lovable/) — Edge Functions + prompts prontos | | Claude Code | Código ou operar a conta | [Claude](/ia/claude/) — MCP + `CLAUDE.md` | | claude.ai | Tirar dúvidas, gerar código | [Claude](/ia/claude/#claudeai-projetos) — Projeto com a documentação | | Cursor, Windsurf, Copilot | Código no seu editor | [Cursor, Windsurf e Copilot](/ia/cursor/) — `@Docs`, regras e MCP | | ChatGPT | Um assistente que consulta a conta | [ChatGPT](/ia/chatgpt/) — GPT personalizado com Actions | | Bolt, v0, Replit | Um app full-stack | [Bolt, v0 e Replit](/ia/bolt-v0-replit/) | | n8n, Make, Zapier | Automação sem código | [n8n, Make e Zapier](/ia/n8n-make-zapier/) | ## As regras de ouro Estas regras valem para qualquer ferramenta. Os prompts desta seção já as incluem — se você escrever os seus, inclua também. 1. **A chave fica só no servidor.** Nunca no React, no HTML, no app mobile ou no chat com a IA. A API não aceita chamadas do navegador. Use Edge Function, rota de API, servidor ou n8n/Make/Zapier, com a chave em um *secret* (`TOPTALKING_API_KEY`). 2. **Toda mensagem enviada leva `Idempotency-Key`.** IAs e automações repetem chamadas; a chave de idempotência impede mensagens duplicadas no WhatsApp do cliente. 3. **Só mande mensagem para quem pediu.** Nada de disparos em massa com `POST /messages`: o WhatsApp pode banir o número. Para campanhas, use [Campanhas](/referencia/campanhas/) (WhatsApp Oficial, com aprovação). 4. **Webhooks: valide a assinatura** (`X-TopTalking-Signature`) com o corpo cru antes de confiar no evento. 5. **Para explorar, use uma chave “Somente leitura”.** Libere escrita só quando o fluxo estiver testado. 6. **Trate erros pelo `code`** e respeite o `Retry-After` em `429`. ## Bloco de contexto (cole em qualquer IA) Se a sua ferramenta não está nos guias, comece a conversa colando este bloco: ```text Contexto: vou integrar meu sistema com a API da TopTalking (WhatsApp + CRM com Kanban). Documentação completa (leia antes de escrever código): https://platform.toptalking.com.br/llms-full.txt Contrato OpenAPI 3.1: https://platform.toptalking.com.br/openapi.yaml Regras obrigatórias: - URL base: https://platform.toptalking.com.br/v1 — JSON em UTF-8. - Autenticação: cabeçalho "Authorization: Bearer ". A chave fica numa variável de ambiente/secret chamada TOPTALKING_API_KEY. - NUNCA coloque a chave em código de front-end (navegador/app) nem no código-fonte. A API bloqueia chamadas do navegador (sem CORS): toda chamada deve sair do backend (servidor, Edge Function, rota de API, automação). - Envios (POST /messages e POST /scheduled-messages) sempre com o cabeçalho Idempotency-Key, derivado do ID da operação (ex.: "pedido-1234-despachado"). - Erros vêm como { "error": { "type", "code", "message", "param", "request_id" } }. Trate pelo "code". Em 429, espere os segundos do cabeçalho Retry-After. - Listas usam paginação por cursor: { "object": "list", "data": [...], "has_more", "next_cursor" }; envie ?cursor=; limit de 1 a 100. - Telefones com DDI e DDD, só dígitos (ex.: 5511999998888). - Webhooks: valide X-TopTalking-Signature ("t=,v1="; v1 = HMAC-SHA256 em hex de "." com o secret whsec_... guardado em TOPTALKING_WEBHOOK_SECRET; tolerância de 300 s; comparação em tempo constante). Responda 2xx em até 10 s e deduplique pelo "id" do evento. - Não invente endpoints nem campos: use só o que está na documentação. - Nada de envio em massa: uma mensagem por ação do usuário/cliente. ``` ## Guias [Lovable](/ia/lovable/)Secrets, Edge Functions e 4 prompts prontos. [Claude](/ia/claude/)Claude Code, Projetos no claude.ai e Claude Desktop. [Cursor, Windsurf e Copilot](/ia/cursor/)@Docs, regras do projeto e MCP. [ChatGPT](/ia/chatgpt/)GPT personalizado com Actions e prompts para Codex. [Bolt, v0 e Replit](/ia/bolt-v0-replit/)Rotas de backend e prompts por plataforma. [n8n, Make e Zapier](/ia/n8n-make-zapier/)Configuração do nó HTTP e do gatilho de webhook. # Lovable > Passo a passo para integrar um app do Lovable com a API da TopTalking - secret TOPTALKING_API_KEY, Supabase Edge Functions como ponte, recebimento de webhooks com assinatura e prompts prontos (formulário que envia WhatsApp, painel do Kanban, webhook para tabela, lembretes). Apps do Lovable têm duas partes: o **front-end** (React, roda no navegador de quem usa o app) e o **back-end** (Lovable Cloud / Supabase, com banco e **Edge Functions**). A chave da TopTalking só pode ficar no back-end. ```text Navegador (React) → Edge Function (Supabase) → API da TopTalking sem chave lê TOPTALKING_API_KEY https://platform.toptalking.com.br/v1 ``` O erro mais comum Pedir para o Lovable “chamar a API da TopTalking” sem dizer onde faz ele colocar o `fetch` (e a chave) direto no React. Isso **não funciona** (a API bloqueia o navegador — você verá um erro de CORS) e **expõe a chave** para qualquer visitante. Os prompts abaixo já evitam isso. ## 1. Gere a chave Em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor), clique em **Criar chave**. Dê um nome como “App Lovable” e escolha: * **Somente leitura** se o app só vai consultar (painéis, relatórios); * **Acesso total** se vai enviar mensagens, criar contatos ou mover cards. ## 2. Guarde a chave como secret O nome do secret deve ser exatamente **`TOPTALKING_API_KEY`**. * **Jeito mais fácil:** mande o primeiro prompt abaixo. Quando o Lovable precisar da chave, ele mostra um **formulário seguro para adicionar o secret** — cole a chave ali, **nunca na conversa**. * **Manual:** adicione o secret nas configurações de secrets do backend do projeto (Lovable Cloud) ou, se o projeto usa uma conta Supabase própria, no painel da Supabase em **Edge Functions → Secrets**. ## 3. Prepare a ponte (Edge Function) Cole este prompt no Lovable antes de pedir qualquer funcionalidade: ```text Vou integrar este app com a API da TopTalking (WhatsApp + CRM com Kanban). Leia a documentação completa antes de escrever código: https://platform.toptalking.com.br/llms-full.txt Configure a base da integração: 1. Crie um secret chamado TOPTALKING_API_KEY (me peça para colar o valor pelo formulário seguro de secrets; não peça a chave no chat). 2. Crie um módulo compartilhado para Edge Functions em supabase/functions/_shared/toptalking.ts com: - const API = "https://platform.toptalking.com.br/v1" - uma função toptalking(path, { method, body, idempotencyKey }) que faz fetch para `${API}${path}` com os cabeçalhos "Authorization: Bearer " + Deno.env.get("TOPTALKING_API_KEY"), "Content-Type: application/json" e, se informado, "Idempotency-Key". - Se a resposta não for 2xx, o corpo é { "error": { "type", "code", "message", "param", "request_id" } }: lance um erro com status, code, message e request_id. Em 429, inclua o valor do cabeçalho Retry-After. 3. Crie a Edge Function "toptalking-status" que chama GET /me e devolve { organizacao: organization.name, acesso: api_key.access }. Ela deve exigir usuário logado (verificação de JWT do Supabase ligada) e tratar CORS (OPTIONS). 4. Adicione na tela de configurações do app um botão "Testar conexão com a TopTalking" que chama essa função com supabase.functions.invoke e mostra o nome da organização ou o erro. Regras obrigatórias para tudo o que fizermos com a TopTalking a partir de agora: - NUNCA chame https://platform.toptalking.com.br a partir do front-end (React) e NUNCA escreva a chave no código. A API bloqueia o navegador (sem CORS). Toda chamada sai de uma Edge Function usando o módulo _shared/toptalking.ts. - Não crie uma função "proxy" genérica que repassa qualquer caminho/método vindo do front-end: crie uma Edge Function por ação, validando os dados recebidos. - Envios de mensagem sempre com Idempotency-Key derivada do ID da operação. - Use só endpoints e campos que existem na documentação. ``` ## 4. Peça a funcionalidade Escolha um dos prompts prontos abaixo (ou adapte). Todos já repetem as regras de segurança — é proposital: o Lovable segue melhor quando a regra está no mesmo prompt. ### Prompt: enviar WhatsApp quando alguém preencher o formulário ```text Quero que, quando um visitante preencher o formulário de contato deste site, ele receba uma mensagem de boas-vindas no WhatsApp pela API da TopTalking e já entre no Kanban da empresa na etapa "Novo lead". Documentação da API (leia antes de codar): https://platform.toptalking.com.br/llms-full.txt - URL base: https://platform.toptalking.com.br/v1 - Autenticação: cabeçalho "Authorization: Bearer ", com a chave lida de Deno.env.get("TOPTALKING_API_KEY") (secret do Supabase). Segurança (obrigatório): - A chave NUNCA vai para o front-end nem para o código. O React chama só a nossa Edge Function; a Edge Function chama a TopTalking. - A API da TopTalking não aceita chamadas do navegador (sem CORS). Não tente chamá-la do React. O que construir: 1. Formulário com: nome, WhatsApp com DDD e uma caixa obrigatória "Aceito receber mensagens no WhatsApp". Valide o telefone no front e no back: só dígitos; com DDD são 10 ou 11 dígitos; se não começar com 55, adicione 55. 2. Tabela "leads": id (uuid), nome, telefone, consentimento (boolean), status ('pendente' | 'enviado' | 'erro'), toptalking_contact_id, toptalking_message_id, erro_codigo, erro_mensagem, created_at. RLS: visitantes não leem nem escrevem direto; só a Edge Function (service role) grava. Usuários logados podem ler. 3. Edge Function "enviar-boas-vindas" (pública, sem exigir login, com CORS liberado só para o domínio do app): a. Recebe { nome, telefone, consentimento }. Recuse se consentimento não for true ou se o telefone for inválido. b. Anti-abuso: se já existe lead com o mesmo telefone nas últimas 24 h, não envie de novo (responda sucesso sem reenviar). c. Grave o lead com status 'pendente'. d. Busque as etapas: GET /v1/stages → { data: [{ id, name, is_final, ... }] }. Pegue o id da etapa com name "Novo lead" (se não existir, siga sem etapa). e. Crie/atualize o contato: POST /v1/contacts com { "phone": telefone, "name": nome, "stage_id": }. A resposta (201 criado ou 200 já existia) traz o contato com "id". f. Envie a mensagem: POST /v1/messages com { "contact_id": , "text": "Olá, ! 👋 Recebemos seu contato e logo vamos falar com você por aqui." } e o cabeçalho "Idempotency-Key: lead--boas-vindas". A resposta 201 traz a mensagem com "id" e "status". g. Atualize o lead com status 'enviado', toptalking_contact_id e toptalking_message_id. h. Em erro, a TopTalking responde { "error": { "type", "code", "message", "request_id" } }: salve erro_codigo/erro_mensagem, status 'erro', e devolva ao front uma mensagem amigável. Exemplos de code: "channel_unavailable" (WhatsApp da empresa desconectado), "invalid_phone" (telefone inválido), "rate_limit_exceeded" (muitas requisições; respeite o cabeçalho Retry-After). 4. Depois de enviar o formulário, mostre "Pronto! Enviamos uma mensagem no seu WhatsApp." ou o erro amigável. Não faça nenhum envio em massa: exatamente uma mensagem por formulário enviado. ``` ### Prompt: painel com as conversas e as etapas do Kanban ```text Quero uma página "Atendimento" (só para usuários logados) que mostre as conversas do WhatsApp da minha empresa organizadas pelas etapas do Kanban da TopTalking, com histórico de mensagens e a opção de mover um card de etapa. Documentação da API (leia antes de codar): https://platform.toptalking.com.br/llms-full.txt - URL base: https://platform.toptalking.com.br/v1 - Autenticação: cabeçalho "Authorization: Bearer ", chave em Deno.env.get("TOPTALKING_API_KEY"). Segurança (obrigatório): - Nunca chame a TopTalking do front-end e nunca exponha a chave. Crie Edge Functions que exigem usuário logado (verificação de JWT do Supabase ligada) e chamam a TopTalking pelo servidor. - Não crie uma função genérica que repassa qualquer caminho: uma função por ação. Edge Functions: 1. "toptalking-quadro" (GET): - GET /v1/stages → lista de etapas em ordem: { data: [{ id, name, color, position, is_final }] }. - Para cada etapa: GET /v1/contacts?stage_id=&archived=false&limit=100 → { data: [contatos], has_more, next_cursor }. Campos do contato: id, name, phone, stage { id, name, is_final }, ai_enabled, unread_count, last_message_at, last_message_preview, resolved_at. - Devolva [{ etapa, contatos }]. Guarde o resultado em cache por 60 segundos (limite da API: 120 requisições/minuto). 2. "toptalking-conversa" (GET ?contact_id=): - GET /v1/contacts/{contact_id}/messages?limit=50 → mensagens da mais recente para a mais antiga (campos: id, direction "inbound"|"outbound", type, text, media { url, mime_type, filename } ou null, status, created_at). 3. "toptalking-mover" (POST { contact_id, stage_id }): - Confira que stage_id está entre os ids de GET /v1/stages (ou é null para tirar do Kanban). - PATCH /v1/contacts/{contact_id} com { "stage_id": stage_id } → devolve o contato atualizado. Front-end: - Uma coluna por etapa (nome e cor da etapa). Cards com nome (ou telefone, se não houver nome), prévia da última mensagem, horário relativo em português ("há 5 min") e um selo com unread_count quando > 0. - Arrastar e soltar entre colunas (ou menu "Mover para…") chama toptalking-mover. Atualize de forma otimista e desfaça se der erro. Se a etapa de destino tiver is_final = true, peça confirmação: "Mover para esta etapa finaliza a conversa". - Clicar no card abre um painel lateral com o histórico (toptalking-conversa), em ordem cronológica: inbound à esquerda, outbound à direita, mostrando texto, imagem/arquivo (media.url) e status de entrega. - Botão "Atualizar" para recarregar o quadro. - Trate erros pelo error.code da resposta ({ "error": { "type", "code", "message", "request_id" } }). Esta página não envia mensagens. ``` ### Prompt: receber mensagens via webhook e salvar numa tabela ```text Quero salvar no banco deste app as mensagens que os clientes mandam para o WhatsApp da empresa, recebendo os webhooks da TopTalking em uma Edge Function. Documentação (leia as seções de Webhooks, Eventos e Verificar a assinatura): https://platform.toptalking.com.br/llms-full.txt 1. Tabela "mensagens_whatsapp": id (uuid, primary key = data.message.id), evento_id (uuid, unique = id do evento), contato_id (uuid), contato_nome (text), telefone (text), direcao (text), tipo (text), texto (text), midia_url (text), recebida_em (timestamptz = data.message.created_at), payload (jsonb com o evento inteiro), created_at (default now()). RLS: só usuários logados podem ler; ninguém escreve pelo front-end. 2. Edge Function pública "toptalking-webhook": - Desligue a verificação de JWT dela em supabase/config.toml ([functions.toptalking-webhook] verify_jwt = false), porque a TopTalking não envia token do Supabase. - Leia o corpo CRU com await req.text() ANTES de qualquer JSON.parse. - Valide o cabeçalho X-TopTalking-Signature, no formato "t=,v1=": v1 = HMAC-SHA256 em hexadecimal, com chave = Deno.env.get("TOPTALKING_WEBHOOK_SECRET") (o valor inteiro, começando com whsec_) e mensagem = t + "." + corpo cru. Use Web Crypto (crypto.subtle.importKey com HMAC/SHA-256 e crypto.subtle.verify, que compara em tempo constante). Rejeite com 401 se a assinatura não bater ou se a diferença entre agora e t for maior que 300 segundos. - Só então faça JSON.parse. O evento tem o formato { id, object: "event", type, created_at, organization_id, data }. - Se type = "message.received": grave em mensagens_whatsapp usando data.message (id, direction, type, text, media?.url, created_at) e data.contact (id, name, phone), com o cliente Supabase usando a service role (SUPABASE_SERVICE_ROLE_KEY) só dentro da função. Use upsert/on conflict do nothing: o mesmo evento pode chegar mais de uma vez. - Se type = "ping" (teste do painel) ou qualquer outro tipo: apenas responda 200. - Responda 200 rapidamente (em menos de 10 segundos). Qualquer resposta fora de 2xx faz a TopTalking tentar de novo. 3. Página "Mensagens recebidas" (só usuários logados) listando a tabela, mais recentes primeiro, com atualização em tempo real. 4. No fim, me diga: - a URL da função (https://.supabase.co/functions/v1/toptalking-webhook) para eu cadastrar no painel da TopTalking em Configurações → Desenvolvedor → Webhooks → Novo webhook, assinando o evento message.received; - que preciso adicionar o secret TOPTALKING_WEBHOOK_SECRET com o "segredo de assinatura" (whsec_...) que o painel mostra. Não use a chave de API da TopTalking no front-end e não escreva nenhum segredo no código. ``` ### Prompt: lembrete automático no WhatsApp (mensagem agendada) ```text Quero que, ao criar um agendamento neste app, o cliente receba um lembrete no WhatsApp 24 horas antes, usando as mensagens agendadas da TopTalking (elas aparecem na Agenda do painel da empresa). Documentação (leia antes de codar): https://platform.toptalking.com.br/llms-full.txt - URL base: https://platform.toptalking.com.br/v1 - Autenticação: "Authorization: Bearer ", chave em Deno.env.get("TOPTALKING_API_KEY"). Nunca no front-end nem no código. 1. Na tabela de agendamentos, adicione a coluna toptalking_lembrete_id (uuid, opcional). 2. Edge Function "agendar-lembrete" (exige usuário logado), recebe { agendamento_id }: - Leia nome, telefone (com DDD; adicione 55 se faltar) e data_hora do agendamento. - Calcule send_at = data_hora − 24 horas, em ISO 8601 COM fuso (ex.: "2026-12-01T09:00:00-03:00"). Se send_at já passou, não agende (send_at precisa estar no futuro e em até 365 dias). - POST /v1/scheduled-messages com { "to": telefone, "send_at": send_at, "title": "Lembrete de agendamento", "text": "Oi {{nome}}! Passando para lembrar do seu horário amanhã às HH:MM. Qualquer coisa, é só responder aqui." } e o cabeçalho "Idempotency-Key: agendamento--lembrete-24h". Atenção: {{nome}} é uma variável da própria TopTalking — mantenha as chaves duplas literalmente no texto. Troque só HH:MM. - A resposta 201 traz { id, status: "pending", ... }. Salve o id em toptalking_lembrete_id. 3. Edge Function "cancelar-lembrete", recebe { agendamento_id }: - DELETE /v1/scheduled-messages/{toptalking_lembrete_id}. Resposta 200 = cancelado (status "canceled"). - Se vier 409 com error.code "invalid_state", o lembrete já foi enviado ou não está mais pendente: só registre. - Limpe toptalking_lembrete_id. 4. Chame "agendar-lembrete" ao criar um agendamento; ao remarcar, chame "cancelar-lembrete" e depois "agendar-lembrete" (use uma Idempotency-Key nova na remarcação, por exemplo com a nova data: agendamento--lembrete-). Ao cancelar o agendamento, chame "cancelar-lembrete". Trate erros pelo error.code da resposta ({ "error": { "type", "code", "message", "request_id" } }). ``` ### Prompt: tela para conectar um WhatsApp por QR Code ```text Quero uma página "Conectar WhatsApp" (só para usuários logados) onde eu crio uma conexão de WhatsApp na TopTalking e leio o QR Code com o celular, sem precisar abrir o painel da TopTalking. Documentação (leia antes de codar): https://platform.toptalking.com.br/llms-full.txt Passo a passo de referência: https://platform.toptalking.com.br/referencia/canais/ - URL base: https://platform.toptalking.com.br/v1 - Autenticação: "Authorization: Bearer ", chave em Deno.env.get("TOPTALKING_API_KEY"). Nunca no front-end nem no código. Edge Functions (exigem usuário logado; o front-end chama SÓ estas funções, nunca a TopTalking): 1. "whatsapp-conectar" (POST { nome }): - POST /v1/channels com { "name": nome } e o cabeçalho "Idempotency-Key: ". - Resposta 201: { object: "channel_connection", channel: { id, name, phone, status }, status, qr_code, qr_code_generated_at, pairing_code }. - Se vier 422 com error.code "limit_reached", devolva a error.message (o plano não permite mais conexões). - Devolva { id: channel.id, status, qrCode: qr_code, telefone: channel.phone }. 2. "whatsapp-status" (GET ?id=): - GET /v1/channels/{id}/qr → mesmo formato acima. Devolva { id, status, qrCode, telefone }. - Se status for "disconnected" e qr_code for null, chame POST /v1/channels/{id}/connect com {} para gerar um novo QR Code e devolva o resultado dele. Front-end: - Botão "Conectar WhatsApp" → chama whatsapp-conectar. - Enquanto status for diferente de "connected", chame whatsapp-status a cada 4 segundos. - qrCode é uma imagem PNG em data URL ("data:image/png;base64,..."): use direto em (264×264). Se qrCode vier null, mostre "Gerando o QR Code…" (é normal nos primeiros segundos). - Instruções abaixo da imagem: "No celular: WhatsApp → Aparelhos conectados → Conectar um aparelho". - Quando status for "connected", pare de consultar e mostre "WhatsApp conectado!". - Desista depois de 3 minutos sem leitura e mostre um botão "Gerar novo QR Code" (que chama whatsapp-status de novo). Trate erros pelo error.code da resposta ({ "error": { "type", "code", "message", "request_id" } }). ``` ## Receber webhooks numa Edge Function Resumo do que o prompt acima faz, para você conferir: 1. A função `toptalking-webhook` fica **pública** (`verify_jwt = false`), porque quem chama é a TopTalking, não um usuário logado. A segurança vem da **assinatura**. 2. Ela lê o corpo cru, valida `X-TopTalking-Signature` com o secret `TOPTALKING_WEBHOOK_SECRET` e só então processa. O código de referência está em [Verificar a assinatura](/webhooks/assinatura/) (aba “Deno / Supabase Edge Function”). 3. No painel da TopTalking, em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor) → **Webhooks** → **Novo webhook**, cadastre `https://.supabase.co/functions/v1/toptalking-webhook` e escolha os eventos. 4. Copie o **segredo de assinatura** (`whsec_...`) e adicione como secret `TOPTALKING_WEBHOOK_SECRET` no Lovable. 5. Clique em **Enviar teste** e veja em **Ver entregas** se a resposta foi `200`. ## Problemas comuns | Sintoma | Causa e solução | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | Erro de **CORS** no console do navegador | O app está chamando a TopTalking direto do React. Peça: “Mova a chamada para uma Edge Function; o front-end só chama a Edge Function”. | | `401 invalid_api_key` / `missing_api_key` | O secret `TOPTALKING_API_KEY` não existe, tem outro nome ou foi colado com espaços. | | `403 read_only_key` | A chave é Somente leitura. Crie outra com Acesso total para enviar ou alterar. | | `422 channel_unavailable` | O WhatsApp da empresa está desconectado. Reconecte no painel em Configurações → Conexões. | | O webhook recebe `401` da própria Supabase | A função está com verificação de JWT ligada. Desligue para `toptalking-webhook`. | | Assinatura do webhook nunca bate | A função fez `req.json()` antes de validar. Valide com `await req.text()`. | | A chave apareceu no código ou no chat | Revogue a chave no painel na hora e crie outra. | # Claude > Use a TopTalking com o Claude - Claude Code (MCP, CLAUDE.md pronto e prompt para escrever a integração), Projetos no claude.ai com o llms-full.txt como conhecimento e Claude Desktop via MCP. O Claude pode trabalhar com a TopTalking de dois jeitos: * **Operar a conta** pelo [servidor MCP](/mcp/visao-geral/): consultar conversas, mover cards, enviar mensagens (com a sua aprovação). * **Escrever código** de integração com a API REST, lendo esta documentação. ## Claude Code ### 1. Conecte o MCP ```bash claude mcp add --transport http toptalking https://platform.toptalking.com.br/mcp --header "Authorization: Bearer tt_live_SUA_CHAVE" ``` Detalhes, escopos e permissões em [MCP no Claude Code](/mcp/claude-code/). Para o Claude Code só escrever código, o MCP é opcional — mas ajuda: ele pode consultar as etapas reais do seu Kanban e testar a chave. ### 2. Adicione as regras ao `CLAUDE.md` Cole no `CLAUDE.md` do seu projeto. O Claude Code lê esse arquivo em toda sessão: CLAUDE.md ```markdown ## Integração com a TopTalking (WhatsApp + CRM) - Documentação completa: https://platform.toptalking.com.br/llms-full.txt Contrato OpenAPI 3.1: https://platform.toptalking.com.br/openapi.yaml Consulte antes de usar um endpoint. Não invente endpoints nem campos. - API REST: base `https://platform.toptalking.com.br/v1`, JSON, cabeçalho `Authorization: Bearer `. - A chave vem SEMPRE da variável de ambiente `TOPTALKING_API_KEY`. Nunca escreva a chave no código, em testes, em logs, em fixtures ou em commits. Nunca chame a API a partir de código que roda no navegador (a API não tem CORS). - Envios (`POST /messages`, `POST /scheduled-messages`) sempre com `Idempotency-Key` derivada do ID da operação (ex.: `pedido--despachado`), para retentativas não duplicarem mensagens. - Erros: `{ "error": { "type", "code", "message", "param", "request_id" } }`. Trate pelo `code`. Em 429, espere `Retry-After` segundos. Não repita erros 4xx. Registre o `request_id` nos logs. - Listas: paginação por cursor (`has_more`, `next_cursor` → `?cursor=`), `limit` de 1 a 100. - Telefones: DDI + DDD, só dígitos (ex.: `5511999998888`). - Webhooks: validar `X-TopTalking-Signature` (`t=,v1=`; v1 = HMAC-SHA256 hex de `.` com `TOPTALKING_WEBHOOK_SECRET`), tolerância de 300 s, comparação em tempo constante, responder 2xx em até 10 s, deduplicar pelo `id` do evento. - Testes automatizados nunca chamam a API real para enviar mensagens: use mocks. - Nada de envio em massa (risco de banimento do número de WhatsApp). - Ferramentas MCP `toptalking` que enviam mensagens ou alteram contatos: só use quando eu pedir explicitamente. ``` ### 3. Peça a integração ```text Leia https://platform.toptalking.com.br/llms-full.txt e as regras da TopTalking no CLAUDE.md. Crie um módulo de integração com a API da TopTalking neste projeto: 1. Um cliente pequeno (sem dependências novas, usando fetch) com: - request(method, path, { body, idempotencyKey, query }) que usa https://platform.toptalking.com.br/v1, o cabeçalho Authorization: Bearer ${process.env.TOPTALKING_API_KEY} e lança um erro tipado com status, code, message e requestId quando a resposta não for 2xx. Em 429, espere Retry-After e tente de novo até 3 vezes. - Funções: getMe(), listStages(), findContactByPhone(phone) (GET /contacts?phone=), upsertContact({ phone, name, email, stageId }), moveContact(contactId, stageId), sendMessage({ to | contactId, text, media, idempotencyKey }), listAllContacts() (percorre todas as páginas com cursor). 2. Uma rota POST /webhooks/toptalking que valida X-TopTalking-Signature com o corpo cru (TOPTALKING_WEBHOOK_SECRET, tolerância de 300 s, comparação em tempo constante), responde 200 rápido e despacha o evento por tipo (message.received, contact.stage_changed, ping), ignorando duplicados pelo id do evento. 3. Testes unitários com fetch mockado, incluindo o vetor de teste da assinatura que está na documentação. 4. Atualize o .env.example com TOPTALKING_API_KEY= e TOPTALKING_WEBHOOK_SECRET= (sem valores). Antes de escrever, mostre o plano e os arquivos que vai criar. ``` ## Claude.ai (Projetos) No claude.ai, os **Projetos** guardam arquivos como conhecimento fixo. Coloque a documentação lá e todas as conversas do projeto já sabem usar a API. 1. Abra [llms-full.txt](/llms-full.txt) no navegador e salve como arquivo (`Ctrl+S` / `Cmd+S`), por exemplo `toptalking-llms-full.txt`. 2. No claude.ai, crie um projeto (ex.: “Integração TopTalking”). 3. Em **Conhecimento do projeto**, envie o arquivo `toptalking-llms-full.txt`. 4. Em **Instruções do projeto**, cole: ```text Você me ajuda a integrar sistemas com a API da TopTalking (WhatsApp + CRM com Kanban). Use APENAS o arquivo de documentação do projeto como referência: não invente endpoints, campos ou eventos. Sempre: chamadas só pelo backend (nunca no navegador), chave em variável de ambiente TOPTALKING_API_KEY, Idempotency-Key em envios, tratamento de erros pelo "code", validação da assinatura dos webhooks. Responda em português do Brasil, com código completo e explicando onde cada arquivo fica. ``` 5. Comece uma conversa no projeto com um pedido concreto. Prompt de exemplo: ```text Tenho uma loja em Next.js (App Router) com pedidos numa tabela "orders" (id, customer_name, customer_phone, status). Quero que, quando o status mudar para "shipped", o cliente receba no WhatsApp: "Olá, ! Seu pedido # saiu para entrega." Escreva: 1. A função de servidor que envia a mensagem pela API da TopTalking (POST /messages) com Idempotency-Key "order--shipped". 2. Onde chamar essa função no fluxo de atualização do pedido. 3. O tratamento dos erros channel_unavailable, invalid_phone e rate_limit_exceeded. 4. Como configurar TOPTALKING_API_KEY na Vercel. Use a documentação do projeto como fonte. ``` ## Claude Desktop Para o Claude Desktop **operar a conta** (consultar conversas, mover cards, enviar com aprovação), conecte o MCP pelo `mcp-remote`: veja [MCP no Claude Desktop](/mcp/claude-desktop/). Você pode combinar com um Projeto: as conversas do projeto têm a documentação, e o MCP dá acesso aos dados reais. ```text Usando as ferramentas da TopTalking: liste as conversas da etapa "Negociação" que não recebem mensagem há mais de 3 dias. Para cada uma, leia as últimas 10 mensagens e sugira uma mensagem de follow-up curta e educada. NÃO envie nada: só me mostre as sugestões para eu aprovar uma por uma. ``` # Cursor, Windsurf e Copilot > Integre a TopTalking no Cursor, Windsurf e GitHub Copilot - MCP, documentação via @Docs (llms-full.txt), regras prontas (.cursor/rules, .windsurf/rules, copilot-instructions.md) e prompt para gerar a integração. Nos editores com IA, combine três coisas: 1. **Documentação** — para a IA escrever código certo (`@Docs` com o `llms-full.txt`). 2. **Regras do projeto** — para ela nunca expor a chave nem duplicar mensagens. 3. **MCP** (opcional) — para ela consultar dados reais da sua conta enquanto programa. ## 1. Adicione a documentação * Cursor 1. Abra as configurações do Cursor, na seção de **Docs** (indexação de documentação). 2. Adicione uma nova documentação com a URL `https://platform.toptalking.com.br/llms-full.txt` e o nome **TopTalking**. 3. No chat, digite `@TopTalking` (ou `@Docs` → TopTalking) para incluir a documentação no contexto. * Windsurf e Copilot Inclua o link direto no pedido — o agente consegue ler a página: ```text Leia https://platform.toptalking.com.br/llms-full.txt antes de começar. ``` Ou baixe o arquivo para dentro do projeto (ex.: `docs/toptalking-llms-full.txt`) e referencie-o no chat. ## 2. Adicione as regras do projeto O conteúdo é o mesmo nos três editores; muda só o arquivo. * Cursor .cursor/rules/toptalking.mdc ```markdown --- description: Regras para integrar com a API da TopTalking (WhatsApp, contatos, Kanban, mensagens agendadas e webhooks) alwaysApply: true --- # TopTalking - Documentação: https://platform.toptalking.com.br/llms-full.txt (use @TopTalking). OpenAPI: https://platform.toptalking.com.br/openapi.yaml - Não invente endpoints, campos ou eventos: use só o que está na documentação. - API REST: https://platform.toptalking.com.br/v1, JSON, cabeçalho `Authorization: Bearer `. - A chave vem SEMPRE de `process.env.TOPTALKING_API_KEY` (ou equivalente). Nunca no código-fonte, testes, logs ou commits. - Nunca chame a API a partir de código de navegador (componentes React/Vue client-side, `"use client"`, HTML). A API não tem CORS. Use rotas de API/Server Actions/Edge Functions. - Envios (`POST /messages`, `POST /scheduled-messages`) sempre com `Idempotency-Key` derivada do ID da operação. - Erros: `{ "error": { "type", "code", "message", "param", "request_id" } }`. Trate pelo `code`; em 429 respeite `Retry-After`. - Listas: cursor (`has_more`, `next_cursor` → `?cursor=`), `limit` até 100. - Telefones: DDI + DDD, só dígitos (ex.: `5511999998888`). - Webhooks: validar `X-TopTalking-Signature` (`t=,v1=`, HMAC-SHA256 hex de `.` com `TOPTALKING_WEBHOOK_SECRET`), tolerância 300 s, comparação em tempo constante, 2xx em até 10 s, deduplicar pelo `id`. - Testes nunca enviam mensagens reais: use mocks. - Nada de envio em massa pelo WhatsApp. ``` * Windsurf Crie um arquivo de regra do workspace em `.windsurf/rules/` (ex.: `.windsurf/rules/toptalking.md`) com o mesmo conteúdo da aba Cursor (sem o bloco `---` do topo) e configure a ativação como **sempre ativa** (Always On) nas regras do Cascade. * GitHub Copilot .github/copilot-instructions.md ```markdown # TopTalking - Documentação: https://platform.toptalking.com.br/llms-full.txt. OpenAPI: https://platform.toptalking.com.br/openapi.yaml - Não invente endpoints, campos ou eventos. - API REST: https://platform.toptalking.com.br/v1, cabeçalho `Authorization: Bearer `. - Chave só em `TOPTALKING_API_KEY` (variável de ambiente). Nunca no código nem no navegador (a API não tem CORS). - `Idempotency-Key` em `POST /messages` e `POST /scheduled-messages`. - Erros pelo `code`; 429 → `Retry-After`. - Paginação por cursor (`next_cursor`). - Webhooks: validar `X-TopTalking-Signature` (HMAC-SHA256 de `.` com `TOPTALKING_WEBHOOK_SECRET`). - Testes com mocks; nada de envio em massa. ``` ## 3. Conecte o MCP (opcional) Com o MCP, o agente pode, por exemplo, buscar os IDs reais das etapas do seu Kanban enquanto escreve o código. * Cursor: [MCP no Cursor](/mcp/cursor/) * VS Code (Copilot) e Windsurf: [MCP no VS Code e Windsurf](/mcp/vscode-windsurf/) Use uma chave **Somente leitura** para o MCP no editor, a menos que você queira que o agente envie mensagens. ## 4. Peça a integração ```text @TopTalking Leia a documentação da TopTalking e siga as regras do projeto (.cursor/rules/toptalking.mdc). Neste projeto Next.js (App Router), crie a integração com a API da TopTalking: 1. lib/toptalking.ts (só servidor: adicione import "server-only"): - função request(method, path, { body, query, idempotencyKey }) com base https://platform.toptalking.com.br/v1, Authorization: Bearer process.env.TOPTALKING_API_KEY, Content-Type: application/json. - erro tipado TopTalkingError { status, code, message, requestId } a partir de { error: { type, code, message, request_id } }. - em 429, esperar Retry-After e tentar de novo até 3 vezes. - funções: sendMessage({ to?, contactId?, text?, media?, idempotencyKey }), upsertContact({ phone, name?, email?, stageId? }), listStages(), moveContact(contactId, stageId | null), listContacts({ search?, stageId?, cursor?, limit? }). 2. app/api/whatsapp/send/route.ts (POST): exige usuário logado, valida { phone, text } com zod e chama sendMessage com Idempotency-Key recebida do cliente (crypto.randomUUID() gerado uma vez por envio no front). 3. app/api/webhooks/toptalking/route.ts (POST): lê o corpo cru com await req.text(), valida X-TopTalking-Signature (TOPTALKING_WEBHOOK_SECRET, 300 s, comparação em tempo constante), responde 200 e trata message.received e contact.stage_changed; ignora duplicados pelo id do evento. Garanta runtime "nodejs". 4. Testes com fetch mockado e o vetor de teste da assinatura da documentação. 5. .env.example com TOPTALKING_API_KEY= e TOPTALKING_WEBHOOK_SECRET=. Nenhum componente client-side pode importar lib/toptalking.ts. ``` # ChatGPT > Crie um GPT personalizado com Actions que acessa a sua conta da TopTalking importando o OpenAPI (https://platform.toptalking.com.br/openapi.yaml) com autenticação Bearer, e use prompts prontos no ChatGPT e no Codex para escrever integrações. Os conectores do ChatGPT exigem login por OAuth, que a TopTalking ainda não suporta. O caminho é um **GPT personalizado com Actions**: o ChatGPT lê o contrato OpenAPI da TopTalking e passa a chamar a API com a sua chave. ## GPT personalizado com Actions Quem usa o GPT usa a sua chave A chave fica salva no GPT. **Qualquer pessoa com acesso ao GPT age na sua conta.** Deixe o GPT como **só eu** (ou compartilhe só com quem pode), use uma chave **Somente leitura** sempre que possível e uma chave exclusiva para o GPT, que você pode revogar a qualquer momento. 1. Gere uma chave em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor) com o nome “ChatGPT” (comece com **Somente leitura**). 2. No ChatGPT, vá em **GPTs → Criar** e abra a aba **Configurar**. (É preciso um plano que permita criar GPTs.) 3. Preencha nome (ex.: “Atendimento TopTalking”), descrição e cole as **instruções** abaixo. 4. Em **Ações**, clique em **Criar nova ação**. 5. Em **Autenticação**, escolha **Chave de API**, tipo **Bearer**, e cole a chave `tt_live_...`. 6. Em **Esquema**, use **Importar de URL** com: ```text https://platform.toptalking.com.br/openapi.yaml ``` As ações aparecem listadas (getMe, listContacts, sendMessage…). 7. Clique em **Testar** na ação `getMe`: a resposta deve trazer o nome da sua organização. 8. Salve o GPT com visibilidade **Somente eu**. Instruções do GPT: ```text Você é o assistente de atendimento da minha empresa e acessa a conta da TopTalking (WhatsApp + CRM com Kanban) pelas ações da API. Regras: - Responda em português do Brasil, de forma objetiva. - Antes de qualquer ação que ENVIA mensagem (sendMessage, createScheduledMessage) ou ALTERA dados (upsertContact, updateContact, cancelScheduledMessage, createWebhook, updateWebhook, deleteWebhook), mostre exatamente o que vai fazer (destinatário, texto, horário) e só execute depois que eu disser "confirmo". - Nunca envie a mesma mensagem para várias pessoas de uma vez. Nada de envio em massa. - Ao enviar mensagem, sempre informe o cabeçalho Idempotency-Key com um valor único para aquele envio. - Para mover um card no Kanban: busque as etapas com listStages e use updateContact com stage_id. Avise que mover para a etapa com is_final = true finaliza a conversa. - Para datas (send_at), use ISO 8601 com o fuso de Brasília (-03:00). - Se uma ação falhar, explique o error.code e o error.message em linguagem simples. - Nunca mostre a chave de API. ``` Exemplos de uso no GPT: ```text Quais conversas estão na etapa "Negociação"? Mostre nome, telefone e a prévia da última mensagem. ``` ```text Leia o histórico da conversa com o telefone 5511999998888 e resuma o que o cliente pediu. ``` ### Problemas comuns | Sintoma | O que fazer | | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A ação responde 401 | Confira se a autenticação é **Chave de API → Bearer** e se a chave está certa (sem espaços). | | A ação de envio responde 403 `read_only_key` | A chave é Somente leitura. Crie outra com Acesso total se quiser permitir envios. | | O importador mostra avisos sobre o esquema | Avisos não impedem o uso. Se a importação falhar, abra `https://platform.toptalking.com.br/openapi.json`, copie o conteúdo e cole no campo de esquema. | | Erro de CORS ou “Failed to fetch” | Não acontece em Actions (a chamada sai dos servidores da OpenAI). Se aparecer, é porque o código está rodando no navegador — veja [Autenticação](/comecando/autenticacao/#a-chave-nunca-vai-para-o-navegador). | ## ChatGPT para escrever código No chat normal (sem GPT), entregue a documentação e as regras no começo: ```text Vou integrar meu sistema com a API da TopTalking (WhatsApp + CRM). Use como referência a documentação em https://platform.toptalking.com.br/llms-full.txt e o contrato https://platform.toptalking.com.br/openapi.yaml. Se não conseguir abrir os links, me avise que eu colo o conteúdo. Regras: chamadas só pelo backend (a API não aceita chamadas do navegador), chave na variável de ambiente TOPTALKING_API_KEY (nunca no código), cabeçalho Authorization: Bearer, Idempotency-Key em todo envio de mensagem, tratamento de erros pelo campo error.code, paginação por cursor e validação da assinatura X-TopTalking-Signature nos webhooks. Não invente endpoints nem campos. O que eu preciso: [descreva aqui — ex.: "uma rota em Express que envia a confirmação do pedido pelo WhatsApp quando o pagamento é aprovado"]. ``` ## Codex O Codex lê o arquivo `AGENTS.md` do repositório. Adicione as regras da TopTalking nele — o conteúdo é o mesmo do [`CLAUDE.md` do guia do Claude](/ia/claude/#2-adicione-as-regras-ao-claudemd) — e peça a tarefa: ```text Leia https://platform.toptalking.com.br/llms-full.txt e as regras da TopTalking no AGENTS.md. Implemente o envio de confirmação de pedido pelo WhatsApp: - Quando um pedido mudar para status "paid", envie POST https://platform.toptalking.com.br/v1/messages com { "to": , "text": "Olá, ! Recebemos o pagamento do pedido #. Obrigado!" } e Idempotency-Key "order--paid". - A chave vem de TOPTALKING_API_KEY. Trate os erros pelo error.code (channel_unavailable, invalid_phone, rate_limit_exceeded com Retry-After) e registre o request_id. - Falha no WhatsApp não pode impedir a mudança de status do pedido: registre e siga. - Escreva testes com a chamada HTTP mockada. ``` # Bolt, v0 e Replit > Como integrar a API da TopTalking em apps criados no Bolt, no v0 (Next.js na Vercel) e no Replit - onde guardar a chave, o padrão de rota de backend e prompts prontos para cada plataforma. Em todas essas plataformas o padrão é o mesmo: **o front-end chama uma rota do seu backend, e só o backend chama a TopTalking**, com a chave guardada como variável de ambiente. ```text Navegador → rota do seu backend (/api/...) → https://platform.toptalking.com.br/v1 sem chave lê TOPTALKING_API_KEY ``` ## O padrão de rota Um exemplo de rota de backend em Next.js (o mesmo raciocínio vale para Express, Flask, Edge Functions…): app/api/whatsapp/boas-vindas/route.ts ```ts import { NextResponse } from 'next/server'; const API = 'https://platform.toptalking.com.br/v1'; export async function POST(req: Request) { const { nome, telefone, leadId } = await req.json(); const digitos = String(telefone ?? '').replace(/\D/g, ''); if (digitos.length < 10 || !nome || !leadId) { return NextResponse.json({ erro: 'Dados inválidos' }, { status: 400 }); } const res = await fetch(`${API}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.TOPTALKING_API_KEY}`, // só existe no servidor 'Content-Type': 'application/json', 'Idempotency-Key': `lead-${leadId}-boas-vindas`, }, body: JSON.stringify({ to: digitos, contact_name: nome, text: `Olá, ${nome}! Recebemos seu contato e logo falamos com você por aqui.`, }), }); const body = await res.json(); if (!res.ok) { // body.error = { type, code, message, request_id } return NextResponse.json({ erro: body.error.message, codigo: body.error.code }, { status: 502 }); } return NextResponse.json({ ok: true, mensagemId: body.id }); } ``` Atenção Variáveis com prefixo público (`NEXT_PUBLIC_`, `VITE_`, `PUBLIC_`, `EXPO_PUBLIC_`) vão parar no navegador. **Nunca** coloque a chave da TopTalking numa delas. ## Por plataforma * Bolt Projetos do Bolt costumam ser front-end (Vite + React). Para ter um backend, use a **integração com Supabase** do Bolt (Edge Functions) ou um framework com rotas de servidor (ex.: Next.js, Remix, Astro com SSR). * Guarde a chave como `TOPTALKING_API_KEY` nos secrets do backend escolhido (Supabase: Edge Functions → Secrets). * Se usar Supabase, os prompts do [guia do Lovable](/ia/lovable/) funcionam quase sem mudança. ```text Quero integrar este projeto com a API da TopTalking (WhatsApp + CRM). Documentação (leia antes de codar): https://platform.toptalking.com.br/llms-full.txt URL base: https://platform.toptalking.com.br/v1 — cabeçalho "Authorization: Bearer ". Regras obrigatórias: - A API NÃO aceita chamadas do navegador (sem CORS) e a chave NUNCA pode ir para o front-end. Crie o backend com Supabase Edge Functions (conecte o Supabase se ainda não estiver conectado). A chave fica no secret TOPTALKING_API_KEY e é lida com Deno.env.get("TOPTALKING_API_KEY"). - Nada de variável VITE_ com a chave. Nada de chave no código. - Envio de mensagem sempre com o cabeçalho Idempotency-Key. Tarefa: na página de checkout, depois que o pedido for criado, chame uma Edge Function "confirmar-pedido-whatsapp" que envia POST /v1/messages com { "to": , "contact_name": , "text": "Olá, ! Recebemos seu pedido #. Avisaremos por aqui quando ele sair para entrega." } e Idempotency-Key "pedido--confirmado". Trate erros pelo error.code ({ "error": { "type", "code", "message" } }) e não bloqueie a finalização do pedido se o WhatsApp falhar. ``` * v0 (Next.js) O v0 gera apps Next.js que rodam na Vercel. Use **Route Handlers** (`app/api/.../route.ts`) ou **Server Actions**. * Adicione `TOPTALKING_API_KEY` nas variáveis de ambiente do projeto (no v0, na área de variáveis do projeto; na Vercel, em **Settings → Environment Variables**). * Não use `NEXT_PUBLIC_`. ```text Integre este app Next.js com a API da TopTalking (WhatsApp + CRM com Kanban). Documentação (leia antes de codar): https://platform.toptalking.com.br/llms-full.txt URL base: https://platform.toptalking.com.br/v1 — cabeçalho "Authorization: Bearer ${process.env.TOPTALKING_API_KEY}". Regras obrigatórias: - Chame a TopTalking SOMENTE em código de servidor (Route Handlers em app/api ou Server Actions). Nunca em componentes "use client". A API não aceita chamadas do navegador. Coloque import "server-only" no módulo da TopTalking. - A chave vem da variável de ambiente TOPTALKING_API_KEY (sem NEXT_PUBLIC_). Me peça para adicioná-la. - Envios com o cabeçalho Idempotency-Key. Tarefa: crie uma página /leads com um formulário (nome, WhatsApp, consentimento obrigatório) que envia para app/api/leads/route.ts. A rota: 1. valida os dados (zod) e normaliza o telefone para só dígitos com 55; 2. busca as etapas (GET /v1/stages) e pega o id da etapa "Novo lead"; 3. cria/atualiza o contato com POST /v1/contacts { phone, name, stage_id }; 4. envia a mensagem de boas-vindas com POST /v1/messages { contact_id, text } e Idempotency-Key "lead--boas-vindas"; 5. responde { ok: true } ou a mensagem de erro amigável a partir de error.code. ``` * Replit No Replit, crie um backend (Node com Express ou Python com Flask/FastAPI) e guarde a chave na ferramenta **Secrets** do Replit, com o nome `TOPTALKING_API_KEY`. No código, ela aparece como variável de ambiente (`process.env.TOPTALKING_API_KEY` / `os.environ["TOPTALKING_API_KEY"]`). ```text Crie um backend em Node.js + Express integrado à API da TopTalking (WhatsApp + CRM). Documentação (leia antes de codar): https://platform.toptalking.com.br/llms-full.txt URL base: https://platform.toptalking.com.br/v1 — cabeçalho "Authorization: Bearer ". Regras obrigatórias: - A chave fica no Secret TOPTALKING_API_KEY (me peça para criar). Nunca no código e nunca enviada ao front-end. - O front-end (se houver) chama só as rotas deste backend; a API da TopTalking não aceita chamadas do navegador. - Envio de mensagem sempre com Idempotency-Key. Tarefas: 1. POST /api/avisar-entrega { pedidoId, nome, telefone }: envia POST /v1/messages com { "to": telefone, "contact_name": nome, "text": "Olá, ! Seu pedido # saiu para entrega. 🚚" } e Idempotency-Key "pedido--saiu". Proteja a rota com um token próprio (cabeçalho X-Admin-Token comparado com o Secret ADMIN_TOKEN). 2. POST /webhooks/toptalking: usa express.raw({ type: "application/json" }) para ler o corpo cru, valida X-TopTalking-Signature ("t=,v1="; HMAC-SHA256 hex de "." com o Secret TOPTALKING_WEBHOOK_SECRET; tolerância de 300 s; crypto.timingSafeEqual), responde 200 e registra no console os eventos message.received. Ignore eventos repetidos pelo id. 3. Mostre a URL pública do webhook para eu cadastrar no painel da TopTalking (Configurações → Desenvolvedor → Webhooks). ``` # n8n, Make e Zapier > Configure o nó HTTP do n8n, o módulo HTTP do Make e o Webhooks by Zapier para chamar a API da TopTalking (cabeçalhos, corpo JSON, Idempotency-Key) e para receber os webhooks com segurança. Ferramentas de automação rodam no servidor delas — então a chave fica protegida lá, nas **credenciais** da ferramenta. Você usa dois blocos: * **Chamar a API** (enviar mensagem, criar contato, mover card): um nó/módulo **HTTP**. * **Receber eventos** (mensagem recebida, etapa alterada): um gatilho de **webhook**. ## Chamar a API Os dados são sempre os mesmos: | Campo | Valor | | ------------------ | -------------------------------------------------------------------------------------------------------------- | | Método | `POST` (enviar), `GET` (consultar), `PATCH` (atualizar) | | URL | `https://platform.toptalking.com.br/v1/messages` (ou outro endpoint da [referência](/referencia/visao-geral/)) | | Cabeçalho | `Authorization: Bearer tt_live_...` | | Cabeçalho | `Content-Type: application/json` | | Cabeçalho (envios) | `Idempotency-Key: ` | | Corpo | JSON | Exemplo de corpo para enviar mensagem: ```json { "to": "5511999998888", "contact_name": "Maria Souza", "text": "Olá, Maria! Recebemos seu pedido #1234. Obrigado pela compra!" } ``` * n8n 1. Adicione um nó **HTTP Request**. 2. **Method:** `POST`. **URL:** `https://platform.toptalking.com.br/v1/messages`. 3. **Authentication:** *Generic Credential Type* → *Header Auth*. Crie a credencial com **Name** `Authorization` e **Value** `Bearer tt_live_SUA_CHAVE`. (Assim a chave fica guardada nas credenciais, e não visível no fluxo.) 4. **Send Headers:** ligado, com `Idempotency-Key` = uma expressão única por operação, por exemplo: ```text {{ 'pedido-' + $json.pedido_id + '-confirmado' }} ``` 5. **Send Body:** ligado, **Body Content Type** `JSON`, **Specify Body** *Using JSON* (com o campo em modo **Expression** para os `{{ }}` funcionarem): ```json { "to": "{{ $json.telefone }}", "contact_name": "{{ $json.nome }}", "text": "Olá, {{ $json.nome }}! Recebemos seu pedido #{{ $json.pedido_id }}." } ``` 6. Em **Settings** do nó, ative **Retry On Fail** se quiser retentativas — a `Idempotency-Key` garante que não haverá mensagens duplicadas. Para listas grandes, use a opção **Pagination** do nó com o parâmetro `cursor` recebendo `{{ $response.body.next_cursor }}` e pare quando `has_more` for `false`. * Make 1. Adicione o módulo **HTTP → Make a request**. 2. **URL:** `https://platform.toptalking.com.br/v1/messages`. **Method:** `POST`. 3. **Headers:** * `Authorization` = `Bearer tt_live_SUA_CHAVE` * `Idempotency-Key` = por exemplo `pedido-{{1.id}}-confirmado` (mapeado do módulo anterior) 4. **Body type:** *Raw*. **Content type:** *JSON (application/json)*. 5. **Request content:** o JSON da mensagem, mapeando os campos dos módulos anteriores. 6. **Parse response:** *Yes* — assim você pode usar `id`, `status` e, em erro, `error.code` nos próximos módulos. Adicione uma rota de erro (*error handler*) para tratar respostas `4xx/5xx` pelo `error.code`. * Zapier 1. Adicione a ação **Webhooks by Zapier → Custom Request**. 2. **Method:** `POST`. **URL:** `https://platform.toptalking.com.br/v1/messages`. 3. **Data:** o JSON da mensagem (mapeando os campos dos passos anteriores). 4. **Headers:** * `Authorization` = `Bearer tt_live_SUA_CHAVE` * `Content-Type` = `application/json` * `Idempotency-Key` = um valor único da operação (ex.: `pedido-` + ID do pedido + `-confirmado`) 5. Teste o passo e confira a resposta `201` com o `id` da mensagem. Automação não é disparo em massa Um fluxo que roda para uma lista inteira de contatos é um disparo em massa — e o WhatsApp pode banir o número. Use automações para mensagens **transacionais** (uma por evento: pedido, cadastro, agendamento). Para campanhas, use o recurso de Campanhas do painel. Veja [Boas práticas no WhatsApp](/comecando/boas-praticas-whatsapp/). ## Receber webhooks 1. Crie o gatilho de webhook na ferramenta e copie a **URL de produção** dele: * n8n: nó **Webhook**, método `POST`. * Make: módulo **Webhooks → Custom webhook**. * Zapier: gatilho **Webhooks by Zapier → Catch Raw Hook** (mantém o corpo cru e os cabeçalhos). 2. No painel da TopTalking, em [Configurações → Desenvolvedor](https://toptalking.com.br/configuracoes/desenvolvedor) → **Webhooks** → **Novo webhook**, cole a URL e escolha os eventos. 3. Clique em **Enviar teste**: a ferramenta deve receber um evento `ping`. Use-o para mapear os campos (`type`, `data.message.text`, `data.contact.phone`…). 4. No fluxo, filtre pelo campo `type` (ex.: só `message.received`) e ignore o `ping`. ### Segurança dos webhooks nas automações A URL do gatilho é pública: qualquer um que a descubra pode mandar eventos falsos. Escolha uma das proteções: **Opção 1 — validar a assinatura (recomendado quando a ferramenta tem o corpo cru e um passo de código).** A regra é a de [Verificar a assinatura](/webhooks/assinatura/): `v1` = HMAC-SHA256 em hex de `.` com o segredo `whsec_...`. No **n8n**, ative a opção **Raw Body** do nó Webhook e adicione um nó **Code** (JavaScript) logo depois: n8n — nó Code (Run Once for All Items) ```js const crypto = require('crypto'); const SECRET = 'whsec_SEU_SEGREDO'; // segredo de assinatura do webhook na TopTalking const item = $input.first(); const cabecalho = item.json.headers['x-toptalking-signature'] || ''; // Corpo cru (opção "Raw Body" do nó Webhook). const corpo = (await this.helpers.getBinaryDataBuffer(0, 'data')).toString('utf8'); const partes = Object.fromEntries( cabecalho.split(',').map((p) => { const i = p.indexOf('='); return [p.slice(0, i).trim(), p.slice(i + 1).trim()]; }), ); const t = partes.t; const v1 = partes.v1 || ''; const esperado = Buffer.from( crypto.createHmac('sha256', SECRET).update(`${t}.${corpo}`).digest('hex'), ); const recebido = Buffer.from(v1); const recente = Math.abs(Date.now() / 1000 - Number(t)) <= 300; const valido = recente && recebido.length === esperado.length && crypto.timingSafeEqual(recebido, esperado); if (!valido) throw new Error('Assinatura inválida: evento ignorado.'); return [{ json: JSON.parse(corpo) }]; ``` Se o seu n8n bloquear o `require('crypto')`, libere o módulo com a variável de ambiente `NODE_FUNCTION_ALLOW_BUILTIN=crypto` (n8n auto-hospedado). No **Zapier**, o *Catch Raw Hook* entrega o corpo cru e os cabeçalhos; dá para validar num passo **Code by Zapier** (JavaScript) com a mesma lógica acima. **Opção 2 — tratar o webhook só como aviso e confirmar na API.** Quando não der para validar a assinatura (comum no Make), não confie no conteúdo do evento: use apenas o ID e busque o dado real na API com a sua chave. Um evento falso não terá um ID válido na sua conta. | Evento | Confirme com | | ------------------------------------------------------------- | -------------------------------------------------------- | | `message.received`, `message.sent`, `message.status_updated` | `GET /v1/messages/{data.message.id}` | | `contact.created`, `contact.updated`, `contact.stage_changed` | `GET /v1/contacts/{data.contact.id}` | | `scheduled_message.sent`, `scheduled_message.failed` | `GET /v1/scheduled-messages/{data.scheduled_message.id}` | | `channel.status_updated` | `GET /v1/channels/{data.channel.id}` | | `campaign.status_updated` | `GET /v1/campaigns/{data.campaign.id}` | Em qualquer opção: responda rápido (a ferramenta normalmente já responde `200` sozinha), e deduplique pelo `id` do evento — a mesma entrega pode chegar mais de uma vez. Veja [Entregas e novas tentativas](/webhooks/entregas/). # llms.txt > A documentação da TopTalking em texto puro para IAs - /llms.txt (índice), /llms-full.txt (completa), /llms-small.txt (compacta) e os recortes por assunto - e como usar cada uma no Lovable, Claude, Cursor e ChatGPT. O [llms.txt](https://llmstxt.org/) é um padrão para entregar documentação a modelos de linguagem em texto puro, sem menus, scripts nem HTML. A TopTalking publica estes arquivos, gerados automaticamente a partir deste site (sempre atualizados): | Arquivo | Conteúdo | Quando usar | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | Índice curto: o que é a API, as regras essenciais (URL base, autenticação, chave só no servidor…) e links para os outros arquivos. | Para a IA “descobrir” a documentação. | | [`/llms-full.txt`](/llms-full.txt) | **A documentação inteira**: guias, referência completa da API, webhooks, MCP e os prompts prontos. | O padrão para a IA escrever código. | | [`/llms-small.txt`](/llms-small.txt) | Versão compacta (sem dicas e notas secundárias, com espaços reduzidos). | IAs com pouco contexto ou quando o arquivo completo não cabe. | ## Recortes por assunto Se você só precisa de uma parte, use os recortes (menores e mais focados): | Arquivo | Conteúdo | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | [`/_llms-txt/api-rest.txt`](/_llms-txt/api-rest.txt) | Começando (autenticação, erros, limites, paginação, idempotência) + referência completa dos endpoints. | | [`/_llms-txt/webhooks.txt`](/_llms-txt/webhooks.txt) | Webhooks: eventos, payloads, assinatura, entregas e a gestão pela API. | | [`/_llms-txt/servidor-mcp.txt`](/_llms-txt/servidor-mcp.txt) | Servidor MCP e a configuração de cada cliente. | | [`/_llms-txt/guias-de-ia.txt`](/_llms-txt/guias-de-ia.txt) | Guias e prompts para Lovable, Claude, Cursor, ChatGPT, Bolt, v0, Replit, n8n, Make e Zapier. | ## Como usar em cada ferramenta | Ferramenta | Como entregar a documentação | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **Lovable, Bolt, v0, Replit** | Cole o link `https://platform.toptalking.com.br/llms-full.txt` no prompt (os prompts prontos já fazem isso). | | **Claude Code, Codex** | Link no `CLAUDE.md`/`AGENTS.md` e no pedido; o agente baixa o arquivo quando precisa. | | **claude.ai (Projetos)** | Baixe o `llms-full.txt` e envie como conhecimento do projeto. Veja [Claude](/ia/claude/#claudeai-projetos). | | **Cursor** | Adicione `https://platform.toptalking.com.br/llms-full.txt` como documentação e use `@TopTalking`. Veja [Cursor](/ia/cursor/). | | **ChatGPT** | Cole o link (ou o conteúdo) no começo da conversa. Para acessar a conta, use um GPT com Actions — veja [ChatGPT](/ia/chatgpt/). | | **Qualquer outra** | Use o [bloco de contexto](/ia/visao-geral/#bloco-de-contexto-cole-em-qualquer-ia) e o link do `llms-full.txt`. | ## Para quem constrói ferramentas * Os arquivos são texto puro em UTF-8 e podem ser lidos de qualquer origem (`Access-Control-Allow-Origin: *`). * O contrato da API em formato estruturado está em [OpenAPI](/recursos/openapi/). # OpenAPI > O contrato OpenAPI 3.1 da API da TopTalking em YAML e JSON - como importar no Postman, Insomnia e Bruno, gerar tipos e clientes e usar no ChatGPT (GPT com Actions). O contrato completo da API está publicado em **OpenAPI 3.1**, com todos os endpoints, schemas, códigos de erro e os payloads dos webhooks (na seção `webhooks`): | Formato | URL | | ------- | ------------------------------------------------------------------ | | YAML | [`https://platform.toptalking.com.br/openapi.yaml`](/openapi.yaml) | | JSON | [`https://platform.toptalking.com.br/openapi.json`](/openapi.json) | Os dois arquivos têm o mesmo conteúdo e podem ser lidos de qualquer origem (`Access-Control-Allow-Origin: *`). ## Importar em clientes HTTP | Ferramenta | Como | | ------------ | -------------------------------------------------------------------------------------------------------------------------- | | **Postman** | **Import** → cole a URL do `openapi.yaml`. Depois, na coleção, configure **Authorization → Bearer Token** com a sua chave. | | **Insomnia** | **Import** → **URL** → cole a URL do `openapi.yaml`. Configure a autenticação Bearer no ambiente. | | **Bruno** | **Import Collection** → **OpenAPI V3 Spec** → arquivo baixado do `openapi.yaml`. | Guarde a chave numa variável de ambiente da ferramenta, não dentro das requisições salvas, principalmente se a coleção for compartilhada. ## Gerar tipos e clientes Tipos TypeScript a partir do contrato, com [openapi-typescript](https://www.npmjs.com/package/openapi-typescript): ```bash npx openapi-typescript https://platform.toptalking.com.br/openapi.yaml -o src/toptalking-api.d.ts ``` ```ts import type { components } from './toptalking-api'; type Contato = components['schemas']['Contact']; type Mensagem = components['schemas']['Message']; ``` Para outras linguagens, use o [OpenAPI Generator](https://openapi-generator.tech/) com a mesma URL. Lembre-se: o cliente gerado deve rodar **no servidor**, nunca no navegador. ## Usar com IA * **ChatGPT:** importe a URL do `openapi.yaml` nas Actions de um GPT personalizado. Passo a passo em [ChatGPT](/ia/chatgpt/). * **Outras IAs:** o `openapi.yaml` é ótimo para a IA conferir nomes de campos e enums. Para explicações e exemplos, combine com o [`llms-full.txt`](/recursos/llms-txt/). ## Convenções do contrato * `servers`: `https://platform.toptalking.com.br/v1`. * Segurança: `bearerAuth` (`Authorization: Bearer tt_live_...`). O cabeçalho `X-Api-Key` também é aceito. * Cada operação tem um `operationId` estável (ex.: `sendMessage`, `listContacts`, `updateContact`). * Os eventos de webhook estão em `webhooks` (ex.: `message.received`), com o envelope `EventEnvelope`.