Guia do Desenvolvedor
Conecte qualquer diagnóstico externo à Leadble para capturar leads, rastrear o funil e registrar pagamentos — tudo em um só lugar.
Funil completo
Da visualização ao pagamento, cada etapa é registrada.
Dados limpos
Validação e normalização rigorosa antes de persistir.
Idempotente
Retries seguros — sem duplicatas de leads ou pagamentos.
Como funciona:
- 1 Crie o diagnóstico no painel e publique-o para obter o
public_key. - 2 Gere uma Chave de API em Configurações → Chaves de API.
- 3 No frontend use o SDK; para vender o premium, redirecione ao Checkout Leadble ou registre pagamentos de gateway externo via REST.
- 4 Leads, sessões e pagamentos aparecem automaticamente no painel Leadble.
Pré-requisitos
Antes de começar a integração, você precisa de três coisas:
Diagnóstico publicado
O diagnóstico precisa estar com status Publicado no painel. Ao publicar, um public_key (formato dlg_...) é gerado automaticamente.
Chave de API do workspace
Acesse Configurações → Chaves de API e crie uma chave. Ela é exibida uma única vez — guarde em variável de ambiente. Formato: lb_sk_...
Frontend com JavaScript
O SDK funciona em qualquer projeto JS/TS (React, Vue, Svelte, Next, Nuxt, HTML puro). Para integrações server-side, use os endpoints REST diretamente.
Autenticação
Todos os endpoints da Capture API requerem dois headers em cada requisição:
Authorization: Bearer lb_sk_a1b2c3d4e5f6...
X-Diagnostic-Key: dlg_k9xm2pqrabcdAuthorization — Chave de API do workspace, prefixo lb_sk_ Gerada em Configurações → Chaves de API. Armazene como variável de ambiente (VITE_LEADBLE_API_KEY).
X-Diagnostic-Key — Identificador público do diagnóstico, prefixo dlg_Gerado automaticamente quando o diagnóstico é publicado. Pode ser exposto no frontend (não é um segredo). Também aceita o UUID interno.
lb_sk_) é um segredo. Para frontends client-side, use o SDK que a mantém em variáveis de ambiente do build. Nunca exponha a chave em HTML estático. SDK ou REST direto?
O SDK e os endpoints REST não são alternativas excludentes — o SDK é apenas um wrapper conveniente em cima dos mesmos 4 endpoints. A maioria dos projetos usa os dois.
Frontend do diagnóstico (browser)
Use o SDK @leadble/capture. Ele gerencia visitorId, captura UTMs, faz batching de eventos e retry automaticamente.
lb.init() → lb.track() → lb.completeLead()
Checkout Leadble (Pix / cartão)
Redirecione para checkout.app.leadble.com.br. O pagamento é processado e registrado automaticamente — confirme com GET /sessions/:id ao retornar.
checkout.app.leadble.com.br/c/{publicKey}?session=...&return=...
Webhook do gateway externo (servidor)
Use o REST direto no seu backend quando integrar Stripe, Hotmart ou outro gateway próprio. A confirmação de pagamento deve ser processada server-side, nunca pelo browser do usuário.
POST /api/v1/capture/payments ← webhook do gateway
| Cenário | Recomendação | Motivo |
|---|---|---|
| Checkout Leadble (Pix/cartão na isca) | Redirecionar | checkout.app.leadble.com.br processa e registra; confirme com GET /sessions/:id. |
| Frontend JS/TS (React, Vue, Next, Nuxt…) | SDK | Gerencia sessão, UTMs, batching e retry automaticamente. |
| HTML estático / sem build tool | SDK via CDN | Mesmo benefício, carregado via <script>. |
| Webhook de pagamento (Stripe, Hotmart…) | REST direto | Processado no servidor; chave API não fica exposta no browser. |
| Backend Python, PHP, Ruby, Go… | REST direto | O SDK é apenas JS/TS; use fetch/axios/http nativa. |
| Automações (n8n, Zapier, scripts) | REST direto | Chamadas pontuais sem necessidade de gerenciamento de estado. |
SDK e REST se complementam
O SDK chama os endpoints sessions, events e leads no frontend. Para vender o premium, redirecione ao Checkout Leadble ou use POST /payments via webhook de gateway externo no backend.
SDK @leadble/capture
v0.4.1 O SDK é a forma mais simples e segura de integrar. Cuida automaticamente de: visitorId, sessionId, captura de UTMs, batching de eventos, retry e validação client-side.
Instalar via npm
npm install @leadble/capture@0.4.1Configurar
import { Leadble, resolveResultPageBaseUrl, buildResultUrl } from '@leadble/capture'
const lb = new Leadble({
apiKey: 'lb_sk_...', // Configurações → Chaves de API
diagnosticKey: 'dlg_...', // public_key do diagnóstico publicado
debug: globalThis._importMeta_.env.DEV // loga payloads no console
})| Opção | Tipo | Descrição |
|---|---|---|
| apiKey | string | Chave de API do workspace (lb_sk_...) |
| diagnosticKey | string | public_key do diagnóstico publicado (dlg_...) |
| debug | boolean? | Loga payloads e erros no console (padrão: false) |
| baseUrl | string? | Base URL da API (padrão: https://app.leadble.com.br) |
| batchDebounceMs | number? | Debounce do batch de eventos em ms (padrão: 2000) |
Início rápido
Exemplo completo do fluxo: da visualização da página ao pagamento do produto premium.
// 1. Chamar na montagem da página (antes de qualquer interação)
await lb.init()
// ↑ captura UTMs, cria visitorId, cria/retoma sessão automaticamente
// 2. Rastrear eventos — batched com debounce de 2s
lb.track('page_view')
lb.track('cta_click')
lb.track('diagnostic_start')
lb.track('step_view', { step: 1 })
lb.track('step_complete', { step: 1 })
lb.track('question_answer', {
questionId: 'receita_anual',
type: 'select',
label: 'Qual é a receita anual da empresa?',
value: '1_5m',
valueLabel: 'Entre R$ 1M e R$ 5M'
})
// 3. Submeter lead ao final do formulário
const { leadId, publicKey, sessionId } = await lb.completeLead({
contact: {
name: 'Maria Silva',
email: 'maria@empresa.com',
phone: '+5511999999999',
company: 'Acme Ltda' // opcional
},
answers: [
{
questionId: 'receita_anual',
type: 'select',
label: 'Qual é a receita anual da empresa?',
value: '1_5m',
valueLabel: 'Entre R$ 1M e R$ 5M'
},
{
questionId: 'papel',
type: 'single_choice',
label: 'Qual é o seu papel?',
value: 'ceo_founder',
valueLabel: 'CEO / Fundador'
}
],
score: 78,
resultSummary: [
{ key: 'faixa', label: 'Faixa de maturidade', value: 'growth', valueLabel: 'Crescimento' }
],
resultPageUrl: resolveResultPageBaseUrl()
})
const resultLink = buildResultUrl(resolveResultPageBaseUrl(), publicKey, { sessionId })
lb.track('result_view')
// 4. Se o lead comprar, registrar o pagamento
await lb.recordPayment({
amountCents: 9700, // R$ 97,00
provider: 'stripe',
paymentMethod: 'credit_card',
status: 'paid',
externalId: 'pi_3Abc123' // ID no gateway
})
lb.track('payment_success')Usando com Vue 3
// Composable para Vue 3
import { Leadble } from '@leadble/capture'
export function useLeadbleCapture() {
const lb = new Leadble({
apiKey: globalThis._importMeta_.env.VITE_LEADBLE_API_KEY,
diagnosticKey: globalThis._importMeta_.env.VITE_LEADBLE_DIAGNOSTIC_KEY,
debug: globalThis._importMeta_.env.DEV
})
onMounted(() => lb.init())
return { lb }
}API do SDK
Métodos disponíveis na instância Leadble.
lb.init()→ Promise<{ sessionId, isNew }>Captura UTMs, cria/retoma sessão. Rotaciona automaticamente se o cache local for paid/abandoned. Chame na montagem da página.
lb.restart()→ Promise<{ sessionId, isNew }>Nova tentativa pagável (botão Recomeçar). Limpa o sessionId em cache, mantém o visitorId e cria outra sessão.
lb.track(eventType, payload?)→ voidEnfileira um evento. Enviado em batch com debounce de 2s e flush no beforeunload.
lb.flush()→ Promise<void>Força o envio imediato de todos os eventos enfileirados.
lb.completeLead(input)→ Promise<{ leadId, publicKey, sessionId }>Valida contato + respostas, faz flush dos eventos e submete o lead. Aceita resultPageUrl, interests e whatsappOptIn.
resolveResultPageBaseUrl(origin?, path?)→ stringMonta a URL base da página de resultado (padrão /diagnostico/resultado). Use no resultPageUrl do completeLead.
buildResultUrl(baseUrl, publicKey, opts?)→ stringMonta link permanente com ?lead=ld_... e opcionalmente session e coupon.
lb.recordPayment(input)→ Promise<{ paymentId }>Registra um pagamento. Valida provider e método antes de enviar.
lb.uploadPremiumReport(file, opts?)→ Promise<{ reportId, emailSent, alreadyExisted }>Envia o PDF premium após pagamento confirmado. Leadble armazena e envia e-mail com link de download ao lead.
Recomeçar / retentativa
Um lead (pessoa) pode refazer o diagnóstico várias vezes. Cada tentativa é uma sessão distinta e o pagamento libera apenas aquela sessão — não o lead para sempre.
| Conceito | Regra |
|---|---|
| Lead | Pessoa (dedup por e-mail + diagnóstico). publicKey estável. |
| Sessão | Uma conclusão pagável. Cada “Recomeçar” gera um novo sessionId. |
| Pagamento | Vinculado à sessão. Checkout com sessão já paga responde alreadyPaid / 409. |
Checklist na isca (botão Recomeçar)
await lb.restart()antes de limpar o estado do quiz (mantém o mesmo visitor).- Refazer perguntas e chamar
completeLead()de novo na nova sessão. - Abrir o checkout apenas com o novo
sessionId. - Confirmar unlock com
GET /sessions/{sessionId}da sessão atual.
async function onRestart() {
const { sessionId } = await lb.restart()
// limpar estado local do quiz / resultado
resetQuizUi()
// NÃO reutilizar o sessionId antigo no checkout
return sessionId
}Evitando leads duplicados
O servidor Leadble aplica duas camadas de deduplicação automática. Ainda assim, a melhor proteção começa no cliente — evitar disparar completeLead() mais de uma vez.
Proteção server-side (automática)
Deduplicação por sessionId
Se a sessão já possui um lead, a API retorna o lead existente com alreadyExisted: true sem criar um novo registro. Retries e double-submits com o mesmo sessionId são seguros.
Deduplicação por email + diagnosticId
Se um lead com o mesmo e-mail já existe para o mesmo diagnóstico, a API retorna o mesmo lead (publicKey estável) e vincula a sessão atual como nova tentativa — inclusive após um pagamento anterior. O pagamento continua liberando só a sessão paga, não o lead inteiro.
Boas práticas no cliente
Desabilite o botão após o primeiro clique
Adicione um estado de loading no botão de submit. Desabilitá-lo visualmente evita cliques duplos acidentais.
Use uma flag ref antes de chamar completeLead()
Uma flag isSubmitting garante que a função não seja chamada duas vezes mesmo que o botão ainda apareça habilitado por um frame.
Não chame completeLead() em efeitos ou watchers
Evite chamar em onMounted, useEffect, watch ou qualquer gancho que possa rodar duas vezes. React StrictMode e Vue watchers imediatos são causas comuns de duplo envio.
Vue 3
// Vue 3 — padrão correto para evitar duplo envio
const isSubmitting = ref(false)
async function handleSubmit() {
if (isSubmitting.value) return // guard: ignora cliques simultâneos
isSubmitting.value = true
try {
const { leadId } = await lb.completeLead({
contact: { name: form.name, email: form.email, phone: form.phone },
answers: form.answers,
score: calculatedScore
})
router.push(`/resultado?leadId=${leadId}`)
} catch (err) {
console.error(err)
} finally {
isSubmitting.value = false
}
}React
// React — padrão correto para evitar duplo envio
const [isSubmitting, setIsSubmitting] = useState(false)
async function handleSubmit() {
if (isSubmitting) return // guard: ignora cliques simultâneos
setIsSubmitting(true)
try {
const { leadId } = await lb.completeLead({
contact: { name, email, phone },
answers,
score: calculatedScore
})
router.push(`/resultado?leadId=${leadId}`)
} catch (err) {
console.error(err)
} finally {
setIsSubmitting(false)
}
}Defesa em profundidade
A deduplicação server-side é a rede de segurança; a flag isSubmitting no cliente é a primeira linha de defesa. Use ambas: o servidor garante consistência mesmo quando o cliente falha, e o cliente evita requisições desnecessárias.
Vender o relatório premium na isca
Depois que a isca captura o lead com a Capture API, você pode oferecer o relatório completo via Checkout Leadble — página de pagamento white-label (Pix e cartão), hospedada em checkout.app.leadble.com.br.
- 1A isca captura sessão + lead (
init→completeLead) - 2Visitante clica em “Desbloquear relatório” → redireciona para o checkout
- 3Paga no checkout (Pix ou cartão) na identidade visual do cliente
- 4Retorna para a isca com
?session=...&paid=1 - 5A isca confirma via
GET /sessions/:ide libera o premium
| Valor | Descrição | Exemplo |
|---|---|---|
| CHECKOUT_URL | URL do checkout Leadble | https://checkout.app.leadble.com.br |
| publicKey | Chave pública do diagnóstico (public_key) | dlg_ab12cd34... |
| sessionId | ID da sessão do visitante (SDK ou POST /sessions) | uuid |
| return | URL de retorno após pagamento (query param) | https://seusite.com/diagnostico |
Checkout Leadble vs gateway externo
Com o Checkout Leadble, você não precisa chamar POST /payments — o pagamento é registrado automaticamente. Use POST /payments apenas se integrar um gateway próprio (Stripe, Hotmart, Mercado Pago…) via webhook no seu backend.
Configurar o produto premium
Antes de redirecionar visitantes, o dono do diagnóstico precisa ativar a venda no painel Leadble:
- Ativar venda do relatório premium
- Definir preço (ex.: R$ 47,90)
- Nome e descrição do produto (exibidos no checkout)
A identidade visual (cor primária e logo) vem do diagnóstico — o checkout já aparece na marca do cliente, sem configuração extra.
completeLead()antes de enviar o visitante ao checkout, para o pagamento ficar vinculado ao lead. Redirecionar para o checkout
No botão “Desbloquear relatório completo”, monte a URL do checkout com session e return:
const CHECKOUT_URL = 'https://checkout.app.leadble.com.br'
const publicKey = 'dlg_...' // public_key do diagnóstico publicado
// sessionId vem do lb.init() ou POST /sessions
const returnUrl = window.location.href.split('?')[0]
lb.track('checkout_view')
const url = new URL(`${CHECKOUT_URL}/c/${publicKey}`)
url.searchParams.set('session', sessionId)
url.searchParams.set('return', returnUrl)
window.location.href = url.toString() URL final: https://checkout.app.leadble.com.br/c/{publicKey}?session={sessionId}&return={returnUrl}
Regras do parâmetro return
- • Isca hospedada na Leadble (
app.leadble.com.br/d/{slug}) — permitido automaticamente - • Isca no site do cliente — o domínio precisa ser o website cadastrado do workspace
- • Outros domínios são ignorados (proteção contra open redirect)
Fluxo completo com SDK
import { Leadble } from '@leadble/capture'
const lb = new Leadble({
apiKey: globalThis._importMeta_.env.VITE_LEADBLE_API_KEY,
diagnosticKey: globalThis._importMeta_.env.VITE_LEADBLE_DIAGNOSTIC_KEY
})
const { sessionId } = await lb.init()
// ... fluxo do diagnóstico ...
await lb.completeLead({ contact: { name, email, phone }, answers })
lb.track('result_view')
// Oferta premium
lb.track('checkout_view')
function irParaCheckout() {
const url = new URL('https://checkout.app.leadble.com.br/c/dlg_...')
url.searchParams.set('session', sessionId)
url.searchParams.set('return', window.location.href.split('?')[0])
window.location.href = url.toString()
}Verificar pagamento
Após o pagamento, o checkout redireciona de volta para o seu return com query params:
{returnUrl}?session={sessionId}&paid=1?paid=1: esse parâmetro é uma dica de UX e pode ser forjado. Sempre confirme no servidor via . async function isSessionPaid(sessionId: string): Promise<boolean> {
const res = await fetch(
`https://app.leadble.com.br/api/v1/capture/sessions/${sessionId}`,
{
headers: {
Authorization: 'Bearer lb_sk_...',
'X-Diagnostic-Key': 'dlg_...'
}
}
)
if (!res.ok) return false
const data = await res.json()
return data.session?.status === 'paid'
}
// Ao carregar a isca (ex.: após retorno do checkout)
const params = new URLSearchParams(location.search)
const sid = params.get('session')
if (sid && await isSessionPaid(sid)) {
revelarRelatorioPremium()
}API key no browser
A checagem usa os mesmos headers da Capture API. Se preferir não expor a lb_sk_ no frontend, implemente um proxy server-side (BFF) que repassa a consulta e devolve apenas { paid: boolean }.
Liberar o conteúdo premium
Com session.status === 'paid' confirmado, a isca libera o relatório completo — página premium, PDF, link de download, seção extra, etc. A entrega do conteúdo é responsabilidade da isca; a Leadble registra o pagamento e o estado da sessão.
Eventos recomendados para métricas no painel:
lb.track('checkout_view') lb.track('checkout_start') lb.track('payment_success') O Pix é assíncrono: o checkout aguarda a confirmação antes de redirecionar. Se quiser feedback em tempo real sem sair da isca, faça polling leve em GET /sessions/:id a cada 3–5 segundos.
Enviar relatório premium para a Leadble
Depois de confirmar o pagamento e gerar o PDF na isca, envie o arquivo para a Leadble. O backend armazena o PDF e envia um e-mail ao lead com link seguro de download. O visitante continua podendo baixar na isca normalmente.
Requisitos: pagamento confirmado (session.status === 'paid'), PDF válido, máximo 25 MB. Use leadPublicKey, sessionId ou leadId para identificar o lead.
// Após confirmar pagamento e gerar o PDF na isca
const session = await lb.getSession(sessionId)
if (session.status !== 'paid') return
const pdfBlob = await generateFullReportPdf(leadData)
const { reportId, emailSent } = await lb.uploadPremiumReport(pdfBlob, {
leadPublicKey: publicKey,
filename: 'relatorio-completo.pdf'
})
// Leadble armazena o PDF e envia e-mail com link de download ao leadCheckout Leadble
Não precisa chamar POST /payments — o checkout já registra o pagamento. Basta verificar GET /sessions/:id, gerar o PDF e chamar uploadPremiumReport.
Endpoints de Captura
Base URL: https://app.leadble.com.br/api/v1/capture
Todos os endpoints requerem os headers de autenticação e aceitam/retornam JSON. Use o SDK para frontends; REST para back-ends, automações ou outras linguagens.
/api/v1/interests Retorna o catálogo completo de interesses ativos, agrupados por categoria. Cada item indica se o diagnóstico oferece serviço naquela área (offered). Use esse endpoint antes de exibir o formulário de contato para listar todas as opções ao lead — inclusive áreas que o cliente não atende. Os slugs selecionados pelo lead são enviados no campo interests do POST /leads.
offered: true significa que o interesse foi selecionado ao criar o diagnóstico (o cliente presta serviço naquela área). Leads podem selecionar qualquer interesse do catálogo, mesmo com offered: false. Request
GET /api/v1/interests
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...Response 200
{
"groups": [
{
"slug": "marketing",
"label": "Marketing",
"interests": [
{ "slug": "marketing_digital", "label": "Marketing Digital", "offered": true },
{ "slug": "seo", "label": "SEO", "offered": false }
]
},
{
"slug": "vendas",
"label": "Vendas",
"interests": [
{ "slug": "vendas", "label": "Vendas", "offered": true }
]
}
]
}| Campo da resposta | Tipo | Descrição |
|---|---|---|
| groups | array | Grupos de interesses do catálogo. |
| groups[].slug | string | Identificador do grupo (ex: marketing). |
| groups[].label | string | Nome legível do grupo (ex: Marketing). |
| groups[].interests | array | Interesses ativos do grupo. |
| groups[].interests[].slug | string | Identificador único do interesse (ex: marketing_digital). |
| groups[].interests[].label | string | Nome legível do interesse (ex: Marketing Digital). |
| groups[].interests[].offered | boolean | true se o diagnóstico oferece serviço nesta área; false caso contrário. |
/sessions Cria ou retoma uma sessão para um visitante. Uma sessão é uma tentativa/conclusão pagável. Se o visitante já tem uma sessão ativa (não paga e não abandonada), ela é retomada. Após pagamento, o próximo POST /sessions (ou lb.restart()) cria uma nova sessão.
Request
POST /api/v1/capture/sessions
Content-Type: application/json
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
{
"visitorId": "550e8400-e29b-41d4-a716-446655440000",
"attribution": {
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "valuation-jun-2026",
"referrer": "https://google.com",
"landing_page": "https://valuation.acme.com/?utm_source=google",
"metadata": {
"gclid": "EAIaIQobChMI..."
}
},
"context": {
"locale": "pt-BR",
"pageUrl": "https://valuation.acme.com/",
"screenWidth": 1440,
"screenHeight": 900
}
}Response 200
{
"session": {
"sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"visitorId": "550e8400-e29b-41d4-a716-446655440000",
"diagnosticId": "3f7a8c12-b456-4789-a012-345678901234",
"status": "viewed",
"isNew": true,
"createdAt": "2026-06-22T17:00:00.000Z"
}
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| visitorId | uuid | Sim | UUID v4 gerado no cliente. Persistir em localStorage para ligar sessões do mesmo visitante. |
| attribution | object? | Não | UTMs, referrer e landing page capturados da URL atual. |
| attribution.utm_source | string? | Não | Fonte do tráfego (google, facebook, email…) |
| attribution.utm_medium | string? | Não | Meio (cpc, organic, newsletter…) |
| attribution.utm_campaign | string? | Não | Nome da campanha |
| attribution.referrer | string? | Não | URL de origem (document.referrer) |
| attribution.landing_page | string? | Não | URL completa com query string |
| attribution.metadata | object? | Não | Outros parâmetros (gclid, fbclid…) |
| context | object? | Não | Contexto do navegador (userAgent, locale, pageUrl, screen) |
/events Registra um ou mais eventos de funil para uma sessão. Os eventos são append-only — nunca atualizados. Aceita batch de até 50 eventos por chamada. Idempotente via eventId único. Para saber onde o visitante abandonou o fluxo, siga o padrão em .
Request
POST /api/v1/capture/events
Content-Type: application/json
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
{
"sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"events": [
{
"eventType": "cta_click",
"eventId": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890"
},
{
"eventType": "step_complete",
"payload": { "step": 1 }
},
{
"eventType": "question_answer",
"payload": {
"questionId": "receita_anual",
"type": "select",
"label": "Qual é a receita anual da empresa?",
"value": "1_5m",
"valueLabel": "Entre R$ 1M e R$ 5M"
}
}
]
}Response 200
{
"accepted": 3,
"sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}Tipos de evento e payloads
| page_view | — | Página do diagnóstico carregou |
| cta_click | — | Visitante clicou em Começar / CTA |
| diagnostic_start | — | Primeiro step do formulário foi exibido |
| step_view | { step: number|string } | Visitante visualizou uma etapa |
| step_complete | { step: number|string } | Visitante completou uma etapa |
| question_answer | { questionId, type, value, label?, valueLabel?, valueLabels? } | Visitante respondeu uma pergunta |
| contact_submit | — | Formulário de contato foi preenchido |
| result_view | — | Resultado gratuito foi exibido |
| checkout_view | — | Oferta paga foi exibida |
| checkout_start | — | Processo de pagamento foi iniciado |
| payment_success | — | Pagamento foi confirmado |
| payment_failed | — | Pagamento falhou |
/leads Salva o lead ao final do formulário. Chame após o usuário preencher os dados de contato e antes de exibir o resultado. Idempotente — re-submissões retornam o lead existente com alreadyExisted: true.
Request
POST /api/v1/capture/leads
Content-Type: application/json
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
{
"sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"contact": {
"name": "Maria Silva",
"email": "maria@empresa.com",
"phone": "+5511999999999",
"company": "Acme Ltda",
"city": "São Paulo",
"state": "SP",
"country": "BR"
},
"customFields": {
"cargo": "Diretor comercial"
},
"answers": [
{
"questionId": "receita_anual",
"type": "select",
"label": "Qual é a receita anual da empresa?",
"value": "1_5m",
"valueLabel": "Entre R$ 1M e R$ 5M"
},
{
"questionId": "papel",
"type": "single_choice",
"label": "Qual é o seu papel?",
"value": "ceo_founder",
"valueLabel": "CEO / Fundador"
},
{
"questionId": "areas_interesse",
"type": "multi_select",
"label": "Áreas de interesse",
"value": ["marketing_digital", "vendas"],
"valueLabels": ["Marketing Digital", "Vendas"]
}
],
"score": 78,
"resultSummary": [
{ "key": "faixa", "label": "Faixa de maturidade", "value": "growth", "valueLabel": "Crescimento" },
{ "key": "valuation", "label": "Valuation estimado", "value": 2500000 }
],
"interests": ["marketing_digital", "vendas"]
}Response 200
{
"lead": {
"id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"publicKey": "ld_a1b2c3d4e5f6789012345678",
"workspaceId": "...",
"diagnosticId": "...",
"sessionId": "...",
"name": "Maria Silva",
"email": "maria@empresa.com",
"phone": "+5511999999999",
"company": "Acme Ltda",
"city": "São Paulo",
"state": "SP",
"country": "BR",
"document": null,
"customFields": { "cargo": "Diretor comercial" },
"answers": [
{
"questionId": "receita_anual",
"type": "select",
"label": "Qual é a receita anual da empresa?",
"value": "1_5m",
"valueLabel": "Entre R$ 1M e R$ 5M"
}
],
"score": 78,
"status": "new",
"attribution": { "utm_source": "google", ... },
"resultSummary": [
{ "key": "faixa", "label": "Faixa de maturidade", "value": "growth", "valueLabel": "Crescimento" }
],
"interests": ["marketing_digital", "vendas"],
"createdAt": "2026-06-22T17:02:00.000Z",
"updatedAt": "2026-06-22T17:02:00.000Z"
},
"alreadyExisted": false
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| sessionId | uuid | Sim | ID da sessão criada em POST /sessions. |
| contact | object | Sim | Dados de contato do lead. Ver seção Campos de contato. |
| answers | Answer[] | Sim* | Perguntas e respostas do diagnóstico. Obrigatório quando o formulário tem quiz. Ver Respostas (answers). |
| customFields | object? | Não | Somente campos extras de contato (lead_fields). Não use para perguntas do quiz. |
| score | number? | Não | Pontuação calculada pelo diagnóstico (0–100). |
| resultSummary | object | array? | Não | Snapshot do resultado. Prefira array com label por item. |
| resultPageUrl | string (url)? | Não | URL base da página de resultado (sem lead/session). Usada pelo funil de e-mail; fallback /diagnostico/resultado. |
| interests | string[]? | Não | Slugs dos interesses selecionados pelo lead. Obtidos via GET /api/v1/interests. |
/leads/:publicKey Recupera um lead pelo identificador público permanente (ld_...) retornado em POST /leads. Use no BFF da isca para renderizar resultado em outro dispositivo via ?lead=ld_xxx.
GET /api/v1/capture/leads/ld_a1b2c3d4e5f6789012345678
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
// Response 200
{
"lead": {
"publicKey": "ld_a1b2c3d4e5f6789012345678",
"answers": [...],
"resultSummary": [...],
"score": 78,
"hasPaidPayment": false
}
}/sessions/:sessionId Snapshot da sessão: status, furthestStep, atribuição e leadPublicKey se o lead já foi capturado. Use para confirmar pagamento após retorno do — verifique session.status === 'paid'.
Request
GET /api/v1/capture/sessions/7c9e6679-7425-40de-944b-e07fc1f90ae7
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...Response 200 (sessão paga)
{
"session": {
"sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"diagnosticId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"visitorId": "550e8400-e29b-41d4-a716-446655440000",
"status": "paid",
"furthestStep": "resultado",
"leadPublicKey": "ld_a1b2c3d4e5f6789012345678",
"paidAt": "2026-06-22T17:05:00.000Z",
"startedAt": "2026-06-22T16:58:00.000Z",
"completedAt": "2026-06-22T17:00:00.000Z",
"createdAt": "2026-06-22T16:58:00.000Z",
"updatedAt": "2026-06-22T17:05:00.000Z"
}
}| Campo | Descrição |
|---|---|
| sessionId | UUID da sessão. |
| status | viewed | started | in_progress | completed | checkout | paid | abandoned. Use paid para liberar premium. |
| paidAt | ISO 8601 — preenchido quando status = paid. |
| leadPublicKey | ld_... se o lead foi capturado; null caso contrário. |
| furthestStep | Última etapa atingida no funil (slug ou número). |
/payments Registra um pagamento do produto premium. Pode ser chamado com sessionId, leadId ou leadPublicKey. Idempotente por externalId + provider.
POST /payments apenas para gateways externos (Stripe, Hotmart…). Ver seção . Request
POST /api/v1/capture/payments
Content-Type: application/json
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
{
"sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"amountCents": 9700,
"currency": "BRL",
"provider": "stripe",
"paymentMethod": "credit_card",
"status": "paid",
"externalId": "pi_3PqRs4TuVwXy5Z",
"metadata": {
"coupon": "LAUNCH20",
"plan": "valuation-pro"
}
}Response 200
{
"payment": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"leadId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"amountCents": 9700,
"currency": "BRL",
"provider": "stripe",
"paymentMethod": "credit_card",
"status": "paid",
"externalId": "pi_3PqRs4TuVwXy5Z",
"paidAt": "2026-06-22T17:05:00.000Z",
"createdAt": "2026-06-22T17:05:00.000Z"
}
}/reportsEnvia o PDF do relatório premium após pagamento confirmado. Body multipart/form-data (não JSON). Armazena o arquivo, envia e-mail com link de download ao lead na 1ª vez. Idempotente por lead — re-upload substitui o PDF existente.
paid. Request
POST /api/v1/capture/reports
Content-Type: multipart/form-data
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
Form fields:
file (required) PDF, máx. 25 MB
leadPublicKey (optional) ld_... — ou use sessionId / leadId
sessionId (optional) uuid da sessão
leadId (optional) uuid do lead
filename (optional) default relatorio.pdf
resendEmail (optional) "true" para reenviar e-mailResponse 200
{
"report": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"leadId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"filename": "relatorio-completo.pdf",
"mimeType": "application/pdf",
"sizeBytes": 245760,
"uploadedAt": "2026-06-27T12:00:00.000Z",
"emailSent": true,
"emailSentAt": null
},
"alreadyExisted": false
}/reports/generate Alternativa ao : em vez de subir um PDF pronto, você envia HTML cru ou uma URL em JSON. A Leadble gera o PDF, armazena no bucket privado, envia o e-mail com link ao lead (1ª vez) e retorna o downloadUrl público. Idempotente por lead — re-geração substitui o PDF existente.
Campo mode
"html" — source é o HTML completo do relatório (máx. 2 MB). "url" — source é uma URL http(s) que a Leadble renderiza.
paid. 502 Erro ao gerar o PDF — falha no serviço de geração (HTML inválido, URL inacessível, timeout).
Request — mode html
POST /api/v1/capture/reports/generate
Content-Type: application/json
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
{
"mode": "html",
"source": "<html><body style=\"font-family: sans-serif; padding: 40px\"><h1>Relatório</h1><p>Gerado via API.</p></body></html>",
"leadPublicKey": "ld_...",
"filename": "relatorio-completo.pdf",
"resendEmail": false,
"options": {
"format": "A4",
"printBackground": true,
"margin": { "top": "1cm", "right": "1cm", "bottom": "1cm", "left": "1cm" }
}
}Request — mode url
POST /api/v1/capture/reports/generate
Content-Type: application/json
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
{
"mode": "url",
"source": "https://minha-isca.com/relatorio/ld_abc123",
"leadPublicKey": "ld_...",
"filename": "relatorio-completo.pdf",
"options": {
"format": "A4",
"landscape": false,
"waitUntil": "networkidle0"
}
}Response 200
{
"report": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"leadId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"filename": "relatorio-completo.pdf",
"mimeType": "application/pdf",
"sizeBytes": 245760,
"uploadedAt": "2026-06-27T12:00:00.000Z",
"emailSent": true,
"emailSentAt": null
},
"downloadUrl": "https://leadble.app/api/r/9f2c...e1",
"alreadyExisted": false
}Campos de contato
O objeto contact é normalizado automaticamente antes de persistir (email em lowercase, telefone em E.164, documento só dígitos, etc.).
| Campo | Obrigatório | Validação / Formato |
|---|---|---|
| name | Sim | Mín. 1 char, máx. 200. Whitespace extra é removido. |
| Sim | Formato RFC. Normalizado para lowercase. | |
| phone | Sim | Aceita qualquer formato (11999..., +5511..., (11) 9...). Normalizado para dígitos + prefixo +. |
| document | Não | CPF ou CNPJ em qualquer formato. Normalizado para apenas dígitos. |
| company | Não | Máx. 200 chars. |
| birthDate | Não | ISO 8601: YYYY-MM-DD |
| site | Não | URL completa. Normalizado para https://. |
| Não | Handle com ou sem @. Normalizado para sem @, lowercase. | |
| city | Não | Máx. 100 chars. |
| state | Não | Máx. 50 chars. Ex: SP, RJ |
| country | Não | ISO 3166-1 alpha-2. Ex: BR. Normalizado para uppercase. |
Perguntas do diagnóstico → answers
Não envie perguntas/respostas do quiz em customFields. Use o array answers com label (pergunta) e valueLabel (texto da opção quando value for slug). Ver seção .
Campos extras de contato (customFields)
Apenas campos adicionais do formulário de contato configurados em lead_fields do diagnóstico (ex.: cargo, faturamento declarado no contato). Não substitui answers.
Respostas (answers)
Padrão para enviar perguntas e respostas do diagnóstico no POST /leads. O painel exibe label como pergunta e valueLabel (ou value) como resposta.
| Campo | Obrigatório | Descrição |
|---|---|---|
| questionId | Sim | Slug estável da pergunta (ex: receita_anual). Padrão: a-z, 0-9 e _. Mesmo ID em todas as submissões. |
| type | Sim | text | number | select | multi_select | single_choice | boolean | scale | date |
| label | Sim | Texto da pergunta exibido no painel (PT-BR). |
| value | Sim | Valor canônico: slug da opção, número, texto, boolean ou array de slugs (multi_select). |
| valueLabel | Não | Texto legível da resposta quando value é slug (select, single_choice, boolean). O painel prioriza este campo. |
| valueLabels | Não | Textos legíveis para cada item de value em multi_select (mesma ordem e tamanho). |
Exemplo completo
{
"answers": [
{
"questionId": "receita_anual",
"type": "select",
"label": "Qual é a receita anual da empresa?",
"value": "1_5m",
"valueLabel": "Entre R$ 1M e R$ 5M"
},
{
"questionId": "papel",
"type": "single_choice",
"label": "Qual é o seu papel?",
"value": "ceo_founder",
"valueLabel": "CEO / Fundador"
},
{
"questionId": "areas_interesse",
"type": "multi_select",
"label": "Áreas de interesse",
"value": ["marketing_digital", "vendas"],
"valueLabels": ["Marketing Digital", "Vendas"]
},
{
"questionId": "receita_ltm",
"type": "number",
"label": "Receita LTM (R$)",
"value": 1200000
}
]
}Regra prática
- Pergunta → sempre em
label - Resposta com slug →
value= slug,valueLabel= texto exibido - Resposta direta (texto, número) → só
value;valueLabelé opcional - resultSummary → prefira array
[{ key, label, value, valueLabel? }]em vez de objeto com chaves técnicas - resultPageUrl → envie a URL base no
completeLead(useresolveResultPageBaseUrl()); o funil de e-mail injetalead,session, cupom e UTMs - buildResultUrl → monte o link permanente após o
completeLeadcompublicKeyesessionId
Funil e abandono
Para saber em qual etapa ou pergunta o visitante saiu, envie eventos em tempo real via POST /events. Isso é separado do array answers do POST /leads (que só grava no momento da captura do contato).
| Canal | Quando | Persiste em |
|---|---|---|
| POST /events | A cada tela e resposta | diagnostic_events + furthest_step |
| POST /leads | Ao capturar contato | leads.answers (CRM) |
Padrão por pergunta
Use o mesmo slug em step e questionId quando cada tela = uma pergunta.
SDK
// Padrão por tela de pergunta — use o MESMO slug em step e questionId
const STEP = 'receita_anual'
// 1. Ao montar a tela
lb.track('step_view', { step: STEP })
// 2. Ao confirmar resposta (Próximo)
lb.track('question_answer', {
questionId: STEP,
type: 'select',
label: 'Qual é a receita anual da empresa?',
value: '1_5m',
valueLabel: 'Entre R$ 1M e R$ 5M'
})
lb.track('step_complete', { step: STEP })REST
POST /api/v1/capture/events
Content-Type: application/json
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...
{
"sessionId": "uuid-da-sessao",
"events": [
{
"eventType": "step_view",
"eventId": "uuid-1",
"payload": { "step": "receita_anual" }
},
{
"eventType": "question_answer",
"eventId": "uuid-2",
"payload": {
"questionId": "receita_anual",
"type": "select",
"label": "Qual é a receita anual da empresa?",
"value": "1_5m",
"valueLabel": "Entre R$ 1M e R$ 5M"
}
},
{
"eventType": "step_complete",
"eventId": "uuid-3",
"payload": { "step": "receita_anual" }
}
]
}Fluxo completo
await lb.init()
lb.track('page_view')
lb.track('cta_click')
lb.track('diagnostic_start')
// Pergunta 1
lb.track('step_view', { step: 'receita_anual' })
lb.track('question_answer', {
questionId: 'receita_anual',
type: 'select',
label: 'Qual é a receita anual da empresa?',
value: '1_5m',
valueLabel: 'Entre R$ 1M e R$ 5M'
})
lb.track('step_complete', { step: 'receita_anual' })
// Pergunta 2
lb.track('step_view', { step: 'papel' })
lb.track('question_answer', {
questionId: 'papel',
type: 'single_choice',
label: 'Qual é o seu papel?',
value: 'ceo_founder',
valueLabel: 'CEO / Fundador'
})
lb.track('step_complete', { step: 'papel' })
// Contato + lead (answers[] = snapshot final para o CRM)
lb.track('step_view', { step: 'contato' })
lb.track('contact_submit')
await lb.completeLead({
contact: { name, email, phone },
answers: [ /* todas as respostas */ ]
})
lb.track('result_view')
// furthest_step na sessão = último step enviado
// última pergunta respondida = último question_answer nos eventosComo saber onde parou
Última etapa atingida
Campo diagnostic_sessions.furthest_step — atualizado automaticamente pelo último step_view ou step_complete do batch. Use slug (receita_anual) ou número (3), mas seja consistente.
Última pergunta respondida
Último evento question_answer da sessão em diagnostic_events (ordenar por occurred_at). O payload traz questionId e label.
Sinais de abandono
step_viewsemstep_completena mesma etapa → viu mas não avançoustep_viewsemquestion_answer→ saiu antes de responderdiagnostic_startsemresult_viewe sem lead → não concluiu
Múltiplas perguntas na mesma tela
Um step_view / step_complete para a tela (ex: dados_financeiros) e um question_answer por pergunta (receita_ltm, ebitda_margin).
Tipos de evento
Os eventos mapeiam para a progressão do funil visível no painel. O campo furthest_step avança mas nunca regride. A métrica views é incrementada na criação da sessão — o evento page_view é registrado mas não altera nenhum contador.
| Tipo | Avança sessão para | Incrementa métrica |
|---|---|---|
| page_view | — | — |
| diagnostic_start | started | starts |
| contact_submit | in_progress | — |
| result_view | completed | completions |
| checkout_view | checkout | — |
| checkout_start | checkout | — |
| payment_success | paid | paid |
| Outros eventos | — | — |
Tipos de pagamento
Providers
stripemercado_pagopagarmehotmartkiwifyeduzzmonetizzemanualotherMétodos
pixcredit_carddebit_cardboletobank_transferotherStatus
pendingpaidfailedrefundedTratamento de erros
Todos os erros seguem o mesmo formato estruturado. O campo data.errors lista todos os problemas de validação, não só o primeiro.
{
"statusCode": 422,
"message": "Dados inválidos.",
"data": {
"errors": [
{
"path": "contact.email",
"code": "invalid_string",
"message": "E-mail inválido."
},
{
"path": "contact.phone",
"code": "too_small",
"message": "Telefone muito curto."
}
]
}
}| HTTP | Quando ocorre | O que fazer |
|---|---|---|
| 401 | API key inválida, revogada ou header Authorization ausente | Verificar a chave de API e o formato Bearer. |
| 401 | Header X-Diagnostic-Key ausente ou inválido | Passar o public_key ou UUID do diagnóstico. |
| 404 | Diagnóstico não encontrado ou não publicado | Verificar se o diagnóstico está com status Publicado. |
| 409 | Conflito de estado (sessão já paga, lead duplicado) | Verificar alreadyExisted na resposta — não é erro crítico. |
| 422 | Dados de entrada inválidos | Ler data.errors para cada campo com problema. |
| 500 | Erro interno da Leadble | Tentar novamente. O SDK faz retry automático com backoff. |
Exemplo completo sem SDK
Para back-ends, automações ou linguagens sem o SDK.
// Exemplo sem SDK — fetch direto
const BASE = 'https://app.leadble.com.br/api/v1/capture'
const HEADERS = {
'Content-Type': 'application/json',
'Authorization': 'Bearer lb_sk_...',
'X-Diagnostic-Key': 'dlg_...'
}
// 1. Criar sessão
const { session } = await fetch(`${BASE}/sessions`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ visitorId: crypto.randomUUID() })
}).then(r => r.json())
const sessionId = session.sessionId
// 2. Enviar eventos
await fetch(`${BASE}/events`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({
sessionId,
events: [{ eventType: 'cta_click' }]
})
})
// 3. Submeter lead
const { lead } = await fetch(`${BASE}/leads`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({
sessionId,
contact: { name, email, phone },
score: 78
})
}).then(r => r.json())