Pular para o conteúdo

Captação de Leads

A captação de leads cria negócios no CRM a partir de um formulário no site do cliente, sem precisar de uma API Key. Cada formulário de captação é configurado no painel Maestron dentro de um funil, aponta para um estágio fixo e gera um token publicável (mcf_...) cuja única capacidade é criar um negócio naquele funil/estágio — ele não lê, não edita e não apaga nada.

No painel Maestron: CRM → abrir um funil → Configurar funil → aba Captação → Adicionar formulário. Ali você define:

  • Etapa alvo, origem, status inicial, responsável e temperatura — resolvidos no servidor, o site nunca os escolhe.
  • Mapa de campos: o name de cada campo do formulário do site → o campo do lead (contact_name, client_email, client_phone, company_name, notes, ou um campo customizado do funil via custom:<chave>).
  • Origens permitidas: lista de domínios que podem enviar. Vazio = qualquer origem.
  • Anti-spam: janela de deduplicação por e-mail e nome do campo honeypot.
  • Redirecionar para: URL de “obrigado” usada quando o envio vem de um <form> HTML puro.
  • Resposta automática: e-mail de confirmação ao lead, sem marca Maestron, com assunto/corpo/Reply-To que você define.
  • Webhook de saída: uma URL https:// que recebe cada lead como POST JSON assinado.
  • Pausar automaticamente: teto de envios/hora — acima dele o formulário é desativado e o responsável avisado. O painel também alerta quando um formulário com tráfego para de receber envios (tag removida do site).

O painel mostra métricas por formulário (envios, leads, spam, conversão) e o log dos últimos envios — os name dos campos recebidos, para depurar o mapeamento.

Gerenciar formulários exige a permissão crm.intake.manage no painel. Regenerar o token invalida o anterior na hora.

Todos os caminhos abaixo partem de https://api.maestron.com.br/api/v1. Nenhum exige header Authorization — a identidade vem do token na URL.

POST /crm/intake/{token}

Enviar um lead

Cria um negócio no funil/estágio do formulário. Aceita JSON (integração / tag JS) ou form-urlencoded (formulário HTML puro).

Parâmetros de caminho

CampoTipoObrigatórioDescrição
token string Sim Token publicável do formulário (mcf_...).

Corpo (body)

CampoTipoObrigatórioDescrição
contact_name string — Nome do contato. Máx. 200. Ausente, cai no e-mail ou telefone.
client_email string — E-mail do contato. Máx. 200.
client_phone string — Telefone do contato. Máx. 50.
company_name string — Empresa do contato. Máx. 200.
notes string — Observações. Máx. 5000.
expected_close_date string (data) — Previsão de fechamento, YYYY-MM-DD. Só aceito se mapeado no formulário.
page_url string — URL da página onde o formulário foi enviado. Guardado na auditoria, não no lead.
referrer string — Referrer do navegador. Guardado na auditoria.
utm object — Objeto com chaves de campanha (source, medium, campaign, term, content, gclid, fbclid). Guardado na auditoria.
curl -X POST https://api.maestron.com.br/api/v1/crm/intake/{token} \
  -H "Content-Type: application/json" \
  -d '{
    "contact_name": "Maria Silva",
    "client_email": "maria@acme.com.br",
    "client_phone": "+55 11 99999-0000",
    "company_name": "Acme Ltda",
    "page_url": "https://acme.com.br/contato",
    "utm": {
      "source": "google",
      "medium": "cpc",
      "campaign": "institucional"
    }
  }'
<?php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.maestron.com.br/api/v1/crm/intake/{token}', [
    'headers' => ['Accept' => 'application/json'],
    'json' => [
        'contact_name' => 'Maria Silva',
        'client_email' => 'maria@acme.com.br',
        'client_phone' => '+55 11 99999-0000',
        'company_name' => 'Acme Ltda',
        'page_url' => 'https://acme.com.br/contato',
        'utm' => [object Object],
    ],
]);
$data = json_decode($response->getBody(), true);
import requests
headers = {}
payload = {
    "contact_name": "Maria Silva",
    "client_email": "maria@acme.com.br",
    "client_phone": "+55 11 99999-0000",
    "company_name": "Acme Ltda",
    "page_url": "https://acme.com.br/contato",
    "utm": [object Object],
}
response = requests.post(
    "https://api.maestron.com.br/api/v1/crm/intake/{token}",
    headers=headers,
    json=payload,
)
data = response.json()
const response = await fetch("https://api.maestron.com.br/api/v1/crm/intake/{token}", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "contact_name": "Maria Silva",
    "client_email": "maria@acme.com.br",
    "client_phone": "+55 11 99999-0000",
    "company_name": "Acme Ltda",
    "page_url": "https://acme.com.br/contato",
    "utm": {
      "source": "google",
      "medium": "cpc",
      "campaign": "institucional"
    }
  }),
});
const data = await response.json();
using var client = new HttpClient();
var payload = new StringContent(
    "{\"contact_name\":\"Maria Silva\",\"client_email\":\"maria@acme.com.br\",\"client_phone\":\"+55 11 99999-0000\",\"company_name\":\"Acme Ltda\",\"page_url\":\"https://acme.com.br/contato\",\"utm\":{\"source\":\"google\",\"medium\":\"cpc\",\"campaign\":\"institucional\"}}",
    System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.maestron.com.br/api/v1/crm/intake/{token}", payload);
var data = await response.Content.ReadAsStringAsync();
POST /crm/intake/{token} HTTP/1.1
Host: api.maestron.com.br
Content-Type: application/json

{
  "contact_name": "Maria Silva",
  "client_email": "maria@acme.com.br",
  "client_phone": "+55 11 99999-0000",
  "company_name": "Acme Ltda",
  "page_url": "https://acme.com.br/contato",
  "utm": {
    "source": "google",
    "medium": "cpc",
    "campaign": "institucional"
  }
}

Respostas

{
  "message": "Lead recebido.",
  "data": {
    "id": "f1a2b3c4-5d6e-4f7a-8b9c-0d1e2f3a4b5c"
  }
}
{
  "message": "Recebido.",
  "data": null
}
"Location: https://acme.com.br/obrigado"
{
  "message": "Os dados informados são inválidos.",
  "data": null,
  "errors": {
    "contact_name": [
      "Informe ao menos nome, e-mail ou telefone."
    ]
  }
}
{
  "message": "Origem não autorizada.",
  "data": null
}
{
  "message": "Recurso não encontrado.",
  "data": null
}
{
  "message": "Muitos envios. Aguarde um momento antes de tentar novamente.",
  "data": null
}

Regras:

  • Pelo menos um de contact_name, client_email ou client_phone é obrigatório — senão 422.
  • Campos fora do padrão (telefone, empresa, …) são traduzidos pelo mapa de campos do formulário antes da validação. Campos com nome canônico não precisam de mapa.
  • funnel_id, stage_id, status, source, assignee_id e contact_date não são aceitos no corpo — vêm do formulário, resolvidos no servidor.
  • O corpo pode ser application/json ou application/x-www-form-urlencoded. Com Accept: text/html e um redirect_url configurado, a resposta é 302 para essa URL.

O corpo do lead usa nomes canônicos: contact_name, client_email, client_phone, company_name, notes. Um formulário de site raramente usa esses nomes — usa nome, seu-email, telefone-2, mensagem. O mapa de campos liga um ao outro.

A chave do mapa é o atributo name do campo no HTML do site — não o id, não o label, não o texto do placeholder. É o name porque é isso que o navegador envia no corpo da requisição (e o que o FormData lê na tag JS).

Exemplo — formulário no site:

<form>
<input name="nome-completo" />
<input name="e-mail" type="email" />
<input name="fone" type="tel" />
<textarea name="obs"></textarea>
</form>

Mapa configurado no painel (ou o efeito equivalente ao enviar já com os nomes canônicos):

name no siteCampo do lead
nome-completocontact_name
e-mailclient_email
foneclient_phone
obsnotes

Campos que podem ser alvo: contact_name, client_email, client_phone, company_name, notes, expected_close_date (data YYYY-MM-DD, ex.: de um <input type="date">) e qualquer campo customizado do funil no formato custom:<chave> (ex.: custom:numero_de_lojas) — os campos são definidos no painel em Configurar funil → Campos. O valor mapeado a um custom: entra estruturado em custom_fields do negócio (ver Negócios → Campos customizados), não nas observações. Na captação a coerção é tolerante: valor que não encaixa no tipo é descartado, nunca gera 422.

Regras:

  • Campos que já usam o nome canônico não precisam de entrada no mapa — passam direto.
  • A tradução acontece antes da validação: 422 por “sem canal de contato” só ocorre se, depois de aplicar o mapa, ainda não há contact_name, client_email nem client_phone.
  • Se contact_name ficar vazio mas houver e-mail ou telefone, o negócio é criado com o contact_name preenchido a partir do e-mail (ou, na falta dele, do telefone) — o CRM nunca fica com um card sem rótulo.
  • Campos não mapeados não se perdem. Qualquer outro campo do site (cargo, faturamento, como nos conheceu…) é anexado às observações do negócio num bloco — Enviado pelo formulário —, uma linha por campo. Limite: 20 campos, 500 caracteres cada. O contexto de campanha (page_url, referrer, utm) tem tratamento próprio (vai para a auditoria, não para as observações).

A tag intake.js ainda tem uma autodetecção que dispensa o mapa nos casos óbvios (campo type="email" → client_email, type="tel" → client_phone, name contendo nome/empresa, <textarea> → notes). A precedência é: data-maestron-field="..." no input → mapa de campos → autodetecção.

Duas proteções configuradas por formulário, ambas retornando 202 quando disparam — o cliente (ou o bot) não distingue de um envio aceito.

Janela anti-duplicidade (dedupe_minutes, padrão 10): se um lead com o mesmo client_email já entrou no mesmo funil dentro dos últimos N minutos, o envio retorna 202 e nenhum negócio novo é criado — o contador de envios do formulário também não sobe. Cobre o visitante que clica “Enviar” duas vezes, recarrega a página de agradecimento ou volta e reenvia, e o bot que repete o mesmo payload. 0 desativa a janela. Envios sem client_email nunca são deduplicados.

Honeypot (honeypot_field, padrão mkt__hp): um campo de texto escondido. A tag intake.js injeta esse campo em cada formulário; um humano nunca o vê nem o preenche, um bot que preenche todos os campos, sim. Se o campo vier preenchido, a resposta é 202 e o envio é descartado em silêncio. Em um formulário HTML montado à mão, adicione você o campo escondido com esse name para ter a mesma proteção.

GET /crm/intake/{token}/config

Ler a configuração do formulário

Consumido pela tag intake.js para se autoconfigurar. Não expõe origens permitidas, responsável nem contadores.

Parâmetros de caminho

CampoTipoObrigatórioDescrição
token string Sim Token publicável do formulário (mcf_...).
curl -X GET https://api.maestron.com.br/api/v1/crm/intake/{token}/config
<?php
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.maestron.com.br/api/v1/crm/intake/{token}/config', [
    'headers' => ['Accept' => 'application/json'],
]);
$data = json_decode($response->getBody(), true);
import requests
headers = {}
response = requests.get(
    "https://api.maestron.com.br/api/v1/crm/intake/{token}/config",
    headers=headers,
)
data = response.json()
const response = await fetch("https://api.maestron.com.br/api/v1/crm/intake/{token}/config", {
  method: "GET",
  headers: {
  },
});
const data = await response.json();
using var client = new HttpClient();
var response = await client.GetAsync("https://api.maestron.com.br/api/v1/crm/intake/{token}/config");
var data = await response.Content.ReadAsStringAsync();
GET /crm/intake/{token}/config HTTP/1.1
Host: api.maestron.com.br

Respostas

{
  "message": "Configuração do formulário.",
  "data": {
    "selector": "form",
    "field_map": {
      "telefone": "client_phone",
      "empresa": "company_name"
    },
    "honeypot_field": "mkt__hp",
    "capture_utm": true,
    "success_message": "Recebemos seu contato!"
  }
}
{
  "message": "Recurso não encontrado.",
  "data": null
}
GET /crm/intake/{version}/intake.js

Baixar a tag de captação

Script estático versionado na URL (estilo js.stripe.com/v3). Cacheável — o CDN fica na frente. Versão atual: 1.0.

Parâmetros de caminho

CampoTipoObrigatórioDescrição
version string Sim Versão do script no formato N.N (ex.: 1.0). Versão desconhecida retorna 404.
curl -X GET https://api.maestron.com.br/api/v1/crm/intake/{version}/intake.js
<?php
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.maestron.com.br/api/v1/crm/intake/{version}/intake.js', [
    'headers' => ['Accept' => 'application/json'],
]);
$data = json_decode($response->getBody(), true);
import requests
headers = {}
response = requests.get(
    "https://api.maestron.com.br/api/v1/crm/intake/{version}/intake.js",
    headers=headers,
)
data = response.json()
const response = await fetch("https://api.maestron.com.br/api/v1/crm/intake/{version}/intake.js", {
  method: "GET",
  headers: {
  },
});
const data = await response.json();
using var client = new HttpClient();
var response = await client.GetAsync("https://api.maestron.com.br/api/v1/crm/intake/{version}/intake.js");
var data = await response.Content.ReadAsStringAsync();
GET /crm/intake/{version}/intake.js HTTP/1.1
Host: api.maestron.com.br

Respostas

"// JavaScript — Content-Type: application/javascript; Cache-Control: public, max-age=3600"
""

Cole no <head> do site (funciona como Custom HTML tag no Google Tag Manager):

<script async
src="https://api.maestron.com.br/api/v1/crm/intake/1.0/intake.js"
data-maestron-token="mcf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"></script>

O script:

  1. Lê a configuração em /crm/intake/{token}/config — o mapa de campos é definido no painel, o site não precisa mudar quando o mapeamento muda.
  2. Encontra os <form> da página (ignora os que têm data-maestron-ignore) e injeta o campo honeypot.
  3. No submit, monta o payload a partir dos campos (mapa + autodetecção de email/tel/nome/empresa, ou data-maestron-field="contact_name" no input), anexa page_url, referrer e UTMs, e faz POST para /crm/intake/{token} com keepalive — sem bloquear o envio nativo do formulário.
  4. Dispara o evento maestron:lead no window (detail: { ok, status, leadId, message }) para o site engatar analytics ou uma tela de obrigado.

Para bloquear o envio nativo e aguardar a resposta, adicione data-maestron-block ao <form>.

Quando a página não tem um <form> nativo, use maestron('submit', payload) — devolve uma Promise que nunca rejeita e resolve com { ok, status, leadId, errors }. Ele já anexa page_url, referrer e UTMs.

// React
import { useState } from 'react';
function ContactForm() {
const [sent, setSent] = useState(false);
async function onSubmit(e) {
e.preventDefault();
const form = new FormData(e.currentTarget);
const res = await window.maestron('submit', {
contact_name: form.get('name'),
client_email: form.get('email'),
notes: form.get('message'),
});
if (res.ok) setSent(true);
}
return sent ? <p>Recebemos seu contato!</p> : (
<form onSubmit={onSubmit}>
<input name="name" required />
<input name="email" type="email" />
<textarea name="message" />
<button>Enviar</button>
</form>
);
}
<!-- Vue 3 -->
<script setup>
import { ref } from 'vue'
const sent = ref(false)
async function submit(e) {
const f = new FormData(e.target)
const res = await window.maestron('submit', {
contact_name: f.get('name'),
client_email: f.get('email'),
})
if (res.ok) sent.value = true
}
</script>
<template>
<p v-if="sent">Recebemos seu contato!</p>
<form v-else @submit.prevent="submit">
<input name="name" required />
<input name="email" type="email" />
<button>Enviar</button>
</form>
</template>

maestron('track', payload) continua existindo para o caso “envia e esquece” (sem aguardar a resposta).

<form action="https://api.maestron.com.br/api/v1/crm/intake/mcf_xxxxxxxx" method="POST">
<input type="text" name="contact_name" placeholder="Seu nome" />
<input type="email" name="client_email" placeholder="Seu e-mail" />
<input type="tel" name="client_phone" placeholder="Seu telefone" />
<button type="submit">Enviar</button>
</form>

Com um redirect_url configurado no formulário, o navegador é redirecionado para lá após o envio (só http(s) — javascript:/data: são recusados). Sem redirect_url, a resposta é uma página mínima de confirmação, sem marca.

Se o cliente não quer construir HTML, o Maestron hospeda o formulário. A página é sem marca (só o nome da empresa), responsiva, com honeypot embutido.

  • Link direto: https://api.maestron.com.br/api/v1/crm/intake/{token}/form — dá pra compartilhar como está. Aceita ?title=Fale%20conosco e ?accent=16a34a (hex de 6 dígitos).
  • Embed:
<iframe src="https://api.maestron.com.br/api/v1/crm/intake/mcf_xxxxxxxx/form"
title="Formulário de contato"
style="width:100%;max-width:480px;height:560px;border:0;" loading="lazy"></iframe>

Os campos vêm do mapa de campos do formulário (mais nome/e-mail/telefone sempre). O envio usa o mesmo endpoint e respeita o redirect_url.

Se auto_reply estiver ligado no formulário e o envio trouxer client_email, o lead recebe um e-mail de confirmação sem marca Maestron — o nome do remetente é a empresa do cliente e o corpo é o texto configurado (auto_reply_subject / auto_reply_body). O Reply-To aponta para reply_to_email (ou, na falta, para o responsável / dono da empresa), então quando o lead responde a mensagem cai no time do cliente.

Quando o envio traz contexto de campanha (utm.*, gclid, fbclid, page_url, referrer), o negócio criado carrega um objeto attribution:

{
"source": "google", "medium": "cpc", "campaign": "institucional",
"gclid": "abc123", "landing_page": "https://cliente.com.br/lp/x",
"referrer": "https://www.google.com/", "via": "intake_form"
}

A tag intake.js preenche isso sozinha. attribution aparece no GET /v1/crm/deals/{id} e no corpo do webhook.

Cada formulário pode ter uma URL de webhook (https://). A cada lead capturado, o Maestron faz um POST JSON assinado para ela — sem passar por Zapier.

{
"event": "lead.captured",
"occurred_at": "2026-09-08T12:00:00-03:00",
"form": { "id": "…", "name": "LP Black Friday" },
"lead": {
"id": "…", "funnel_id": "…", "stage_id": "…",
"contact_name": "Maria Silva", "company_name": "Acme",
"client_email": "maria@acme.com.br", "client_phone": "+55 11 99999-0000",
"source": "site", "status": "em_contato", "notes": "…",
"attribution": { "source": "google", "via": "intake_form" },
"created_at": "2026-09-08T12:00:00-03:00"
}
}

Headers: X-Maestron-Event, X-Maestron-Delivery (id único da entrega), e X-Maestron-Signature: sha256=<hmac> — HMAC-SHA256 do corpo bruto com o segredo do webhook (gerado no painel junto com a URL, prefixo whsec_). Verifique antes de confiar no payload:

$expected = 'sha256='.hash_hmac('sha256', file_get_contents('php://input'), $secret);
if (! hash_equals($expected, $_SERVER['HTTP_X_MAESTRON_SIGNATURE'] ?? '')) { http_response_code(401); exit; }

Entrega: responda 2xx para confirmar. 4xx (exceto 429) = erro definitivo, sem retry. 5xx / 429 / timeout (5s) = reentrega com backoff exponencial (até 6 tentativas em 24h). O painel tem um botão de teste que dispara um event: "ping".

  • Rate limit: 20 envios/minuto por token e 10/minuto por IP. Ao estourar, 429 com Retry-After.
  • Origens: se o formulário tem lista de origens permitidas, um Origin fora dela recebe 403 — validado no servidor, independente do CORS do navegador. O painel recusa domínios conhecidos (google, facebook, youtube, o próprio maestron.com.br…) nessa lista — quase sempre erro de digitação; use o domínio do site do cliente.
  • CORS: o endpoint responde a qualquer origem (Access-Control-Allow-Origin reflete o Origin), sem credenciais — a leitura da resposta pelo navegador funciona de qualquer site.
  • Token: revogável e regenerável a qualquer momento no painel; o token antigo para de responder na hora.
  • Erro interno nunca vaza detalhe para o cliente — apenas o envelope genérico.