Anil CRM
Conecte seu sistema ao Anil CRM: leia e crie contatos, acompanhe conversas, envie mensagens, aplique etiquetas e receba eventos em tempo real por webhook.
https://api.msgflowapp.com.br
404, como se não existissem.
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?".
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.
É 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.
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.
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.
| Grupo | Escopos |
|---|---|
| Contatos e etiquetas | contacts:read contacts:write labels:read labels:write |
| Atendimento | conversations:read conversations:write messages:read messages:send |
| Números e relatórios | campaigns: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 |
| Rota | Escopo |
|---|---|
GET /v1/contacts | contacts:read |
GET /v1/contacts/:id | contacts:read |
POST /v1/contacts | contacts:write |
PATCH /v1/contacts/:id | contacts:write |
A listagem aceita search, labelId, page e
limit.
| Rota | Escopo |
|---|---|
GET /v1/conversations | conversations:read |
POST /v1/conversations — abrir com um número | conversations:write |
POST /v1/conversations/:id/assign | conversations:write |
POST /v1/conversations/:id/unassign | conversations:write |
POST /v1/conversations/:id/resolve | conversations:write |
POST /v1/conversations/:id/reopen | conversations:write |
POST /v1/conversations/:id/transfer-sector | conversations:write |
assign recebe { "userId": "..." }.
transfer-sector recebe { "sectorId": "..." }, ou
null para tirar do setor. Para desatribuir use
/unassign — não um null no assign.
| Rota | Escopo |
|---|---|
GET /v1/conversations/:id/messages | messages:read |
POST /v1/conversations/:id/messages — texto | messages:send |
POST /v1/conversations/:id/messages/media — mídia | messages:send |
POST /v1/conversations/:id/messages/template — template | messages:send |
POST /v1/media/upload-url | messages:send |
GET /v1/messages/:id/download | messages:read |
/download devolve { url, fileName } — uma URL assinada e
temporária, não os bytes. Baixe a partir dela.
| Rota | Escopo |
|---|---|
GET /v1/labels | labels:read |
GET /v1/conversations/:id/labels | labels:read |
POST /v1/contacts/:id/labels/:labelId | labels:write |
DELETE /v1/contacts/:id/labels/:labelId | labels:write |
POST /v1/conversations/:id/labels/:labelId | labels:write |
DELETE /v1/conversations/:id/labels/:labelId | labels:write |
| Rota | Escopo |
|---|---|
GET /v1/reports/overview | reports: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.
| Rota | Escopo |
|---|---|
GET /v1/sectors | sectors:read |
GET /v1/users | users:read |
GET /v1/channels | channels:read |
GET /v1/quick-replies | quick-replies:read |
GET /v1/boards | boards:read |
GET /v1/chatbots | chatbots:read |
GET /v1/knowledge-bases | knowledge:read |
GET /v1/capture-forms | capture:read |
GET /v1/tracking-links | tracking:read |
GET /v1/scheduled-messages | scheduled:read |
GET /v1/campaigns | campaigns:read |
GET /v1/campaigns/:id | campaigns:read |
POST /v1/campaigns — criar | campaigns:write |
POST /v1/campaigns/:id/dispatch | |
POST /v1/campaigns/:id/pause | |
POST /v1/campaigns/:id/resume | |
POST /v1/campaigns/:id/cancel | |
GET /v1/me | nenhum |
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.
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.
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.
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 }
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.
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"
}'
| type | Para quê |
|---|---|
IMAGE | foto, print, comprovante |
VIDEO | vídeo |
AUDIO | áudio |
DOCUMENT | PDF e afins — use fileName |
STICKER | figurinha (webp) |
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.
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ção | Quando vale |
|---|---|
POST /v1/campaigns/:id/dispatch | só em DRAFT |
POST /v1/campaigns/:id/pause | só em execução |
POST /v1/campaigns/:id/resume | só pausada |
POST /v1/campaigns/:id/cancel | enquanto não encerrou |
GET /v1/campaigns/:id | acompanhar o progresso |
GET /v1/campaigns/:id antes.
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.
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.
| Rota | Aceita |
|---|---|
POST /v1/conversations/:id/messages | sim |
POST /v1/conversations/:id/messages/media | sim |
POST /v1/conversations/:id/messages/template | sim |
POST /v1/conversations | sim |
POST /v1/campaigns | sim |
POST /v1/contacts | sim |
409 — espere e tente de novo,
não troque a chave.
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 rota | Por minuto |
|---|---|
| Leitura (contatos, conversas, mensagens, organização) | 120 |
| Escrita de contato, filtros de relatório, pedir URL de upload | 60 |
| Envio de mensagem (texto, mídia, template) | 30 |
| Abrir conversa | 30 |
| Campanhas: criar, disparar, pausar, cancelar | 30 |
429. Os cabeçalhos
x-ratelimit-remaining e retry-after dizem quanto sobrou e
quando voltar. Trate o 429 com espera progressiva.
Todo erro devolve JSON no formato { "error": "mensagem" }.
| Código | O que significa |
|---|---|
400 | Corpo ou parâmetro inválido. |
401 | Chave 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. |
403 | A 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. |
404 | Nã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. |
409 | Outra requisição com a mesma Idempotency-Key ainda está em andamento. |
429 | Limite por minuto estourado. |
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.
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))
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.
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:
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.
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.
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.