Anil CRM

Guia de Integração

Conecte seu sistema ao Anil CRM: leia e crie contatos, acompanhe conversas, envie mensagens, aplique etiquetas e receba eventos em tempo real por webhook.

Base https://api.msgflowapp.com.br

1. Criar a chave de API

  1. Entre no Anil CRM e vá em Configurações → Chaves de API.
  2. Clique em Nova chave, dê um nome que diga para que ela serve (ERP produção, integração teste) e marque só os escopos que a integração realmente precisa.
  3. Copie a chave agora. Ela aparece uma única vez e não pode ser recuperada depois — nem por quem a criou. Se perder, revogue e crie outra.
Só o dono ou um administrador pode emitir chave. A chave alcança tudo o que o administrador dono dela alcança, e é pessoal: se essa pessoa for desativada ou rebaixada, a chave para de funcionar na mesma hora. Prefira uma chave por integração, para poder revogar uma sem derrubar as outras.
Restrição por canal. Ao criar a chave você pode limitá-la a canais específicos. Uma chave restrita não enxerga nem age em conversas de outros canais — elas respondem 404, como se não existissem.

2. Autenticar

Mande a chave no cabeçalho Authorization, em toda requisição.

curl https://api.msgflowapp.com.br/v1/me \
  -H "Authorization: Bearer anil_live_xxxxxxxx_sua_chave_aqui"

A resposta confirma a credencial e mostra o alcance dela:

{
  "tenantId": "...",
  "tenant": {                  // a organização desta chave, por extenso
    "id": "...",
    "name": "Difafá Cosméticos",
    "slug": "difafa-cosmeticos"
  },
  "key": { "id": "...", "name": "ERP produção", "prefix": "anil_live_xxxxxxxx" },
  "scopes": ["contacts:read", "messages:send"],
  "channelIds": [],            // vazio = todos os canais
  "owner": { "id": "...", "role": "ADMIN" }
}

Chame este endpoint primeiro, sempre. Ele é a resposta rápida para "por que deu 401?" e para "esta chave é da empresa certa?".

Fixar a organização esperada (opcional)

Se você administra mais de uma empresa, um risco real é usar a chave de uma no ambiente de outra — e a requisição funcionar, agindo no lugar errado sem nenhum sinal. Para fechar isso, mande junto o cabeçalho X-Anil-Org com o slug da organização que você espera:

curl https://api.msgflowapp.com.br/v1/me   -H "Authorization: Bearer SUA_CHAVE"   -H "X-Anil-Org: difafa-cosmeticos"

Se a chave for de outra empresa, a requisição para com 403 e a mensagem diz quais são as duas. O slug sai do slug em /v1/me.

O cabeçalho só sabe RECUSAR. Ele nunca escolhe a empresa — quem define a organização é a chave, e só ela. Mandar um slug diferente não dá acesso a lugar nenhum: derruba a requisição. É por isso que usá-lo é seguro em produção, e é também por isso que ele não substitui ter a chave certa.

É opcional e compatível: integração que não manda o cabeçalho continua funcionando igual. Vale a pena quando o mesmo código roda contra empresas diferentes, ou entre ambientes de teste e produção.

Nunca coloque a chave na URL. Query string vaza em log de proxy, histórico de navegador e no cabeçalho Referer. A chave só é aceita no cabeçalho Authorization.
/v1 é contrato; /api não é. O prefixo /api é o backend da nossa própria tela e muda junto com ela, sem aviso. Integre sempre por /v1.

3. Escopos

O escopo diz o que a chave faz; o papel de quem a emitiu diz até onde ela enxerga. Os dois se somam. Chamar uma rota sem o escopo dela devolve 403 nomeando o que falta.

GrupoEscopos
Contatos e etiquetascontacts:read contacts:write labels:read labels:write
Atendimentoconversations:read conversations:write messages:read messages:send
Números e relatórioscampaigns:read campaigns:write reports:read scheduled:read
Organização (leitura)sectors:read users:read channels:read quick-replies:read boards:read chatbots:read knowledge:read capture:read tracking:read
Etiqueta se aplica e se remove, não se cria. Criar mexe na taxonomia da empresa inteira e apagar tira o vínculo de todo mundo de uma vez — isso fica na tela. Configuração da organização é só leitura: criar setor, usuário, canal ou chatbot por API mudaria como a empresa atende, e um erro de integração ali é muito pior que um contato errado.

4. Endpoints

Contatos

RotaEscopo
GET /v1/contactscontacts:read
GET /v1/contacts/:idcontacts:read
POST /v1/contactscontacts:write
PATCH /v1/contacts/:idcontacts:write

A listagem aceita search, labelId, page e limit.

Conversas e atendimento

RotaEscopo
GET /v1/conversationsconversations:read
POST /v1/conversations — abrir com um númeroconversations:write
POST /v1/conversations/:id/assignconversations:write
POST /v1/conversations/:id/unassignconversations:write
POST /v1/conversations/:id/resolveconversations:write
POST /v1/conversations/:id/reopenconversations:write
POST /v1/conversations/:id/transfer-sectorconversations:write

assign recebe { "userId": "..." }. transfer-sector recebe { "sectorId": "..." }, ou null para tirar do setor. Para desatribuir use /unassign — não um null no assign.

Mensagens

RotaEscopo
GET /v1/conversations/:id/messagesmessages:read
POST /v1/conversations/:id/messages — textomessages:send
POST /v1/conversations/:id/messages/media — mídiamessages:send
POST /v1/conversations/:id/messages/template — templatemessages:send
POST /v1/media/upload-urlmessages:send
GET /v1/messages/:id/downloadmessages:read

/download devolve { url, fileName } — uma URL assinada e temporária, não os bytes. Baixe a partir dela.

Etiquetas

RotaEscopo
GET /v1/labelslabels:read
GET /v1/conversations/:id/labelslabels:read
POST /v1/contacts/:id/labels/:labelIdlabels:write
DELETE /v1/contacts/:id/labels/:labelIdlabels:write
POST /v1/conversations/:id/labels/:labelIdlabels:write
DELETE /v1/conversations/:id/labels/:labelIdlabels:write

Relatórios

RotaEscopo
GET /v1/reports/overviewreports:read
GET /v1/reports/sources
GET /v1/reports/sales
GET /v1/reports/ai
GET /v1/reports/calls
GET /v1/reports/capture
GET /v1/reports/filters

/reports/filters devolve os canais, setores e atendentes disponíveis — use para montar os filtros das outras rotas de relatório.

Organização e campanhas (leitura)

RotaEscopo
GET /v1/sectorssectors:read
GET /v1/usersusers:read
GET /v1/channelschannels:read
GET /v1/quick-repliesquick-replies:read
GET /v1/boardsboards:read
GET /v1/chatbotschatbots:read
GET /v1/knowledge-basesknowledge:read
GET /v1/capture-formscapture:read
GET /v1/tracking-linkstracking:read
GET /v1/scheduled-messagesscheduled:read
GET /v1/campaignscampaigns:read
GET /v1/campaigns/:idcampaigns:read
POST /v1/campaigns — criarcampaigns:write
POST /v1/campaigns/:id/dispatch
POST /v1/campaigns/:id/pause
POST /v1/campaigns/:id/resume
POST /v1/campaigns/:id/cancel
GET /v1/menenhum

5. Abrir conversa com um número novo

Toda mensagem sai dentro de uma conversa. Se a pessoa já falou com você, a conversa existe — pegue o id dela em GET /v1/conversations e pule para o envio.

Se ela nunca falou com você, abra a conversa primeiro:

curl -X POST https://api.msgflowapp.com.br/v1/conversations \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "channelId": "uuid-do-canal",
    "phone": "5517999998888",
    "name": "Nadine"
  }'

Responde 201 com a conversa. Use o id dela para enviar. O channelId — ou seja, por qual número seu a conversa vai sair — você pega em GET /v1/channels. O name é opcional e só vale se o contato ainda não existir.

Chamar duas vezes com o mesmo número não cria duas conversas. A rota devolve a conversa que já existe naquele canal. Pode repetir sem medo.
Esta é a rota que inicia contato com quem não te procurou. Abrir conversa não envia nada sozinho, mas é o primeiro passo do contato frio — e contato frio em volume é o jeito mais rápido de o WhatsApp bloquear o seu número. Se o seu caso é falar com muita gente de uma vez, não use a API: crie uma campanha pela tela do Anil, que aplica intervalo entre disparos, rodízio de números, janela de horário e cota diária. Não existe rota de campanha na API.

6. Enviar mensagem

Este endpoint envia na hora, sem nenhum intervalo entre disparos. Ele não aplica atraso, rodízio de números, janela de horário nem cota diária — o ritmo é responsabilidade de quem chama. Um laço no seu código queima o número de WhatsApp, e o número é seu. Para envio em massa, crie uma campanha pela tela, que aplica o ritmo — pela API não existe esse caminho. Este endpoint é para mensagem avulsa, resposta e notificação.
curl -X POST https://api.msgflowapp.com.br/v1/conversations/CONV_ID/messages \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-4821-confirmado" \
  -d '{"content": "Seu pedido saiu para entrega!"}'

Para imagem, áudio ou documento veja enviar mídia.

7. Enviar mídia

São dois passos: você pede uma URL de upload, manda o arquivo direto para o storage, e depois envia a mensagem citando a chave que recebeu. O arquivo nunca passa pela API — por isso funciona com arquivo grande.

Passo 1 — pedir a URL

curl -X POST https://api.msgflowapp.com.br/v1/media/upload-url \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"filename": "boleto.pdf", "contentType": "application/pdf"}'

# → { "uploadUrl": "https://...", "mediaKey": "...", "expiresInSec": 300 }

Passo 2 — subir o arquivo

curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @boleto.pdf

Use PUT, e o mesmo Content-Type que você informou no passo 1. A URL vale 5 minutos.

Passo 3 — enviar

curl -X POST https://api.msgflowapp.com.br/v1/conversations/CONV_ID/messages/media \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: boleto-4821" \
  -d '{
    "type": "DOCUMENT",
    "mediaKey": "a-key-do-passo-1",
    "fileName": "boleto.pdf",
    "caption": "Segue o boleto"
  }'
typePara quê
IMAGEfoto, print, comprovante
VIDEOvídeo
AUDIOáudio
DOCUMENTPDF e afins — use fileName
STICKERfigurinha (webp)
Mídia pesa mais que texto no julgamento do WhatsApp. Rajada de imagem para quem não te procurou é o padrão clássico de conta bloqueada. Este endpoint também não aplica intervalo nenhum.

8. Template — falar depois das 24h

Só para canal Oficial (WhatsApp Business API). Em canal não oficial esta rota responde 422, e não é necessária: lá não existe janela de 24h.

No canal Oficial, se o contato não te responde há mais de 24 horas, mensagem comum é recusada com 422. O único jeito de falar com ele é um template aprovado pela Meta.

curl -X POST https://api.msgflowapp.com.br/v1/conversations/CONV_ID/messages/template \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "templateName": "confirmacao_pedido",
    "languageCode": "pt_BR",
    "bodyParams": ["Nadine", "4821"]
  }'

bodyParams preenche {{1}}, {{2}}… na ordem. O template precisa estar aprovado na Meta antes.

9. Campanhas — o caminho seguro para volume

Se você vai falar com muita gente, é por aqui. Diferente do envio avulso, a campanha passa pelo motor de disparo: intervalo entre mensagens, rodízio de números, janela de horário e cota diária. É o anti-ban que o envio direto não tem.

Criar

curl -X POST https://api.msgflowapp.com.br/v1/campaigns \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Promo de setembro",
    "channelId": "uuid-do-canal",
    "contentMode": "TEXT",
    "messageText": "Oi {nome}! Temos novidade.",
    "recipientSource": "SEGMENT",
    "segmentLabelIds": ["uuid-da-etiqueta"],
    "minDelaySec": 30,
    "maxDelaySec": 60,
    "sendWeekdays": [1,2,3,4,5],
    "sendHours": [9,10,11,14,15,16],
    "dailyLimit": 200
  }'

Ela nasce em DRAFT — criar não dispara. Confira e então dispare:

curl -X POST https://api.msgflowapp.com.br/v1/campaigns/ID/dispatch \
  -H "Authorization: Bearer SUA_CHAVE"
AçãoQuando vale
POST /v1/campaigns/:id/dispatchsó em DRAFT
POST /v1/campaigns/:id/pausesó em execução
POST /v1/campaigns/:id/resumesó pausada
POST /v1/campaigns/:id/cancelenquanto não encerrou
GET /v1/campaigns/:idacompanhar o progresso
Disparo não tem desfazer. Cancelar segura o que ainda não saiu; o que já foi entregue, já foi. Por isso criar e disparar são duas chamadas — dá para conferir os destinatários em GET /v1/campaigns/:id antes.
Ritmo: minDelaySec/maxDelaySec é o intervalo aleatório entre mensagens (o padrão é 30–60s, e mais lento é mais seguro). sendWeekdays usa 1=segunda. sendHours são as horas cheias liberadas. sendChannelIds liga o rodízio entre vários números.

10. Idempotência

Rede falha e bibliotecas HTTP repetem requisição sozinhas. Sem proteção, um retry de "enviar mensagem" vira mensagem duplicada no WhatsApp de uma pessoa real — dano visível e que não dá para desfazer.

Mande o cabeçalho Idempotency-Key com um valor único por operação (o id do pedido no seu sistema serve bem). Repetir a mesma chave devolve o mesmo resultado, sem executar de novo.

RotaAceita
POST /v1/conversations/:id/messagessim
POST /v1/conversations/:id/messages/mediasim
POST /v1/conversations/:id/messages/templatesim
POST /v1/conversationssim
POST /v1/campaignssim
POST /v1/contactssim
A reserva vale 24 horas. Se você reenviar enquanto a primeira ainda está em andamento, a resposta é 409 — espere e tente de novo, não troque a chave.

11. Limites de uso

O limite é por chave, não por IP — sua integração sai de um servidor só e não seria justo dividir balde com terceiros. Janela de 1 minuto.

Tipo de rotaPor minuto
Leitura (contatos, conversas, mensagens, organização)120
Escrita de contato, filtros de relatório, pedir URL de upload60
Envio de mensagem (texto, mídia, template)30
Abrir conversa30
Campanhas: criar, disparar, pausar, cancelar30
Estourou, a resposta é 429. Os cabeçalhos x-ratelimit-remaining e retry-after dizem quanto sobrou e quando voltar. Trate o 429 com espera progressiva.
Este limite protege a nossa infraestrutura, não o seu número. São coisas diferentes: caber no limite de 30 envios por minuto não significa que o WhatsApp vai achar o ritmo natural. Veja o aviso do envio de mensagem.

12. Erros

Todo erro devolve JSON no formato { "error": "mensagem" }.

CódigoO que significa
400Corpo ou parâmetro inválido.
401Chave ausente, malformada, revogada, expirada — ou o dono dela foi desativado ou rebaixado, ou a organização foi fechada. Todos respondem igual, para não revelar qual prefixo existe. Reativar o usuário ou reabrir a organização religa a chave sozinho.
403A chave é válida, mas (a) não tem o escopo da rota — a mensagem nomeia o que falta; ou (b) o X-Anil-Org enviado não é a organização da chave.
404Não existe ou está fora dos canais da chave. Os dois casos respondem igual, de propósito: distinguir permitiria descobrir o que existe sem ter acesso.
409Outra requisição com a mesma Idempotency-Key ainda está em andamento.
429Limite por minuto estourado.

13. Webhooks

Em vez de ficar perguntando "tem novidade?", receba um POST assim que algo acontece. Cadastre o endpoint em Configurações → Webhooks e guarde o segredo que aparece na criação — ele também só é mostrado uma vez.

Validar a assinatura

Toda entrega leva a assinatura do corpo. Valide sempre — sem isso qualquer um que descubra sua URL pode postar nela.

// Node.js — assine o corpo CRU, antes de qualquer parse
const assinatura = 'sha256=' + crypto
  .createHmac('sha256', SEU_SEGREDO)
  .update(`${timestamp}.${corpoCru}`)
  .digest('hex')

// compare com o cabeçalho recebido, em tempo constante
crypto.timingSafeEqual(Buffer.from(assinatura), Buffer.from(recebido))
O timestamp entra no cálculo justamente para impedir replay: sem ele, um corpo capturado poderia ser reenviado para sempre e a assinatura continuaria conferindo. Rejeite entregas com timestamp muito antigo.

Entrega e retentativa

Responda 2xx rápido — processe depois, em fila sua. Falhou, tentamos 5 vezes com espera crescente. Depois de 10 falhas seguidas o endpoint é desativado automaticamente e você recebe aviso na tela; é só arrumar e reativar, que o contador zera. O histórico de cada entrega fica visível em Configurações → Webhooks.

14. Conectar ao Claude (MCP)

O Anil expõe um servidor MCP, o padrão que o Claude usa para falar com sistemas externos. Com ele você conversa com o CRM em linguagem natural: "quantas conversas estão na fila do setor de vendas?", "resume o atendimento do contato X".

No Claude, adicione um conector com:

URL https://mcp.msgflowapp.com.br/mcp/sua-organização

Autentique com a mesma chave de API, no cabeçalho Authorization: Bearer. A URL exata da sua organização aparece em Configurações → Chaves de API, ao lado da chave — copie as duas do mesmo lugar.

Administra mais de uma empresa? Use um conector por empresa. O Claude identifica conector pela URL, então dois conectores na mesma URL não coexistem — é para isso que existe o trecho da organização no fim do endereço. Com Difafá e S4R41VA ligadas ao mesmo tempo, dá para pedir comparações entre elas (“mensagens recebidas em cada uma no mês passado”): o assistente consulta as duas e junta os números.
A empresa continua vindo da CHAVE, não da URL. O trecho final do endereço é só conferência: se você configurar um conector da empresa A com a chave da empresa B, toda chamada falha com 403 em vez de agir na empresa errada em silêncio. O endereço rotula; quem autoriza é a chave.

O endereço antigo /mcp, sem a organização, continua funcionando para quem já tem um conector configurado.

São 29 ferramentas: ler contatos, conversas, mensagens, relatórios e campanhas; aplicar etiqueta; atribuir e finalizar atendimento; enviar mensagem e template; abrir conversa; e criar, disparar, pausar ou cancelar campanha.

Use uma chave separada e estreita para o MCP. O assistente enxerga tudo o que a chave enxerga — inclusive o conteúdo das conversas. Marque só os escopos que você quer que ele alcance, e não reaproveite a chave da sua integração de sistema.
Com campaigns:write na chave, o assistente pode disparar campanha para milhares de pessoas. O disparo não tem desfazer. As ferramentas que agem são marcadas para o Claude pedir sua confirmação antes de executar, mas a decisão de dar esse escopo ao MCP é sua — se não quiser, deixe a caixa desmarcada e ele continua podendo ler e acompanhar campanhas.
O MCP é um cliente desta mesma API. Ele não tem atalho para o banco e não faz nada que a chave não permita — escopo faltando vira erro, igual a qualquer integração.