Leads (Oportunidades)
Enviar leads para o funil comercial via API ou webhooks de entrada, com rastreabilidade de origem, canal, mídia e campanha.
Leads (Oportunidades)
Envie leads para o funil comercial do OctaBuild por dois caminhos:
| Caminho | Autenticação | Uso típico |
|---|---|---|
POST /v1/oportunidades | API Key (Authorization: Bearer) | Integrações servidor-a-servidor |
POST /api/webhooks/inbound/{token} | Token na URL + segredo opcional | Typebot, Zapier, n8n, landing pages, parceiros |
Ambos criam a pessoa (lead), a oportunidade, o touchpoint de conversão e disparam a distribuição automática (roleta) numa única transação — a menos que a requisição peça o contrário (ver Distribuição abaixo).
O corpo da requisição segue um contrato fixo — as mesmas chaves para todas as fontes e todos os tenants, no nível raiz do JSON. Não há mapeamento de campos por webhook: a fonte deve enviar os nomes de campo documentados aqui.
Rastreabilidade
Todo lead carrega uma hierarquia de atribuição:
| Nível | O que responde | Comportamento na API |
|---|---|---|
| Origem | Quem cadastrou (Corretor, Marketing, SDR, Correspondente, Gestor, Imobiliária Parceira) | Nunca criada pela API. No webhook, vem do cadastro da fonte; na API v1, o campo origin aceita um slug existente — inválido/ausente cai em marketing. |
| Canal | Onde foi captado (Instagram, Google, Site, OLX…) | Nunca criado pela API. Resolvido por slug ou nome contra o catálogo do painel; desconhecido → fica vazio (o texto bruto é preservado no touchpoint). |
| Mídia | Como foi captado (Mídia Paga, Mídia Orgânica) | Nunca criada pela API. Mesma resolução do canal. |
| Campanha → Conjunto → Anúncio | Qual campanha/conjunto/anúncio trouxe o lead | Criados automaticamente por nome (get-or-create). ad_id externo (Meta/Google) deduplica o anúncio. |
Os catálogos de Origem, Canal e Mídia são gerenciados em Configurações → Comercial → Origens & Canais.
Distribuição
Por padrão todo lead novo entra nas roletas ativas, que sorteiam o responsável. Dois campos, aceitos pelos dois caminhos, mudam esse comportamento:
| Campo | Tipo | Padrão | Efeito |
|---|---|---|---|
distribuir | boolean | true | false mantém o lead fora de toda roleta — ele é criado sem responsável e fica disponível para atribuição manual |
sdr_id | uuid | — | Atribui o lead direto a um usuário da organização, preenchendo SDR e responsável |
Enviar sdr_id já dispensa a roleta: a atribuição explícita substitui o sorteio,
mesmo com distribuir: true. Sem isso a roleta consumiria a vaga de um
participante sem conseguir atribuir o lead.
O sdr_id é o id do usuário (o mesmo do painel), e ele precisa ser membro da organização — caso contrário a requisição é recusada com 400 e nada é gravado. Leads com distribuir: false e sem sdr_id ficam sem responsável até alguém assumi-los no painel.
Lead duplicado e permitir_alteracao
Telefone, e-mail e CPF são exclusivos entre oportunidades ativas da organização. Por padrão, reenviar um lead já cadastrado é recusado com 409 — nenhum dado do lead existente é alterado (a re-entrada fica registrada como conversão, contando no dashboard).
Envie permitir_alteracao: true (aceito pelos dois caminhos) para que a requisição atualize o lead existente em vez de ser recusada:
| Comportamento | permitir_alteracao ausente/false | permitir_alteracao: true |
|---|---|---|
| Resposta | 409 com duplicate_field e o número da oportunidade | 200 com updated: true e mensagem de sucesso |
| Dados do lead | intactos | atualizados incrementalmente |
| Conversão | registrada | registrada |
Atualização incremental significa: só os campos presentes na requisição são gravados (o valor enviado substitui o atual); campo ausente preserva o que já estava lá. O cônjuge enviado é criado/vinculado normalmente.
O que nunca muda nesse caminho: fase, status, responsável/SDR (o lead não é redistribuído nem troca de dono) e a atribuição de primeiro toque — origem, canal, mídia, campanha e ponto de captação só são preenchidos se estiverem vazios. A atribuição da nova entrada fica no touchpoint da conversão.
A exclusividade continua valendo: se o telefone, e-mail ou CPF enviado pertencer a
outra oportunidade ativa, a atualização é abortada com 400 e a mensagem indica
o número da oportunidade em conflito — nada é gravado.
{
"success": true,
"data": {
"oportunidade_id": "550e8400-e29b-41d4-a716-446655440000",
"numero": 169,
"created": false,
"updated": true,
"duplicate_field": "telefone",
"message": "Já existia a oportunidade #169 cadastrada com este telefone — dados atualizados com sucesso."
}
}No webhook, permitir_alteracao também aceita sim/não, 1/0, e pode virar padrão
da fonte pelos campos fixos do cadastro do webhook.
Lead scoring e temperatura
Formulários de qualificação (landing page, quiz, calculadora) normalmente já calculam uma nota para o lead. Envie essa nota em score — aceito pelos dois caminhos — e o OctaBuild registra a pontuação e classifica o lead automaticamente.
A escala esperada é 0 a 100, inteiro. A faixa (temperatura) é derivada da nota:
| Nota | Temperatura |
|---|---|
| ≥ 50 | 🔥 Quente |
| 20 a 49 | 🌤 Morno |
| < 20 | ❄️ Frio |
A temperatura aparece como etiqueta na lista de oportunidades e no kanban, e serve de critério de ordenação para o time comercial priorizar o atendimento.
Sem score, o lead entra com a nota padrão 10 (frio) — o mesmo comportamento de antes desta funcionalidade, então integrações existentes não mudam.
A nota é registrada por entrada (touchpoint), e a pontuação da oportunidade é a
soma das notas de todas as entradas. Um lead que preenche o formulário duas vezes
com nota 40 termina com 80 — e sobe de morno para quente. Se a origem reenvia o mesmo
lead com frequência, prefira distribuir: false ou trate o reenvio como atualização
para não inflar a pontuação.
O corpo inteiro do formulário continua arquivado no lead, então as respostas que geraram a nota ficam disponíveis para auditoria mesmo que só a nota seja exibida na tela.
{
"nome": "Maria Silva",
"telefone": "+5561999990000",
"score": 72
}No webhook, score também aceita os nomes alternativos lead_score, pontuacao e nota, valor em texto ("72"), e decimal — que é arredondado. Valor negativo ou não numérico é ignorado e o lead cai no padrão 10.
POST /v1/oportunidades
Cria (ou detecta duplicidade de) uma oportunidade. Requer API Key.
Body
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
nome | string | Sim | Nome do lead |
telefone | string | Não | Telefone (normalizado para E.164 BR) |
email | string | Não | E-mail válido |
empreendimento_id | uuid | Não | Empreendimento de interesse |
unidade_id | uuid | Não | Unidade de interesse |
observacoes | string | Não | Texto livre |
documento | string | Não | CPF (somente dígitos ou formatado) |
data_nascimento | string | Não | Data YYYY-MM-DD |
estado_civil | string | Não | Estado civil (ex.: casado) |
profissao | string | Não | Profissão |
renda_mensal | number | Não | Renda mensal |
tem_fgts | boolean | Não | Possui FGTS |
tem_dependente | boolean | Não | Possui dependentes |
rg | string | Não | RG |
cep | string | Não | CEP do endereço |
logradouro | string | Não | Logradouro |
numero | string | Não | Número |
complemento | string | Não | Complemento |
bairro | string | Não | Bairro |
cidade | string | Não | Cidade |
uf | string | Não | UF (2 letras) |
conjuge | objeto | Não | Dados do cônjuge (leads casados) — ver abaixo |
utm_source | string | Não | UTM de origem |
utm_medium | string | Não | UTM de mídia |
utm_campaign | string | Não | UTM de campanha |
utm_content | string | Não | UTM de conteúdo |
utm_term | string | Não | UTM de termo |
ad_id | string | Não | Id externo do anúncio (Meta/Google) — deduplica o anúncio na árvore |
form_id | string | Não | Id externo do formulário |
referrer_url | string | Não | URL de referência |
landing_url | string | Não | URL da landing page |
origin | string | Não | Slug de uma Origem existente (ex.: marketing). Inválido → marketing |
channel | string | Não | Slug ou nome de um Canal existente (ex.: instagram). Fallback: utm_source |
media_type | string | Não | Slug ou nome de uma Mídia existente (ex.: midia_paga) |
campaign_name | string | Não | Nome da campanha (get-or-create). Fallback: utm_campaign |
ad_set_name | string | Não | Nome do conjunto de anúncios (get-or-create, dentro da campanha) |
ad_name | string | Não | Nome do anúncio (get-or-create) |
capture_point | string | Não | Nome de um Ponto de Captação existente (nunca criado pela API) |
capture_location | string | Não | Local de captação em texto livre (endereço) |
capture_lat | number | Não | Latitude (-90 a 90) |
capture_lng | number | Não | Longitude (-180 a 180) |
score | number | Não | Nota do formulário de qualificação, inteiro ≥ 0 (escala 0–100). Define a temperatura do lead. Padrão 10 — ver Lead scoring abaixo |
distribuir | boolean | Não | Envia o lead para as roletas ativas. Padrão true; false = sem roleta |
sdr_id | uuid | Não | Usuário atribuído como SDR/responsável — dispensa a roleta |
permitir_alteracao | boolean | Não | Padrão false. true atualiza o lead já cadastrado em vez de recusar com 409 |
Objeto conjuge
Quando o lead é casado, envie os dados do cônjuge — o OctaBuild cria (ou reusa,
por CPF e depois telefone) o cadastro da pessoa e a vincula à oportunidade como
conjuge:
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
conjuge.nome | string | Sim (dentro do objeto) | Nome do cônjuge |
conjuge.telefone | string | Não | Telefone |
conjuge.email | string | Não | |
conjuge.documento | string | Não | CPF |
conjuge.rg | string | Não | RG |
conjuge.data_nascimento | string | Não | Data YYYY-MM-DD |
conjuge.profissao | string | Não | Profissão |
Request
curl -X POST "https://api.octabuild.ai/v1/oportunidades" \
-H "Authorization: Bearer ea_live_sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"nome": "Maria Silva",
"telefone": "+5561999990000",
"email": "maria@email.com",
"channel": "instagram",
"media_type": "midia_paga",
"campaign_name": "Lançamento Jardins",
"ad_set_name": "Publico frio DF",
"ad_name": "Video 30s",
"utm_source": "instagram",
"utm_medium": "paid_social",
"utm_campaign": "lancamento-jardins",
"documento": "123.456.789-00",
"estado_civil": "casado",
"renda_mensal": 8500,
"cep": "71900-000",
"logradouro": "Rua das Palmeiras",
"numero": "120",
"bairro": "Águas Claras",
"cidade": "Brasília",
"uf": "DF",
"conjuge": {
"nome": "João Silva",
"telefone": "+5561988880000",
"documento": "987.654.321-00"
}
}'Response — 201 Created (ou 200 se já existia em modo merge)
{
"success": true,
"data": {
"oportunidade_id": "550e8400-e29b-41d4-a716-446655440000",
"numero": 42,
"created": true
}
}Response — 409 Conflict (duplicado)
Telefone, e-mail ou CPF já usados em oportunidade ativa bloqueiam o cadastro (nada é gravado). Para atualizar o lead existente em vez de receber o erro, envie permitir_alteracao: true (ver Lead duplicado acima):
{
"success": false,
"error": {
"code": "DUPLICATE_TELEFONE",
"message": "Já existe uma oportunidade ativa com este telefone (#42)."
}
}Códigos possíveis: DUPLICATE_TELEFONE, DUPLICATE_EMAIL, DUPLICATE_CPF, VALIDATION_ERROR (400), RATE_LIMITED (429).
POST /api/webhooks/inbound/{token}
Webhook de entrada para fontes low-code (Typebot, Zapier, n8n, landing pages). Crie a fonte em Configurações → Integrações → Webhooks — a URL com o token é exibida na criação.
- Origem e Canal dos leads desta fonte são definidos no cadastro do webhook (não no corpo).
- Se a fonte tiver segredo, envie o header
x-webhook-secret. - Rate limit: 120 requisições/minuto por fonte.
Body (contrato fixo)
Além dos campos de atribuição (utm_*, ad_id, form_id, referrer_url, landing_url, campaign_name, ad_set_name, ad_name — mesmos significados da API v1), o webhook aceita campos de qualificação:
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
nome | string | Um entre nome/telefone/email | Nome do lead |
telefone | string | ↑ | Telefone |
email | string | ↑ | |
documento | string | Não | CPF (somente dígitos ou formatado) |
data_nascimento | string | Não | Data YYYY-MM-DD |
estado_civil | string | Não | Estado civil |
profissao | string | Não | Profissão |
renda_mensal | string/number | Não | Renda mensal |
tem_fgts | string/boolean | Não | Aceita sim/não, true/false, 1/0 |
tem_dependente | string/boolean | Não | Idem |
rg | string | Não | RG |
cep | string | Não | CEP do endereço |
logradouro | string | Não | Logradouro |
numero | string | Não | Número |
complemento | string | Não | Complemento |
bairro | string | Não | Bairro |
cidade | string | Não | Cidade |
uf | string | Não | UF (2 letras) |
empreendimento_id | uuid | Não | Empreendimento de interesse |
unidade_id | uuid | Não | Unidade de interesse |
observacoes | string | Não | Texto livre |
conjuge_nome | string | Não | Nome do cônjuge — presente, ativa o vínculo do cônjuge |
conjuge_telefone | string | Não | Telefone do cônjuge |
conjuge_email | string | Não | E-mail do cônjuge |
conjuge_documento | string | Não | CPF do cônjuge |
conjuge_rg | string | Não | RG do cônjuge |
conjuge_data_nascimento | string | Não | Data YYYY-MM-DD |
conjuge_profissao | string | Não | Profissão do cônjuge |
score | string/number | Não | Nota do formulário de qualificação (escala 0–100). Aliases aceitos: lead_score, pontuacao, nota. Padrão 10 — ver Lead scoring abaixo |
distribuir | string/boolean | Não | Envia o lead para as roletas ativas. Padrão true; aceita sim/não, true/false, 1/0 |
sdr_id | uuid | Não | Usuário atribuído como SDR/responsável — dispensa a roleta |
permitir_alteracao | string/boolean | Não | Padrão false. true atualiza o lead já cadastrado em vez de recusar com 409 |
No webhook os dados do cônjuge são chaves planas com prefixo conjuge_
(o contrato é raso). Na API v1, use o objeto aninhado conjuge.
O corpo inteiro da requisição é arquivado no lead para auditoria (payload do touchpoint), então campos extras não são perdidos — apenas não são estruturados.
Request
curl -X POST "https://app.octabuild.ai/api/webhooks/inbound/wh_seu_token_aqui" \
-H "Content-Type: application/json" \
-H "x-webhook-secret: whsec_seu_segredo" \
-d '{
"nome": "Maria Silva",
"telefone": "+5561999990000",
"email": "maria@email.com",
"utm_source": "instagram",
"utm_campaign": "lancamento-jardins",
"campaign_name": "Lançamento Jardins"
}'Response
201 (criado) / 200 (fundido ou atualizado) com { success, data: { oportunidade_id, numero, created, updated } }, ou:
| Status | Motivo |
|---|---|
400 | JSON inválido, payload sem nome/telefone/email, sdr_id que não é usuário da organização, ou identificador em conflito com outra oportunidade na atualização |
401 | Segredo inválido |
404 | Token inexistente ou fonte inativa |
409 | Lead duplicado (error, duplicate_field, oportunidade_numero) — evitável com permitir_alteracao: true |
429 | Rate limit excedido |