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.
Tags
Os dois caminhos aceitam tags com o código de cada tag. O código aparece ao lado do nome em Configurações → Comercial → Tags, com botão de copiar.
{ "nome": "Maria Silva", "telefone": "+5561999990000", "tags": ["investidor", "feirao-set"] }| Regra | Comportamento |
|---|---|
| Identificador | O código, nunca o nome. Renomear a tag no painel não muda o código, então a integração continua marcando os leads |
| Tag inexistente | Nunca criada pela API. O código volta em tags.ignoradas com motivo: "nao_encontrada" e o lead é cadastrado normalmente |
| Tag desativada | Não é aplicada; volta em tags.ignoradas com motivo: "inativa" |
| Lead que já existe | Com permitir_alteracao: true, as tags enviadas somam às que o lead já tem — nenhuma é removida. Duplicado recusado (409) não recebe tag |
| Limite | Até 20 tags por requisição. Caixa e espaços são ignorados (" Investidor " = investidor) |
A resposta traz o desfecho sempre que a requisição mandou tags:
"tags": {
"aplicadas": ["investidor"],
"ignoradas": [{ "codigo": "feirao-set", "motivo": "nao_encontrada" }]
}No webhook, tags aceita lista JSON ou texto separado por vírgula ("investidor, feirao-set"),
e pode ter um padrão por fonte em static_fields.tags.
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.
Respostas de qualificação (probabilidade de conversão)
A ficha do lead tem um painel Qualificação com seis perguntas que somam até 10 pontos; a soma × 10 é a probabilidade de conversão. Duas dessas respostas podem chegar direto do formulário, e o painel já abre pontuado:
| Campo | Nome alternativo (webhook) | Valores aceitos | Pontos |
|---|---|---|---|
tipo_renda | situacao_trabalho | formal · informal · mista — ou o rótulo do formulário ("Renda Formal", "Renda Informal", "Renda Mista") | Formal 1 · Informal 0 · Mista 2 |
faixa_renda | renda_faixa | ate_2500 · 2501_3500 · 3501_4500 · acima_4500 — ou o rótulo ("Abaixo de R$2.500/mês", "R$2.501 a R$3.500/mês", "R$3.501 a R$4.500/mês", "Acima de R$4.500/mês") | 0 · 1 · 2 · 3 |
O rótulo é lido pelo sentido, não por igualdade exata: variações como "Até R$ 2.500", "Entre 3500 e 4500" ou "CLT" também são reconhecidas. Valor que não se reconhece fica em branco (o lead é criado normalmente e o corpo segue arquivado). Com permitir_alteracao: true, a resposta nova substitui a anterior; numa reentrada sem essa flag, só preenche o que estava vazio.
Esta pontuação é independente de score: score define a temperatura (quente/morno/frio), e as respostas definem a probabilidade de conversão do painel.
{
"nome": "Maria Silva",
"whatsapp": "+5561999990000",
"situacao_trabalho": "Renda Formal",
"renda_faixa": "Abaixo de R$2.500/mês"
}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 |
tipo_renda | string | Não | Tipo de renda: formal, informal, mista ou o rótulo do formulário — ver Respostas de qualificação |
faixa_renda | string | Não | Faixa de renda: ate_2500, 2501_3500, 3501_4500, acima_4500 ou o rótulo do formulário |
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 |
tags | string[] | Não | Códigos das tags a aplicar (máx. 20). Desconhecida/inativa é ignorada sem recusar o lead — ver Tags |
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"
},
"tags": ["investidor", "feirao-set"]
}'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,
"tags": {
"aplicadas": ["investidor", "feirao-set"],
"ignoradas": []
}
}
}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 |
tipo_renda | string | Não | Também aceito como situacao_trabalho. Valor canônico ou rótulo ("Renda Formal") |
faixa_renda | string | Não | Também aceito como renda_faixa. Valor canônico ou rótulo ("Abaixo de R$2.500/mês") |
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 |
tags | string[]/string | Não | Códigos das tags — lista JSON ou texto separado por vírgula. Ver Tags |
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 |