Documentação
Guia de integração
Como ligar o site, um formulário próprio, um ERP ou uma automação a este CRM. Os exemplos usam https://ENDERECO-DO-SEU-CRM no lugar do endereço do sistema: troque pelo seu. A chave da API nunca aparece aqui: onde ela vai, o exemplo diz COLE_AQUI_A_SUA_CHAVE.
1. Por onde o site fala com o CRM
Quatro caminhos. Escolha pelo que o seu site já tem.
| Se o seu site… | Use | Precisa programar? |
|---|---|---|
| não tem formulário, ou pode trocar o que tem | O formulário de captura, por link ou numa moldura dentro da página | Não |
| quer um botão "Fale com a gente" em todas as páginas | O widget: uma linha de <script> | Não |
| já tem formulários próprios e quer mantê-los | A API pública, chamada pelo servidor do site ou por n8n, Make ou Zapier | Sim, ou uma ferramenta de automação |
| precisa saber, lá fora, quando algo acontece aqui | Os webhooks de saída | Sim, ou uma ferramenta de automação |
O formulário e o widget são públicos por desenho: o visitante preenche direto do navegador, com proteção contra robô. A API não é — ela pede uma chave, e a chave não pode ir para o navegador (veja a seção da API).
2. Formulário de captura
Pronto, com a sua marca, e sem uma linha de código.
Crie em Ajustes → Formulários: escolha os campos (nome e WhatsApp são sempre pedidos; o e-mail e os campos de Ajustes → Campos, se quiser), o funil e a etapa onde o lead entra, e quem fica com ele. O formulário já sai com o nome, o logo e a cor da sua empresa. Cada um tem o próprio endereço, e os botões "Copiar link" e "Copiar widget" da tela de Formulários entregam os trechos abaixo prontos.
Por link
Para um botão do site, a bio do Instagram ou um anúncio:
https://ENDERECO-DO-SEU-CRM/f/NOME-DO-FORMULARIODentro de uma página do site
O formulário pode ser emoldurado em qualquer site — é a única tela do sistema que aceita isso. O ?embed=1 tira a margem de página inteira:
<iframe src="https://ENDERECO-DO-SEU-CRM/f/NOME-DO-FORMULARIO?embed=1" title="Fale com a gente" loading="lazy" style="width:100%;max-width:560px;height:720px;border:0"></iframe>De onde veio o lead
O formulário guarda de onde a pessoa veio quando o endereço dele traz as marcações de campanha — utm_source, utm_medium, utm_campaign, utm_term, utm_content — e os identificadores de clique gclid, fbclid, ttclid, msclkid. O identificador de clique é apagado em 90 dias, a janela de conversão do Google e da Meta; as marcações ficam.
Pelo link, isso acontece sozinho: o anúncio aponta para o link e as marcações vêm junto. Na moldura, o formulário lê o endereço DELE, e não o da página do site — a origem só entra se estiver no src da moldura. Quem quer a origem sem pensar nisso usa o widget, que a leva sozinho.
3. Widget
Um botão flutuante que abre o formulário num painel, sem sair da página.
Cole antes do </body>, ou no campo de scripts do construtor do site (WordPress, Wix, Webflow…). Ele abre o MESMO formulário, num painel, e leva para dentro dele as marcações de campanha da página: a origem entra sozinha.
<script src="https://ENDERECO-DO-SEU-CRM/api/publico/widget.js" data-formulario="NOME-DO-FORMULARIO" data-texto="Fale com a gente" async></script>| Atributo | O que faz | Sem ele |
|---|---|---|
data-formulario | Qual formulário o painel abre. Obrigatório. | O botão não aparece. |
data-texto | O texto do botão. | "Fale com a gente" |
data-cor | A cor do botão, em hexadecimal (#1a7f5a). | Um azul-marinho quase preto. |
data-lado | esquerda põe o botão no canto esquerdo. | Canto direito. |
O script não usa cookie, não rastreia ninguém e não carrega biblioteca: monta o botão e o painel, e sai da frente.
4. API pública
Para o formulário que o site já tem, um ERP ou uma automação.
A chave
Um administrador cria a chave em Ajustes → API e webhooks. Ela aparece uma vez, na criação: o sistema guarda só uma impressão dela e não consegue mostrá-la de novo. Use uma chave por integração — revogar uma não derruba as outras. Uma chave sem permissão de escrita só lê.
Toda chamada leva a chave no cabeçalho Authorization:
Authorization: Bearer COLE_AQUI_A_SUA_CHAVECriar um negócio · POST /api/publico/v1/negocios
O caminho de quem tem formulário próprio: cria o negócio e, se o telefone ainda não é de nenhum contato, o contato junto. O mesmo telefone duas vezes cria um segundo negócio, nunca um segundo contato. Precisa de uma chave que escreve.
| No corpo (JSON) | Tipo | Obrigatório | O que é |
|---|---|---|---|
nome | texto | sim | O nome da pessoa. |
telefone | texto | sim | Com DDD, com ou sem +55, com ou sem pontuação: 11988776655 e +55 (11) 98877-6655 são o mesmo número. É por ele que o CRM reconhece quem já é contato. |
email | texto | não | O e-mail. |
titulo | texto | não | O título do negócio. Sem ele, o nome da pessoa. |
origem | texto | não | De onde veio, como você quiser chamar (até 60 caracteres). Sem ele, api. |
etapa_id | id | não | A etapa onde ele entra — tem de ser uma etapa aberta (nem ganho, nem perda). Sem ela, a primeira etapa do funil padrão. |
responsavel_id | id | não | Quem fica com ele: uma pessoa ativa da equipe. Sem ele, o negócio entra sem responsável, a não ser que uma automação de distribuição o atribua. |
campos_do_contato | objeto | não | Campos personalizados do contato, pela chave (veja abaixo). |
campos_do_negocio | objeto | não | Campos personalizados do negócio, pela chave. |
utm | objeto | não | A origem da campanha, como veio na URL do site: utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, fbclid, ttclid, msclkid. O que não está nessa lista é ignorado. |
curl -X POST "https://ENDERECO-DO-SEU-CRM/api/publico/v1/negocios" \
-H "Authorization: Bearer COLE_AQUI_A_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"nome": "Ana Paula",
"telefone": "11988776655",
"email": "ana@exemplo.com",
"titulo": "Orçamento pelo site",
"origem": "site",
"campos_do_contato": {
"cidade": "Campinas"
},
"utm": {
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "outubro"
}
}'Resposta: 201 com { "id": "…" }, o id do negócio criado.
Ler os negócios · GET /api/publico/v1/negocios
Para conferir o que entrou, ou manter outro sistema em dia. Qualquer chave lê.
| Na consulta | Tipo | Obrigatório | O que é |
|---|---|---|---|
desde | data | não | Data ISO 8601 (2026-10-01 ou 2026-10-01T12:00:00-03:00; sem hora, meia-noite em UTC). Com ela, a lista vem do mais antigo para o mais novo. |
cursor | texto | não | O paginacao.proximo da resposta anterior, sem alterar. Continua de onde parou. |
limite | número | não | Quantos por página: de 1 a 200. Sem ele, 50. |
curl "https://ENDERECO-DO-SEU-CRM/api/publico/v1/negocios?desde=2026-10-01&limite=100" \
-H "Authorization: Bearer COLE_AQUI_A_SUA_CHAVE"Resposta: { "negocios": [...], "paginacao": {...} }. Cada item traz id, title, value, status, source, stage_id, pipeline_id, contact_id, created_at.
Criar um contato, sem negócio · POST /api/publico/v1/contatos
Para manter a base em dia — uma lista de e-mails, o cadastro de um evento. Quem quer o contato e a oportunidade usa /negocios, que cria os dois. Precisa de uma chave que escreve.
| No corpo (JSON) | Tipo | Obrigatório | O que é |
|---|---|---|---|
nome | texto | sim | O nome da pessoa. |
telefone | texto | sim | Com DDD, com ou sem +55, com ou sem pontuação: 11988776655 e +55 (11) 98877-6655 são o mesmo número. É por ele que o CRM reconhece quem já é contato. |
email | texto | não | O e-mail. |
origem | texto | não | De onde veio, como você quiser chamar (até 60 caracteres). Sem ele, api. |
campos | objeto | não | Campos personalizados do contato, pela chave. Só valem para contato novo. |
curl -X POST "https://ENDERECO-DO-SEU-CRM/api/publico/v1/contatos" \
-H "Authorization: Bearer COLE_AQUI_A_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"nome": "Bruno Lima",
"telefone": "11977665544",
"email": "bruno@exemplo.com",
"origem": "evento"
}'Resposta: 201 com { "id": "…", "criado": true } para contato novo; 200 com "criado": false quando o telefone já era de alguém.
Ler os contatos · GET /api/publico/v1/contatos
O mesmo contrato da leitura de negócios. Qualquer chave lê.
| Na consulta | Tipo | Obrigatório | O que é |
|---|---|---|---|
desde | data | não | Data ISO 8601 (2026-10-01 ou 2026-10-01T12:00:00-03:00; sem hora, meia-noite em UTC). Com ela, a lista vem do mais antigo para o mais novo. |
cursor | texto | não | O paginacao.proximo da resposta anterior, sem alterar. Continua de onde parou. |
limite | número | não | Quantos por página: de 1 a 200. Sem ele, 50. |
curl "https://ENDERECO-DO-SEU-CRM/api/publico/v1/contatos?desde=2026-10-01" \
-H "Authorization: Bearer COLE_AQUI_A_SUA_CHAVE"Resposta: { "contatos": [...], "paginacao": {...} }. Cada item traz id, name, email, phone_e164, source, tags, created_at.
Lançar o investimento em anúncio · POST /api/publico/v1/investimento
Para quem tem o gasto numa planilha ou num painel: um lançamento, ou uma lista em lancamentos (até 500 por chamada). Mandar de novo o mesmo dia da mesma campanha SUBSTITUI o valor — a plataforma corrige número por até 72 horas, e somar daria o dobro. Um lançamento com erro recusa a chamada inteira. Precisa de uma chave que escreve.
| No corpo (JSON) | Tipo | Obrigatório | O que é |
|---|---|---|---|
campanha | texto | sim | O nome da campanha, igual ao utm_campaign dos leads dela: é por ele que o gasto encontra a venda. |
dia | data | sim | AAAA-MM-DD, o dia do gasto. |
custo | número | não | Em reais, com ponto decimal e sem sinal: 182.40. |
cliques | número | não | Inteiro. |
plataforma | texto | não | meta, google ou o que você usar. Sem ela, manual. |
conta | texto | não | A conta de anúncio, se você tem mais de uma. |
campanha_externa | texto | não | O id da campanha na plataforma (também aceito como campanhaExterna). Sem ele, vale o nome. |
lancamentos | lista | não | Vários lançamentos de uma vez, cada um com os campos acima. |
curl -X POST "https://ENDERECO-DO-SEU-CRM/api/publico/v1/investimento" \
-H "Authorization: Bearer COLE_AQUI_A_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"lancamentos": [
{
"plataforma": "meta",
"campanha": "outubro",
"dia": "2026-10-30",
"custo": 182.4,
"cliques": 311
}
]
}'Resposta: 201 com { "lancados": 1 }.
Ler o investimento · GET /api/publico/v1/investimento
Do dia mais recente para o mais antigo, até 500 linhas. Qualquer chave lê.
| Na consulta | Tipo | Obrigatório | O que é |
|---|---|---|---|
de | data | não | AAAA-MM-DD, o primeiro dia. |
ate | data | não | AAAA-MM-DD, o último dia. |
curl "https://ENDERECO-DO-SEU-CRM/api/publico/v1/investimento?de=2026-10-01&ate=2026-10-31" \
-H "Authorization: Bearer COLE_AQUI_A_SUA_CHAVE"Resposta: { "investimento": [...] }. Cada item traz plataforma, conta, campanha_externa, campanha, dia, custo, cliques, atualizado_em.
Do formulário do site para a API
O caso mais comum: o site já tem o formulário e só precisa mandar o que a pessoa preencheu. Quem recebe o envio é o servidor do site, e é ele que chama a API — com a chave guardada numa variável de ambiente, nunca escrita no código. Sem servidor próprio, o mesmo passo se faz num n8n, num Make ou num Zapier, com o nó de requisição HTTP.
<?php
// No SERVIDOR do site (PHP 7.4 ou mais novo), ao receber o formulário.
$corpo = json_encode([
'nome' => $_POST['nome'] ?? '',
'telefone' => $_POST['telefone'] ?? '',
'email' => $_POST['email'] ?? '',
'origem' => 'site',
]);
$ch = curl_init('https://ENDERECO-DO-SEU-CRM/api/publico/v1/negocios');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $corpo,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('CRM_API_KEY'),
'Content-Type: application/json',
],
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);// No SERVIDOR do site (Node 18 ou mais novo), ao receber o formulário.
const resposta = await fetch('https://ENDERECO-DO-SEU-CRM/api/publico/v1/negocios', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.CRM_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
nome: formulario.nome,
telefone: formulario.telefone,
email: formulario.email,
origem: 'site',
}),
})
if (!resposta.ok) console.error(resposta.status, await resposta.json())Campos personalizados
Os campos de Ajustes → Campos vão pela CHAVE do campo, e não pelo rótulo: o rótulo muda quando alguém o reescreve, a chave não. Em /negocios, os do contato vão em campos_do_contato e os do negócio em campos_do_negocio; em /contatos, em campos. O cidade do exemplo é ilustrativo: use as chaves da sua instalação.
O valor é gravado como chega — a API não confere o tipo. Mande no formato do campo: data como AAAA-MM-DD, número com ponto decimal e sem separador de milhar, sim/não como true ou false, e opção de lista com o valor exato dela. Uma chave que não existe em Ajustes → Campos é guardada, mas não aparece em tela nenhuma.
Quando o telefone já é de um contato, o que está preenchido nele não é sobrescrito: a API só completa o que está vazio. A correção que a equipe fez na ficha vale mais que o que a integração manda depois.
Os ids das etapas e das pessoas, e as chaves dos campos desta instalação, estão na versão deste guia dentro do sistema: Ajustes → Guia de integração.
Ler tudo, sem perder nenhum
Sem desde nem cursor, a lista traz só os mais recentes, do mais novo para o mais antigo — serve para uma olhada, não para sincronizar. Para ler tudo, chame com desde e siga paginacao.proximo: chame de novo com ?cursor= e o valor dele até paginacao.tem_mais ser false. Cada registro vem uma vez só, mesmo quando entram cem no mesmo segundo.
curl "https://ENDERECO-DO-SEU-CRM/api/publico/v1/negocios?cursor=VALOR_DE_PAGINACAO_PROXIMO" \
-H "Authorization: Bearer COLE_AQUI_A_SUA_CHAVE"Guarde o último proximo: na próxima sincronização, comece dele e receba só o que entrou depois, sem depender de relógio. Para saber na hora em que algo entra, use os webhooks.
Quando dá errado
Todo erro volta como { "erro": "…" }, com a frase que diz o que houve.
| Status | Quando |
|---|---|
400 | O corpo não é JSON. |
401 | Sem chave, chave errada ou revogada. A resposta é sempre a mesma, de propósito: ela não diz o que faltou. |
403 | A chave é válida, mas só lê — e a rota escreve. Não gere outra: dê a esta a permissão, ou crie uma que escreva. |
409 | O etapa_id não existe ou não é uma etapa aberta (ou o CRM não tem nenhuma etapa aberta). |
422 | Um campo não passou — nome, telefone, responsavel_id, limite, cursor, desde ou um lançamento. A frase em erro diz qual. |
500 | Falha do lado do CRM. Tente de novo; se continuar, quem instalou o sistema vê o motivo no registro do servidor. |
Limites
- Listas de negócios e contatos: até 200 por página (50 sem o
limite). - Investimento: até 500 lançamentos por chamada; a leitura traz até 500 linhas.
- Não há limite de chamadas por minuto. A proteção é a chave: uma por integração, revogável na hora.
5. Webhooks de saída
O CRM avisa o seu sistema quando algo acontece aqui dentro.
Cadastre o endereço que vai receber em Ajustes → API e webhooks, marque os eventos e, de preferência, um segredo. O endereço precisa ser https e público: rede interna e redirecionamento são recusados. No n8n, no Make ou no Zapier, é o gatilho de webhook genérico — Webhook, Custom webhook ou Catch Hook — com o endereço que ele gerar.
| Evento | Quando | Em `dados` |
|---|---|---|
contato.criado | Contato novo | id, nome, telefone, email, origem, criado_em |
negocio.criado | Negócio novo | id, titulo, valor, funil_id, etapa_id, contato_id, responsavel_id, situacao, origem |
negocio.mudou_etapa | Negócio mudou de etapa | id, titulo, valor, funil_id, etapa_id, contato_id, responsavel_id, situacao, origem, etapa_anterior_id |
negocio.ganho | Negócio ganho | id, titulo, valor, funil_id, etapa_id, contato_id, responsavel_id, situacao, origem |
negocio.perdido | Negócio perdido | id, titulo, valor, funil_id, etapa_id, contato_id, responsavel_id, situacao, origem |
proposta.aceita | Proposta aceita pelo cliente | negocio_id, resposta, nota, total_centavos |
proposta.recusada | Proposta recusada pelo cliente | negocio_id, resposta, nota, total_centavos |
contato.criado vale para todo contato novo, por qualquer porta — inclusive a importação de planilha. negocio.criado não: negócio trazido pela importação não é negócio novo, e não avisa ninguém. Ganho e perda chegam como negocio.mudou_etapa E como o evento próprio, para quem só quer saber de fechamento não precisar filtrar etapa por nome.
Os campos
| Campo | Em | O que é |
|---|---|---|
id | contato.criado | O id do contato. |
nome | contato.criado | O nome. |
telefone | contato.criado | No formato internacional. |
email | contato.criado | Pode vir nulo. |
origem | contato.criado | De onde ele veio. |
criado_em | contato.criado | Quando entrou, ISO 8601. |
id | negocio.* | O id do negócio. |
titulo | negocio.* | O título. |
valor | negocio.* | Em reais, número. |
funil_id | negocio.* | O funil. |
etapa_id | negocio.* | A etapa em que ele está agora. |
contato_id | negocio.* | O contato. |
responsavel_id | negocio.* | Quem cuida dele. Pode vir nulo. |
situacao | negocio.* | open (aberto), won (ganho) ou lost (perdido). |
origem | negocio.* | De onde ele veio: whatsapp, api, o formulário… |
etapa_anterior_id | negocio.* | A etapa de onde ele saiu. |
negocio_id | proposta.* | O negócio da proposta. |
resposta | proposta.* | aceita ou recusada. |
nota | proposta.* | O que o cliente escreveu ao responder. Pode vir nulo. |
total_centavos | proposta.* | O total do documento, em CENTAVOS (inteiro): 189000 é R$ 1.890,00. |
O que chega
Um POST com o corpo em JSON — o nome do evento, os dados dele e a hora do envio:
{
"evento": "negocio.ganho",
"dados": {
"id": "6f1c2a90-3b5e-4d7a-9c21-8e4f0b7d2a15",
"titulo": "Orçamento pelo site",
"valor": 1890,
"funil_id": "a3d9e0c4-7f12-4b8e-9d65-1c2b3a4f5e60",
"etapa_id": "b7e4f1a2-0c3d-4e5f-8a9b-6c7d8e9f0a1b",
"contato_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"responsavel_id": null,
"situacao": "won",
"origem": "api"
},
"enviado_em": "2026-10-31T14:02:13.907Z"
}| Cabeçalho | Valor | Quando |
|---|---|---|
Content-Type | application/json | Sempre. |
User-Agent | CRM-Webhook/1 | Sempre. |
X-Crm-Evento | o evento, como negocio.ganho | Sempre — dá para filtrar sem abrir o corpo. |
X-Crm-Signature | sha256= e a assinatura em hexadecimal | Quando o assinante tem segredo. |
Conferir a assinatura
Com segredo cadastrado, o X-Crm-Signature leva sha256= e o HMAC-SHA256 do corpo, em hexadecimal, com o segredo como chave. Confira antes de acreditar no aviso: sem isso, quem descobrir o seu endereço inventa um "negócio ganho". A conta é sobre o corpo CRU, os bytes como chegaram — um JSON relido e reescrito muda espaço e ordem, e a conta deixa de bater.
const crypto = require('node:crypto')
// corpoCru: os bytes como chegaram. No Express: express.raw({ type: "application/json" })
function assinaturaConfere(corpoCru, recebida, segredo) {
const esperada = 'sha256=' + crypto.createHmac('sha256', segredo).update(corpoCru).digest('hex')
const r = String(recebida || '')
return r.length === esperada.length && crypto.timingSafeEqual(Buffer.from(r), Buffer.from(esperada))
}
// assinaturaConfere(req.body, req.get('X-Crm-Signature'), process.env.CRM_WEBHOOK_SECRET)<?php
$corpo = file_get_contents('php://input');
$esperada = 'sha256=' . hash_hmac('sha256', $corpo, getenv('CRM_WEBHOOK_SECRET'));
$recebida = $_SERVER['HTTP_X_CRM_SIGNATURE'] ?? '';
if (!hash_equals($esperada, $recebida)) {
http_response_code(401);
exit;
}
$aviso = json_decode($corpo, true); // $aviso['evento'], $aviso['dados']
http_response_code(200);Quando chega, e se falhar
- O aviso sai na próxima batida do agendador, que roda a cada 10 minutos: não é instantâneo.
- Responda com um status 2xx em até 10 segundos. Redirecionamento (3xx) conta como falha, e não se repete.
- Falhou? O CRM tenta de novo nas batidas seguintes, com espera crescente, até 5 tentativas. Depois disso, desiste.
- Cada entrega fica no histórico de Ajustes → API e webhooks, com o status que o seu sistema respondeu ou o motivo da falha.
- O mesmo aviso pode chegar duas vezes (quando o seu servidor recebeu, mas respondeu tarde). Use o evento e o
idemdadospara não processar duas vezes.