Vetora

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…UsePrecisa programar?
não tem formulário, ou pode trocar o que temO formulário de captura, por link ou numa moldura dentro da páginaNão
quer um botão "Fale com a gente" em todas as páginasO widget: uma linha de <script>Não
já tem formulários próprios e quer mantê-losA API pública, chamada pelo servidor do site ou por n8n, Make ou ZapierSim, ou uma ferramenta de automação
precisa saber, lá fora, quando algo acontece aquiOs webhooks de saídaSim, 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:

Link do formulário
https://ENDERECO-DO-SEU-CRM/f/NOME-DO-FORMULARIO

Dentro 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:

Moldura (iframe)
<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.

Widget
<script src="https://ENDERECO-DO-SEU-CRM/api/publico/widget.js" data-formulario="NOME-DO-FORMULARIO" data-texto="Fale com a gente" async></script>
AtributoO que fazSem ele
data-formularioQual formulário o painel abre. Obrigatório.O botão não aparece.
data-textoO texto do botão."Fale com a gente"
data-corA cor do botão, em hexadecimal (#1a7f5a).Um azul-marinho quase preto.
data-ladoesquerda 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:

Cabeçalho
Authorization: Bearer COLE_AQUI_A_SUA_CHAVE

Criar 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)TipoObrigatórioO que é
nometextosimO nome da pessoa.
telefonetextosimCom 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.
emailtextonãoO e-mail.
titulotextonãoO título do negócio. Sem ele, o nome da pessoa.
origemtextonãoDe onde veio, como você quiser chamar (até 60 caracteres). Sem ele, api.
etapa_ididnãoA etapa onde ele entra — tem de ser uma etapa aberta (nem ganho, nem perda). Sem ela, a primeira etapa do funil padrão.
responsavel_ididnãoQuem 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_contatoobjetonãoCampos personalizados do contato, pela chave (veja abaixo).
campos_do_negocioobjetonãoCampos personalizados do negócio, pela chave.
utmobjetonãoA 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.
POST /api/publico/v1/negocios
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 consultaTipoObrigatórioO que é
desdedatanãoData 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.
cursortextonãoO paginacao.proximo da resposta anterior, sem alterar. Continua de onde parou.
limitenúmeronãoQuantos por página: de 1 a 200. Sem ele, 50.
GET /api/publico/v1/negocios
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)TipoObrigatórioO que é
nometextosimO nome da pessoa.
telefonetextosimCom 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.
emailtextonãoO e-mail.
origemtextonãoDe onde veio, como você quiser chamar (até 60 caracteres). Sem ele, api.
camposobjetonãoCampos personalizados do contato, pela chave. Só valem para contato novo.
POST /api/publico/v1/contatos
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 consultaTipoObrigatórioO que é
desdedatanãoData 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.
cursortextonãoO paginacao.proximo da resposta anterior, sem alterar. Continua de onde parou.
limitenúmeronãoQuantos por página: de 1 a 200. Sem ele, 50.
GET /api/publico/v1/contatos
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)TipoObrigatórioO que é
campanhatextosimO nome da campanha, igual ao utm_campaign dos leads dela: é por ele que o gasto encontra a venda.
diadatasimAAAA-MM-DD, o dia do gasto.
custonúmeronãoEm reais, com ponto decimal e sem sinal: 182.40.
cliquesnúmeronãoInteiro.
plataformatextonãometa, google ou o que você usar. Sem ela, manual.
contatextonãoA conta de anúncio, se você tem mais de uma.
campanha_externatextonãoO id da campanha na plataforma (também aceito como campanhaExterna). Sem ele, vale o nome.
lancamentoslistanãoVários lançamentos de uma vez, cada um com os campos acima.
POST /api/publico/v1/investimento
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 consultaTipoObrigatórioO que é
dedatanãoAAAA-MM-DD, o primeiro dia.
atedatanãoAAAA-MM-DD, o último dia.
GET /api/publico/v1/investimento
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 (WordPress e afins)
<?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);
Node.js
// 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.

A página seguinte
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.

StatusQuando
400O corpo não é JSON.
401Sem chave, chave errada ou revogada. A resposta é sempre a mesma, de propósito: ela não diz o que faltou.
403A 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.
409O etapa_id não existe ou não é uma etapa aberta (ou o CRM não tem nenhuma etapa aberta).
422Um campo não passou — nome, telefone, responsavel_id, limite, cursor, desde ou um lançamento. A frase em erro diz qual.
500Falha 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.

EventoQuandoEm `dados`
contato.criadoContato novoid, nome, telefone, email, origem, criado_em
negocio.criadoNegócio novoid, titulo, valor, funil_id, etapa_id, contato_id, responsavel_id, situacao, origem
negocio.mudou_etapaNegócio mudou de etapaid, titulo, valor, funil_id, etapa_id, contato_id, responsavel_id, situacao, origem, etapa_anterior_id
negocio.ganhoNegócio ganhoid, titulo, valor, funil_id, etapa_id, contato_id, responsavel_id, situacao, origem
negocio.perdidoNegócio perdidoid, titulo, valor, funil_id, etapa_id, contato_id, responsavel_id, situacao, origem
proposta.aceitaProposta aceita pelo clientenegocio_id, resposta, nota, total_centavos
proposta.recusadaProposta recusada pelo clientenegocio_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

CampoEmO que é
idcontato.criadoO id do contato.
nomecontato.criadoO nome.
telefonecontato.criadoNo formato internacional.
emailcontato.criadoPode vir nulo.
origemcontato.criadoDe onde ele veio.
criado_emcontato.criadoQuando entrou, ISO 8601.
idnegocio.*O id do negócio.
titulonegocio.*O título.
valornegocio.*Em reais, número.
funil_idnegocio.*O funil.
etapa_idnegocio.*A etapa em que ele está agora.
contato_idnegocio.*O contato.
responsavel_idnegocio.*Quem cuida dele. Pode vir nulo.
situacaonegocio.*open (aberto), won (ganho) ou lost (perdido).
origemnegocio.*De onde ele veio: whatsapp, api, o formulário…
etapa_anterior_idnegocio.*A etapa de onde ele saiu.
negocio_idproposta.*O negócio da proposta.
respostaproposta.*aceita ou recusada.
notaproposta.*O que o cliente escreveu ao responder. Pode vir nulo.
total_centavosproposta.*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:

Corpo de um negocio.ganho
{
  "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çalhoValorQuando
Content-Typeapplication/jsonSempre.
User-AgentCRM-Webhook/1Sempre.
X-Crm-Eventoo evento, como negocio.ganhoSempre — dá para filtrar sem abrir o corpo.
X-Crm-Signaturesha256= e a assinatura em hexadecimalQuando 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.

Node.js
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
<?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 id em dados para não processar duas vezes.