n8n-nodes-zarkchat
v0.10.0
Published
Nodes do ZarkChat para o n8n: CRM, contatos, conversas, envio de mensagem no WhatsApp, base de conhecimento e gatilho por webhook assinado.
Maintainers
Readme
n8n-nodes-zarkchat
Nodes do ZarkChat para o n8n: CRM, conversas, envio de mensagem no WhatsApp, base de conhecimento e gatilho por webhook assinado.
Dois nodes e duas credenciais:
| Item | Para quê |
| ------------------------ | ------------------------------------------------------------------- |
| ZarkChat | 10 resources, 43 operações. Também usável como ferramenta de Agent. |
| ZarkChat Trigger | Recebe os webhooks de saída, com validação de assinatura HMAC. |
| ZarkChat API | Credencial da api-key (zk_live_...) e da URL base. |
| ZarkChat Webhook API | Credencial do segredo HMAC de um webhook. |
Versões
0.10.0 (2026-09-23)
Pedido do integrador do Tantalo, para a Onda 3 (pedido no painel) e a Onda 4 (baixa pelo painel).
- Recurso Order (
orders:read/orders:write): Create, Get, Get Many, Update, Set Status, Set Payment e Cancel. O Create tem o campo Idempotency Key: use uma chave ESTÁVEL por pedido, e repetir a chamada devolve o mesmo pedido. - Set Status e Set Payment devolvem 409 e 422 como item, não como erro. 409 é "outra mão já mudou" (
ORDER_STATUS_RACE/ORDER_PAYMENT_RACE) e 422 é "essa transição não existe". O item trazstatusCodee oerrorda API (error.code,error.details.esperado,error.details.atual), para o workflow distinguir "o outro já fez" de "a API caiu". - Gatilho com os eventos que faltavam:
order.created,order.status_changed,order.payment_changed,number.silentealert.triggered. A lista passa a ser conferida por teste contra o schema que validaPOST /v1/webhooks, então não envelhece mais em silêncio. - Knowledge Base: Create Document e Update Document (
kb:write). Fecha o ciclo "o humano respondeu, o agente aprende". Apagar continua fora da api-key. - Correção: campo JSON digitado na tela. Medido num n8n de verdade (2.34.5): campo do tipo JSON digitado na tela chega como TEXTO, e a API recusava ("expected array, received string"). Valia para Update Config, as variáveis do Flow, os blocos do Typing Plan, os parâmetros de template e o interativo da mensagem. Agora texto vira objeto antes de sair; quem já passava por expressão não muda nada.
Sem quebra: nenhuma operação existente mudou de contrato. Instale a mesma versão no editor, nos workers E no processo de webhook.
0.9.1 (2026-09-16)
Discard Rehearsal Conversation (agent, escopo agents:write), o DESCARTE.
Esta operação foi descrita na seção 0.9.0 abaixo e não estava no 0.9.0
publicado. O erro foi meu e é simples de contar: ela entrou no código depois
que a versão já tinha sido marcada como 0.9.0, e eu não subi o número. Como
versão publicada no npm é imutável, o código ficou sem como chegar ao editor, e
o integrador do Tantalo travou exatamente nisso, tentando rodar os roteiros 3 e 4. A rota DELETE /v1/agents/{id}/ensaio sempre esteve no ar; o que faltava era
o node.
O que ela faz: joga fora a conversa de ensaio e o que ela gerou, para o próximo ensaio começar limpo. É o que torna possível testar ESCALADA mais de uma vez. Quando o agente escala, o bot fica desligado naquela conversa, e como a conversa de ensaio é determinística por agente, sem o descarte o ambiente de teste queima na primeira escalada. Só age em conversa marcada como teste.
Sem quebra: nenhuma operação existente mudou de contrato.
0.9.0 (2026-09-16)
Send Rehearsal Message e Ensure Rehearsal Conversation
(agent, escopo agents:write), o ENSAIO.
A rota existe desde o M17 e o node não a expunha, então quem quisesse exercitar
o agente em volume não tinha por onde: chamar api.zark.chat por HTTP Request é
proibido no projeto, e a tela manda uma mensagem por vez. O integrador do
Tantalo bateu nisso tentando produzir os roteiros de teste que nós mesmos
pedimos.
Send Rehearsal Message injeta uma mensagem como se o CLIENTE tivesse mandado,
pelo mesmo caminho de ingestão de uma real, e nada vai para o WhatsApp: o
message.received dispara de verdade e acorda o workflow, o fluxo roda inteiro
e responde chamando o envio, e só o último pulo é engolido porque a conversa é
marcada is_test pelo SERVIDOR. É assim que se testa ponta a ponta sem gastar
(nem derrubar) o número do cliente.
A conversa alvo é sempre a de ensaio derivada do agentId, e o corpo só aceita
texto: não há como apontar para conversa de cliente.
Ensure Rehearsal Conversation cria (ou reusa) essa conversa e devolve
conversationId e waId, para abrir no painel e acompanhar as respostas
chegando pela tela normal. O waId é derivado do agentId, então é estável
entre execuções e pode entrar na lista branca do modo teste.
Sem quebra: nenhuma operação existente mudou de contrato.
0.8.0 (2026-09-07)
Três operações que fecham o caminho de confirmação do cardápio pelo próprio
WhatsApp, sem depender de o dono abrir o painel. O Tantalo manda o cardápio
às 9h e abre às 10h: se a confirmação exigisse a tela, o restaurante abria sem
cardápio. Agora o humano confirma respondendo /confirmo no WhatsApp, e o
workflow aplica pela API. A confirmação humana continua existindo, só que no
lugar onde o dono já está.
agent: getMenuItems(agents:read) — itens e preços do cardápio de HOJE, estruturados, para o agente montar marmita sem parsear texto. Ver detalhe na tabela abaixo.agent: getPendingMenuProposal(agents:read) — a proposta pendente, ounull. É como o workflow descobre oidda proposta quando o dono responde/confirmonuma execução diferente daquela que criou a proposta; traz também odiffdo que vai mudar.agent: applyMenuProposal(agents:write) — a operação que de fato muda o cardápio. Só deve ser disparada por uma confirmação humana, nunca por decisão do agente: é a garantia inteira do desenho. Descartar proposta não foi exposta de propósito, porque é decisão de painel.
Sem quebra: nenhuma operação existente mudou de contrato.
0.7.0 (2026-09-06)
agent: createMenuProposal e agent: getMenuText, o caminho novo do
"cardápio editável por item" (M15.4). Antes, um workflow que recebia o
cardápio do dia por WhatsApp gravava direto em config.itens via
Update Config, sem revisão nenhuma. Agora ele manda o texto cru para
Create Menu Proposal, que cria uma PROPOSTA pendente: o cardápio só muda
depois que um humano confirmar no painel. Get Menu Text devolve a mensagem
pronta (sem item pausado, sem item de outro dia) para o agente responder ao
cliente. Sem quebra: nenhuma operação existente mudou de contrato.
0.6.0 — 2026-08-31
Três entregas que estavam no código e nunca tinham sido publicadas. Quem
estava na 0.5.0 não tinha nenhuma delas, mesmo com a API já pronta dos dois
lados — foi o que motivou o job de publicação automática por tag (ver abaixo).
conversation: setBot— liga/desliga o bot de uma conversa. É o outro lado dobotEnabledque ogetjá devolvia, e fecha o handoff para humano: sem ele, quando o agente escala a conversa fica num limbo, com o bot ligado no painel e ninguém avisado. Escopoconversations:bot, ESTREITO: não muda status, atribuição nem equipe.conversation: setPresenceeconversation: typingPlan— presença simulada ("digitando..."/"gravando...") e o cálculo do tempo plausível de digitação. Existiam desde o M14.15.6 e nunca chegaram ao npm.agent: getConfigcom as guardas —knowledgeBases(todas as bases, não só a principal),somenteDaBaseeacoesSensiveis.
0.5.0
Idempotency-Key no envio de mensagem. Sem ele o envio por api-key estava
100% quebrado.
Instalação
No n8n auto-hospedado, em Settings → Community Nodes → Install, informe:
n8n-nodes-zarkchatPara testar antes de publicar, com o pacote local:
cd packages/n8n-nodes-zarkchat
npm run build
npm link
cd ~/.n8n/nodes && npm link n8n-nodes-zarkchatReinicie o n8n. Os nodes aparecem buscando por "ZarkChat".
Credenciais
ZarkChat API
Crie a chave no painel em Configurações → API. Ela aparece uma única vez.
| Campo | Valor |
| -------- | ------------------------------------------ |
| API Key | zk_live_... |
| Base URL | https://api.zark.chat (sem barra no fim) |
Instalação própria do ZarkChat? Troque a Base URL pela raiz da sua API, sem o
/v1no fim (as rotas já o incluem). O padrão aponta para a instância hospedada pela ZARK.
O botão Test chama
GET /v1/conversations/counts, então a chave precisa do escopoconversations:readpara o teste passar. Uma chave sem esse escopo pode estar perfeitamente válida e ainda assim falhar no teste.
Escopos por resource:
| Resource | Escopo necessário |
| ------------------------------------- | ------------------------------------------------------------------------- |
| Agent | agents:read / agents:write |
| Contact, Label, Lead Source, Pipeline | contacts:read / contacts:write / contacts:merge / contacts:export |
| Conversation | conversations:read, messages:read, messages:send |
| Flow | flows:trigger |
| Knowledge Base | kb:search |
| Media | messages:read |
| Message | messages:send / messages:read |
Dê só o que o workflow usa: se a chave vazar, o estrago fica contido.
ZarkChat Webhook API
O segredo aparece quando você cria o webhook no painel. Guarde na hora: ele não é exibido de novo.
Gatilho: o cadastro é manual, e é de propósito
O ZarkChat Trigger não se registra sozinho no ZarkChat. A rota que cria webhook (POST /v1/webhooks) exige login de admin e não aceita api-key, então o node não teria como fazer isso com a credencial que possui. Em vez de falhar de forma confusa na ativação, ele mostra a URL para você colar.
Passo a passo:
- Adicione o ZarkChat Trigger ao workflow e copie a Production URL.
- No painel do ZarkChat, vá em WhatsApp → Webhooks, cole a URL e assine os eventos.
- Copie o secret exibido na criação e guarde na credencial ZarkChat Webhook API.
- Marque os mesmos eventos no node.
- Use o botão Testar do painel para ver o primeiro evento chegando.
Deixe Validate Signature ligado. Sem isso, qualquer pessoa que descubra a URL do seu webhook injeta evento falso no seu agente. O node valida sobre o corpo cru, exatamente como a API assina.
Respondendo no corpo (desde 0.2.0)
O campo Respond controla quando e como o gatilho responde:
| Opção | Quando usar |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Immediately (padrão) | Responde assim que o evento chega, antes do workflow rodar. Comportamento de sempre; quem já tem workflow publicado não muda sozinho. |
| When Last Node Finishes | Espera o workflow terminar e devolve o JSON do último node no corpo. É o que o Chat de Teste do painel precisa: monte a resposta como {"reply": "..."} no último node do fluxo. |
| Using 'Respond to Webhook' Node | Controle total (status, corpo, headers) via um node Respond to Webhook em qualquer ponto do fluxo. |
O padrão nunca muda sozinho: instalações existentes continuam em Immediately sem precisar reconfigurar nada.
Evento que chega e não está na lista selecionada é descartado com 200, e não com erro: responder erro faria o ZarkChat repetir a entrega (30s, 2min, 10min, 30min, 2h) de algo que este fluxo ignora de propósito.
Referência: as 49 operações
Extraída do código. Cada linha é uma opção real do campo Operation, na ordem em que aparece na UI.
Agent
| Operação | Escopo | O que faz |
| ----------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Apply Menu Proposal | agents:write | Aplica a proposta pendente ao cardápio de verdade. É a operação que muda o que o cliente recebe, e só deve ser disparada por confirmação humana. Ver abaixo. |
| Create Menu Proposal | agents:write | Manda o texto cru do cardápio (colado ou do WhatsApp) e cria uma PROPOSTA pendente. NÃO grava o cardápio: um humano precisa confirmar. Ver abaixo. |
| Get Config | agents:read | Configuração daquele cliente e a base vinculada. É o que permite um único workflow atender todos os tenants. |
| Get Menu Items | agents:read | Itens e preços do cardápio de HOJE, estruturados, para o agente montar marmita sem parsear texto. Ver abaixo. |
| Get Menu Text | agents:read | Mensagem de cardápio pronta para enviar, já gerada a partir dos itens, sem item pausado e sem item de outro dia. |
| Get Pending Menu Proposal | agents:read | A proposta de cardápio pendente, ou null. Traz também o diff do que vai mudar. Ver abaixo. |
| Update Config | agents:write | Grava chaves na config do agente (v1.14.0). Ver "Update Config: merge raso, não reescrita" logo abaixo. |
Campo (Get Config): Agent ID.
Campo (Get Menu Text): Agent ID.
Campo (Get Menu Items): Agent ID.
Campo (Get Pending Menu Proposal): Agent ID.
Campos (Create Menu Proposal): Agent ID + Origin (Pasted/WhatsApp) + Menu Text (até 20000 caracteres).
Campos (Apply Menu Proposal): Agent ID + Proposal ID.
Campos (Update Config): Agent ID + Config (JSON).
Create Menu Proposal: não confunda com Update Config
Create Menu Proposal chama POST /v1/agents/{id}/cardapio/propostas com {"origem": "...", "texto": "..."}. Ela não grava o cardápio: o texto cru vai para uma IA que interpreta os itens e monta uma PROPOSTA pendente, guardada à parte. O cardápio que o cliente recebe (via Get Menu Text) só muda depois que um humano revisar e aplicar a proposta no painel. Um workflow de WhatsApp usa sempre Origin = WhatsApp; Pasted existe para quando o texto vem colado por outro canal.
Isso é diferente de gravar config.itens direto pelo Update Config: aquele caminho grava na hora, sem revisão. Prefira Create Menu Proposal sempre que o texto do cardápio vier solto (mensagem de WhatsApp, colagem), justamente porque ele passa por uma conferência antes de valer.
Mudou em 2026-08-14: a
configvem agrupada por seção, e não mais como mapa plano.config.business_hoursvirouconfig.dados_negocio.business_hours. As chaves de primeiro nível são as seções do molde, então variam por nicho; seção do tipo lista vem como array. Dado exato (horário, PIX, cardápio do dia, itens) vem daqui, nunca da base de conhecimento.
Confirmar o cardápio pelo WhatsApp: Get Menu Items, Get Pending Menu Proposal e Apply Menu Proposal
O caso real que motivou as três (Tantalo): o dono manda o cardápio às 9h, o restaurante abre às 10h. Exigir que ele abra o painel para confirmar deixava o cardápio pendente e o restaurante abria sem cardápio. A solução é confirmar pelo próprio WhatsApp: o agente responde um resumo da proposta, o dono responde /confirmo, e o workflow aplica pela API. O humano continua confirmando, só que no lugar onde ele já está.
Get Menu Items (GET /v1/agents/{id}/cardapio/itens) devolve { itens, precos } estruturados, o cardápio de hoje item a item, para o agente montar marmita sem parsear texto. Três detalhes que já confundiram integrador:
- Item de outro dia não vem. Item pausado vem, marcado com
disponivel: false, para o agente poder dizer "acabou" e oferecer alternativa em vez de fingir que o item não existe. disponiveleaceita_proteina_premiumvêm sempre preenchidos, nunca ausentes: não é preciso conhecer a semântica de ausência para acertar.tipoé a composição do prato (guarnicao,carne,bebida,sobremesa) egrupoé a categoria de venda (Marmitex,Bebidas). São coisas diferentes.
Get Pending Menu Proposal (GET /v1/agents/{id}/cardapio/propostas/pendente) devolve a proposta pendente, ou null. É como o workflow descobre o id da proposta quando o dono responde /confirmo numa execução diferente daquela que criou a proposta (Create Menu Proposal já devolve o id na hora, mas a confirmação chega numa mensagem seguinte, workflow novo). A resposta traz também o diff (o que entra, sai, muda de preço, volta a ser vendido, ambíguo), para o agente confirmar com o dono o que vai mudar antes de aplicar.
Apply Menu Proposal (POST /v1/agents/{id}/cardapio/propostas/{propostaId}/aplicar, campo Proposal ID obrigatório) é a operação que muda o cardápio de verdade. Ela só deve ser disparada por uma confirmação humana explícita, nunca por decisão do agente: é a garantia inteira do desenho. Se o agente aplicar sozinho, a confirmação humana deixa de existir e o recurso vira só mais um passo automático. Se a API responder 409, a proposta já foi aplicada ou descartada por outra pessoa (outro atendente, o painel); releia com Get Pending Menu Proposal antes de tentar de novo, em vez de repetir a chamada.
Descartar proposta não é exposta neste pacote, de propósito. Descartar é decisão de painel: um agente que descarta a proposta por engano apaga o trabalho que o dono fez de manhã, sem chance de desfazer pelo WhatsApp.
Update Config: merge raso, não reescrita
Update Config chama PATCH /v1/agents/{id}/config com o corpo {"config": {...}}, e o merge acontece raso, só no nível raiz de config. Isso quer dizer: cada chave que você manda substitui a mesma chave lá na API, e todas as outras chaves continuam exatamente como estavam, sem precisar reenviá-las.
Exemplo: o dono de um restaurante manda o cardápio do dia por WhatsApp de manhã, enquanto cozinha. Um workflow interpreta a mensagem e grava direto na config do agente, em vez de guardar num banco próprio:
{ "config": { "itens": [{ "nome": "Feijoada", "preco": 32 }] } }Isso troca só a chave itens. A chave precos (ou qualquer outra seção da config) não é tocada, mesmo que este corpo não a mencione.
O detalhe que engana: o merge é raso só até o primeiro nível. Se a seção que você está atualizando tem mais de um campo (dados_negocio, por exemplo, reúne business_hours e outros), mandar só {"config": {"dados_negocio": {"business_hours": "..."}}} substitui a seção dados_negocio inteira, apagando os campos dela que não vieram no corpo. Nesse caso, releia a seção com Get Config, monte o objeto completo dela com o campo alterado, e mande a seção inteira de volta.
Onde isso importa: o dado mora onde um restaurante novo encontra de graça (a config do agente, que já existe), e ninguém técnico precisa cadastrar nada num sistema à parte.
Contact
| Operação | Escopo | Campos |
| ---------------------- | ----------------- | ------------------------------------------------------------------------ |
| Get Many | contacts:read | Filters: Number ID, Label ID, Status, Search, Cursor |
| Get | contacts:read | Contact ID |
| Create | contacts:write | Number ID, WhatsApp Number, Name |
| Update | contacts:write | Contact ID + Update Fields (nome, opt out, status, origem, indicado por) |
| Move To Stage | contacts:write | Contact ID, Stage ID |
| Get Stage History | contacts:read | Contact ID |
| Add Label | contacts:write | Contact ID, Label ID |
| Remove Label | contacts:write | Contact ID, Label ID |
| Bulk Update Labels | contacts:write | Contact IDs (vírgula), Label ID, Action (assign/remove) |
| Set Custom Field | contacts:write | Contact ID, Field Key, Value |
| Merge | contacts:merge | Contact ID, Other Contact ID |
| Get Stats | contacts:read | — |
| Export | contacts:export | Number ID |
stageId não entra no Update: mudança de estágio é auditada e tem operação própria. A API recusa com 422.
Conversation
| Operação | Escopo | Campos |
| ------------------- | -------------------- | ----------------------------------------------------------------------------------------- |
| Get Many | conversations:read | Filters: Status, Assigned, Number ID, Contact ID, Search, Cursor, Limit |
| Get | conversations:read | Conversation ID. Traz botEnabled. |
| Get Counts | conversations:read | Number ID |
| Get Insight | conversations:read | Conversation ID. Temperatura, estágio e score prontos. |
| Get Messages | messages:read | Conversation ID + Cursor, Limit |
| Send Message | messages:send | Conversation ID + corpo (Text, Media, Template, Interactive) |
| Set Presence | presence:write | Conversation ID + State (Composing, Recording, Paused) + Duration (Ms) |
| Set Bot | conversations:bot | Conversation ID + Bot Enabled. Rota estreita: não muda status, atribuição nem equipe. |
| Get Typing Plan | presence:write | Blocks (lista de textos) + Mode (Auto, Typed, Pasted, None) + Budget (Ms) |
Novo em 2026-08-31 —
Set Bot: é o outro lado dobotEnabledque oGetjá devolve, e fecha o handoff para humano. Sem ele, quando o agente escala a conversa fica num limbo: bot ligado no painel, ninguém avisado, cliente falando sozinho — e o silêncio, que é a resposta certa, vira indistinguível de sistema quebrado. O escopo é ESTREITO de propósito (conversations:bot, nãoconversations:write): o integrador não precisa poder mudar status, atribuição e equipe só para calar o robô. Pedido pelo integrador do Tantalo, com essas palavras.
Mudou em 2026-08-14: cada item de Get Messages já traz
media(URL assinada 15 min,mime,sizeBytes,filename,expiresAt) quando a mensagem tem anexo, sem precisar de umMedia: Getpor mensagem.mediaAssetIdcontinua no item por retrocompat.Message: Get(mensagem avulsa) não ganhou esse campo: ainda devolve sómediaAssetId, useMedia: Getnesse caso, ou quando a URL de uma mensagem já listada tiver expirado.
Message
| Operação | Escopo | Campos |
| -------- | --------------- | -------------------------------------------------------------------- |
| Send | messages:send | Number ID, To (E.164 sem +) + corpo. Não exige conversa existente. |
| Get | messages:read | Message ID |
Knowledge Base
| Operação | Escopo | Campos |
| ------------------- | ----------- | ---------------------------------------------------------------------- |
| Create Document | kb:write | Knowledge Base ID, Title, Content. Cria documento de texto e vetoriza. |
| Update Document | kb:write | Knowledge Base ID, Document ID, e Title e/ou Content |
| Search | kb:search | Knowledge Base ID, Query, Top K (1 a 20) |
Devolve os trechos com a origem, para o agente citar a fonte.
Order
| Operação | Escopo | O que faz |
| --------------- | -------------- | ------------------------------------------------------------------------------------------------------- |
| Cancel | orders:write | Cancela o pedido; o motivo é obrigatório e fica na auditoria. |
| Create | orders:write | Cria o pedido (itens em centavos). Idempotency Key estável por pedido. |
| Get | orders:read | Um pedido pelo id. |
| Get Many | orders:read | Filtros: status, faixa de data, busca, conferencia=pendente (lista do fim do dia) e quadro=hoje. |
| Set Payment | orders:write | Status do pagamento, forma, troco, comprovante. Idempotente. 409 volta como item. |
| Set Status | orders:write | Move de coluna. 422 (transição inválida) e 409 (outra mão já moveu) voltam como item, com statusCode. |
| Update | orders:write | Cliente, endereço, taxa, observação. Nunca status nem pagamento. |
A conferência do pagamento pelo dono não está aqui: ela é só de pessoa, no painel.
Flow
| Operação | Escopo | Campos |
| ----------- | --------------- | ------------------------------------------------------------- |
| Trigger | flows:trigger | Flow ID, Target (Contact ID ou Number + Phone), Variables |
Media
| Operação | Escopo | Campos |
| -------- | --------------- | -------------- |
| Get | messages:read | Media Asset ID |
Metadados e URL assinada (15 minutos) de um anexo recebido. O mime autoritativo vem daqui, não da mensagem: é o caminho para baixar áudio, imagem e documento a partir do mediaAssetId que a mensagem entrega. Desde 2026-08-14, Conversation: Get Messages já traz esse mesmo media embutido em cada item; use esta operação separada só para Message: Get (não resolve media) ou quando a URL assinada de uma mensagem já listada expirou. Upload continua fechado para api-key, de propósito: POST /v1/media é rota do painel.
Pipeline
| Operação | Escopo | Campos |
| -------------- | --------------- | ----------------------------- |
| Get Board | contacts:read | Filters (+ Per Column) |
| Get Column | contacts:read | Stage ID + Filters (+ Cursor) |
| Get Stages | contacts:read | — |
| Get Stats | contacts:read | Filters |
Filters, comuns aos três: Number ID, Label ID, Status, Search, Source ID.
Use Get Stages para descobrir o stageId que Contact: Move To Stage exige.
Label e Lead Source
| Resource | Operação | Escopo |
| --------------- | ------------ | --------------- |
| Label | Get Many | contacts:read |
| Lead Source | Get Many | contacts:read |
Corpo de mensagem
Send Message e Send compartilham a mesma união, discriminada por Message Type:
| Tipo | Campos | | --------------- | -------------------------------------------------------------- | | Text | Text (1 a 4096 caracteres) | | Media | Media Kind, Media Source (Asset ID ou URL) + Caption, Filename | | Template | Template ID ou Name + Language, Parameters | | Interactive | Payload cru do WhatsApp (botões, listas) |
A API aceita
media.voice: true(Media Kind = Audio) para marcar o áudio como nota de voz (ptt) em vez de arquivo comum, mas este campo ainda não tem UI no node (só Caption/Filename em Media Options). Para enviar nota de voz hoje, monte o corpo com um nó HTTP genérico até sair uma versão do pacote com o campo.
Esqueleto de um agente de atendimento
Fluxo mínimo que respeita as regras da casa:
ZarkChat Trigger (message.received, Validate Signature ligado)
│
├─ ZarkChat Conversation: Get ← checa botEnabled ANTES de tudo
│ └─ IF botEnabled = false → PARA. Um humano assumiu.
│
├─ ZarkChat Agent: Get Config ← como este cliente quer ser atendido
├─ ZarkChat Conversation: Get Insight ← temperatura e estágio, já calculados
├─ ZarkChat Conversation: Get Messages ← só as últimas, não a conversa toda
│ └─ item já traz `media` (URL assinada) quando tem anexo; `Media: Get` só se precisar reobter depois de expirado
├─ ZarkChat Knowledge Base: Search ← trechos relevantes, com a fonte
│
├─ (seu modelo de IA monta a resposta)
│
└─ IF confiança baixa OU cliente pediu humano?
├─ sim → não envia. O coach do painel sugere ao atendente.
└─ não → ZarkChat Conversation: Send MessageOs IDs que o trigger entrega em data: conversationId, contactId, messageId, waId. O knowledgeBaseId vem da config do agente.
Economia que vale: Agent: Get Config muda raramente, então cachear corta ~1 chamada de cada 7.
Limites
120 requisições por minuto por api-key. Envios têm um teto extra de 10 por segundo. Toda resposta traz X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset, então dá para se auto-regular sem bater no limite.
Um agente que gasta ~7 chamadas por mensagem sustenta cerca de 17 mensagens por minuto. Se precisar de mais, cachear Agent: Get Config corta boa parte das chamadas, porque essa configuração muda raramente.
O node trata 429 sozinho (desde 0.2.0)
Ao bater no limite, a API responde 429 com o cabeçalho Retry-After. O node ZarkChat lê esse cabeçalho e espera o tempo exato antes de tentar de novo, em vez de aplicar um intervalo fixo (a retentativa nativa do n8n, em Settings do node, não lê cabeçalho de resposta). Teto de 4 tentativas (a chamada original + até 3 reenvios): se ainda assim continuar limitado, o erro final deixa claro que foi ritmo, não defeito, para não confundir com um bug do workflow. Qualquer outro erro (400, 500, etc.) propaga direto, sem essa retentativa.
Por que só 49 operações, se a API tem 142 rotas
As outras 102 são do painel e exigem login de usuário com papel. Uma api-key receberia 403 em todas elas, sempre. Expor essas rotas aqui só produziria nodes que falham, então a fronteira do pacote é exatamente o que uma chave alcança. Há um teste que quebra o build se alguma operação sair dessa fronteira.
Dois cuidados que evitam problema real
Não responda por cima de um atendente. O ZarkChat pausa o bot quando um humano assume, mas o seu workflow é externo e não sabe disso sozinho. Antes de enviar, use Conversation: Get e confira botEnabled.
Silêncio é resposta válida. Sem base para responder, não responda. Inventar resposta em nome do cliente custa mais caro que demorar.
Desenvolvimento
npm run build # tsc + copia ícones e .node.json para dist
npm run type-check
npm run lint # eslint-plugin-n8n-nodes-base, as convenções oficiais
npm testTestar num n8n de verdade antes de publicar
npm run build
mkdir -p /tmp/n8n-lab/.n8n/nodes/node_modules
cp -r . /tmp/n8n-lab/.n8n/nodes/node_modules/n8n-nodes-zarkchat
rm -rf /tmp/n8n-lab/.n8n/nodes/node_modules/n8n-nodes-zarkchat/node_modules
N8N_USER_FOLDER=/tmp/n8n-lab N8N_PORT=5679 \
N8N_COMMUNITY_PACKAGES_ENABLED=true npx n8n startCom N8N_USER_FOLDER=X, o n8n cria X/.n8n/ e procura os nodes em X/.n8n/nodes/, não em X/nodes/. Errar esse caminho faz o n8n subir normalmente e simplesmente ignorar o pacote, sem erro nenhum no log.
Publicar
npm run build
npm pack --dry-run # confira que dist, icons, README e LICENSE estão no tarball
npm login
npm publish # publishConfig já força --access publicDepois de publicado, quem usa n8n auto-hospedado instala em Settings → Community Nodes → Install digitando n8n-nodes-zarkchat.
Em n8n com EXECUTIONS_MODE=queue, o pacote precisa estar no editor, no webhook e nos workers: um node presente só no editor aparece na tela e falha na execução, porque quem executa é o worker.
Versão: semver. O npm só permite despublicar uma versão nas primeiras 72 horas, então prefira publicar um patch novo a tentar apagar.
MUDANÇA DE CONTRATO (M14.13) — leia antes de escrever knowledgeBase
agent: getConfig passou a devolver todas as bases do agente. O campo
antigo continua existindo, e a diferença importa:
{
"agentId": "...",
"knowledgeBase": { "id": "...", "name": "Cardápio" }, // PRINCIPAL, compat
"knowledgeBases": [
// TODAS (M14.13)
{ "id": "...", "name": "Cardápio" },
{ "id": "...", "name": "Política de troca" },
{ "id": "...", "name": "Perguntas frequentes" },
],
"somenteDaBase": false,
"acoesSensiveis": ["cancelar_pedido"],
"enabled": true,
}O sintoma de ler o campo errado: o agente responde com um terço do
material e o cliente conclui que a base está incompleta. Um workflow que faz
knowledgeBase?.id continua funcionando, mas só consulta a base principal —
foi exatamente o que encontramos no workflow do Tantalo.
O correto é iterar knowledgeBases e buscar em cada uma:
// RUIM (só a principal)
const kbId = $('Buscar - Config do Agente').first().json.knowledgeBase?.id ?? '';
// BOM (todas)
const bases = $('Buscar - Config do Agente').first().json.knowledgeBases ?? [];knowledgeBase (singular) é sempre o primeiro item de knowledgeBases, e
é a mesma base de sempre: workflow antigo não passa a ler outra por acidente.
As duas guardas novas
| Campo | Quem respeita | Se ignorar |
| ---------------- | -------------- | --------------------------------------------------------------- |
| somenteDaBase | o workflow | o agente improvisa onde o cliente pediu para ele não improvisar |
| acoesSensiveis | o workflow | o agente cancela pedido e dá desconto sem ninguém confirmar |
Nenhuma das duas bloqueia nada sozinha. A resposta não é gerada no
ZarkChat: cadastrar aqui só declara a intenção. Com somenteDaBase: true, o
prompt precisa mandar dizer "não sei" quando a busca não trouxer nada — em vez
de improvisar. Com acoesSensiveis preenchido, a tool correspondente precisa
pedir confirmação humana antes de executar.
Presença: "digitando..." de verdade (M14.15)
Escopo presence:write, separado de messages:send: quem só sinaliza não
deve poder enviar.
Não implemente o cálculo de tempo no workflow. POST /v1/typing-plan
recebe os blocos e devolve typingMs e pausaMs de cada um, já com piso, teto
e orçamento da resposta inteira. O Wait fixo de 2 segundos que a maioria dos
workflows usa faz um "ok" e um cardápio de 900 caracteres levarem o mesmo
tempo — e é isso que denuncia o robô.
Sequência por bloco:
typing-plan (uma vez, com todos os blocos)
→ por bloco: presence(composing, durationMs) → wait(typingMs)
→ checar botEnabled → sendMessage → wait(pausaMs)
→ ao cair no silêncio/handoff: presence(paused)O último passo não é enfeite: cliente olhando "digitando..." esperando resposta que nunca vem é pior que silêncio limpo, porque promete resposta imediata.
presence nunca devolve erro por falta de capacidade: a resposta é sempre
ok: true com applied dizendo se o sinal saiu. No número oficial da Meta,
applied vem false — o indicador de lá é preso ao recibo de leitura.
Papel gestor e o que api-key NÃO alcança
Existe um papel novo entre admin e atendente: gestor. Ele faz tudo que o
atendente faz e enxerga compliance só das equipes dele.
Todas as rotas de compliance recusam api-key (403). O ApiScopeGuard é
allowlist: rota sem escopo declarado não aceita zk_live_*. Automação de
terceiro não define o que a empresa monitora nem lê trecho de conversa. Se o
seu workflow precisa de algo de lá, peça ao time — não contorne com JWT de
usuário.
Licença
MIT
