Integrando com IA? Copie o contexto técnico completo para usar com Claude, ChatGPT ou Gemini.

Integração

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. 1 Crie o diagnóstico no painel e publique-o para obter o public_key.
  2. 2 Gere uma Chave de API em Configurações → Chaves de API.
  3. 3 No frontend use o SDK; para vender o premium, redirecione ao Checkout Leadble ou registre pagamentos de gateway externo via REST.
  4. 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:

http
Authorization: Bearer lb_sk_a1b2c3d4e5f6...
X-Diagnostic-Key: dlg_k9xm2pqrabcd
Authorization — 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.

⚠️ Atenção: A chave de API (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.

Arquitetura típica

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)Redirecionarcheckout.app.leadble.com.br processa e registra; confirme com GET /sessions/:id.
Frontend JS/TS (React, Vue, Next, Nuxt…)SDKGerencia sessão, UTMs, batching e retry automaticamente.
HTML estático / sem build toolSDK via CDNMesmo benefício, carregado via <script>.
Webhook de pagamento (Stripe, Hotmart…)REST diretoProcessado no servidor; chave API não fica exposta no browser.
Backend Python, PHP, Ruby, Go…REST diretoO SDK é apenas JS/TS; use fetch/axios/http nativa.
Automações (n8n, Zapier, scripts)REST diretoChamadas 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

bash
npm install @leadble/capture@0.4.1

Configurar

typescript
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.

typescript
// 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

typescript
// 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?)→ void

Enfileira 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?)→ string

Monta a URL base da página de resultado (padrão /diagnostico/resultado). Use no resultPageUrl do completeLead.

buildResultUrl(baseUrl, publicKey, opts?)→ string

Monta 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)

  1. await lb.restart() antes de limpar o estado do quiz (mantém o mesmo visitor).
  2. Refazer perguntas e chamar completeLead() de novo na nova sessão.
  3. Abrir o checkout apenas com o novosessionId.
  4. Confirmar unlock com GET /sessions/{sessionId} da sessão atual.
typescript
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)

1

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.

2

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

typescript
// 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

typescript
// 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.

Checkout

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.

Fluxo resumido
  1. 1A isca captura sessão + lead (initcompleteLead)
  2. 2Visitante clica em “Desbloquear relatório” → redireciona para o checkout
  3. 3Paga no checkout (Pix ou cartão) na identidade visual do cliente
  4. 4Retorna para a isca com ?session=...&paid=1
  5. 5A isca confirma via GET /sessions/:id e libera o premium
Valor Descrição Exemplo
CHECKOUT_URLURL do checkout Leadblehttps://checkout.app.leadble.com.br
publicKeyChave pública do diagnóstico (public_key)dlg_ab12cd34...
sessionIdID da sessão do visitante (SDK ou POST /sessions)uuid
returnURL 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.

Pré-requisito na isca: capture o lead com 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:

javascript
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

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

text
{returnUrl}?session={sessionId}&paid=1
⚠️ Não confie apenas em ?paid=1: esse parâmetro é uma dica de UX e pode ser forjado. Sempre confirme no servidor via .
typescript
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.

typescript
// 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 lead

Checkout Leadble

Não precisa chamar POST /payments — o checkout já registra o pagamento. Basta verificar GET /sessions/:id, gerar o PDF e chamar uploadPremiumReport.

API Reference

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.

Endpoints disponíveis
GET/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.

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

http
GET /api/v1/interests
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...

Response 200

json
{
  "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
groupsarrayGrupos de interesses do catálogo.
groups[].slugstringIdentificador do grupo (ex: marketing).
groups[].labelstringNome legível do grupo (ex: Marketing).
groups[].interestsarrayInteresses ativos do grupo.
groups[].interests[].slugstringIdentificador único do interesse (ex: marketing_digital).
groups[].interests[].labelstringNome legível do interesse (ex: Marketing Digital).
groups[].interests[].offeredbooleantrue se o diagnóstico oferece serviço nesta área; false caso contrário.
POST/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.

Chame assim que a página carregar — antes de qualquer interação do usuário. Garante que UTMs, referrer e landing page sejam capturados corretamente.

Request

http
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

json
{
  "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
visitorIduuidSimUUID v4 gerado no cliente. Persistir em localStorage para ligar sessões do mesmo visitante.
attributionobject?NãoUTMs, referrer e landing page capturados da URL atual.
attribution.utm_sourcestring?NãoFonte do tráfego (google, facebook, email…)
attribution.utm_mediumstring?NãoMeio (cpc, organic, newsletter…)
attribution.utm_campaignstring?NãoNome da campanha
attribution.referrerstring?NãoURL de origem (document.referrer)
attribution.landing_pagestring?NãoURL completa com query string
attribution.metadataobject?NãoOutros parâmetros (gclid, fbclid…)
contextobject?NãoContexto do navegador (userAgent, locale, pageUrl, screen)
POST/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

http
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

json
{
  "accepted": 3,
  "sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}

Tipos de evento e payloads

page_viewPágina do diagnóstico carregou
cta_clickVisitante clicou em Começar / CTA
diagnostic_startPrimeiro 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_submitFormulário de contato foi preenchido
result_viewResultado gratuito foi exibido
checkout_viewOferta paga foi exibida
checkout_startProcesso de pagamento foi iniciado
payment_successPagamento foi confirmado
payment_failedPagamento falhou
POST/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

http
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

json
{
  "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
sessionIduuidSimID da sessão criada em POST /sessions.
contactobjectSimDados de contato do lead. Ver seção Campos de contato.
answersAnswer[]Sim*Perguntas e respostas do diagnóstico. Obrigatório quando o formulário tem quiz. Ver Respostas (answers).
customFieldsobject?NãoSomente campos extras de contato (lead_fields). Não use para perguntas do quiz.
scorenumber?NãoPontuação calculada pelo diagnóstico (0–100).
resultSummaryobject | array?NãoSnapshot do resultado. Prefira array com label por item.
resultPageUrlstring (url)?NãoURL base da página de resultado (sem lead/session). Usada pelo funil de e-mail; fallback /diagnostico/resultado.
interestsstring[]?NãoSlugs dos interesses selecionados pelo lead. Obtidos via GET /api/v1/interests.
GET/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.

http
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
  }
}
GET/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

http
GET /api/v1/capture/sessions/7c9e6679-7425-40de-944b-e07fc1f90ae7
Authorization: Bearer lb_sk_...
X-Diagnostic-Key: dlg_...

Response 200 (sessão paga)

json
{
  "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
sessionIdUUID da sessão.
statusviewed | started | in_progress | completed | checkout | paid | abandoned. Use paid para liberar premium.
paidAtISO 8601 — preenchido quando status = paid.
leadPublicKeyld_... se o lead foi capturado; null caso contrário.
furthestStepÚltima etapa atingida no funil (slug ou número).
POST/payments

Registra um pagamento do produto premium. Pode ser chamado com sessionId, leadId ou leadPublicKey. Idempotente por externalId + provider.

Usando Checkout Leadble? Não precisa chamar este endpoint — o checkout registra o pagamento automaticamente. Use POST /payments apenas para gateways externos (Stripe, Hotmart…). Ver seção .

Request

http
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

json
{
  "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"
  }
}
POST/reports

Envia 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.

409 Pagamento não confirmado — retornado se o lead/sessão ainda não tem pagamento paid.

Request

http
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-mail

Response 200

json
{
  "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
}
POST/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.

409 Pagamento não confirmado — retornado se o lead/sessão ainda não tem pagamento paid.
502 Erro ao gerar o PDF — falha no serviço de geração (HTML inválido, URL inacessível, timeout).

Request — mode html

http
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

http
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

json
{
  "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
nameSimMín. 1 char, máx. 200. Whitespace extra é removido.
emailSimFormato RFC. Normalizado para lowercase.
phoneSimAceita qualquer formato (11999..., +5511..., (11) 9...). Normalizado para dígitos + prefixo +.
documentNãoCPF ou CNPJ em qualquer formato. Normalizado para apenas dígitos.
companyNãoMáx. 200 chars.
birthDateNãoISO 8601: YYYY-MM-DD
siteNãoURL completa. Normalizado para https://.
instagramNãoHandle com ou sem @. Normalizado para sem @, lowercase.
cityNãoMáx. 100 chars.
stateNãoMáx. 50 chars. Ex: SP, RJ
countryNãoISO 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
questionIdSimSlug estável da pergunta (ex: receita_anual). Padrão: a-z, 0-9 e _. Mesmo ID em todas as submissões.
typeSimtext | number | select | multi_select | single_choice | boolean | scale | date
labelSimTexto da pergunta exibido no painel (PT-BR).
valueSimValor canônico: slug da opção, número, texto, boolean ou array de slugs (multi_select).
valueLabelNãoTexto legível da resposta quando value é slug (select, single_choice, boolean). O painel prioriza este campo.
valueLabelsNãoTextos legíveis para cada item de value em multi_select (mesma ordem e tamanho).

Exemplo completo

json
{
  "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 slugvalue = 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 (use resolveResultPageBaseUrl()); o funil de e-mail injeta lead, session, cupom e UTMs
  • buildResultUrl → monte o link permanente após o completeLead com publicKey e sessionId

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

typescript
// 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

http
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

typescript
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 eventos

Como 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_view sem step_complete na mesma etapa → viu mas não avançou
  • step_view sem question_answer → saiu antes de responder
  • diagnostic_start sem result_view e 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_startstartedstarts
contact_submitin_progress
result_viewcompletedcompletions
checkout_viewcheckout
checkout_startcheckout
payment_successpaidpaid
Outros eventos

Tipos de pagamento

Providers

stripemercado_pagopagarmehotmartkiwifyeduzzmonetizzemanualother

Métodos

pixcredit_carddebit_cardboletobank_transferother

Status

pendingpaidfailedrefunded

Tratamento 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.

json
{
  "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
401API key inválida, revogada ou header Authorization ausenteVerificar a chave de API e o formato Bearer.
401Header X-Diagnostic-Key ausente ou inválidoPassar o public_key ou UUID do diagnóstico.
404Diagnóstico não encontrado ou não publicadoVerificar se o diagnóstico está com status Publicado.
409Conflito de estado (sessão já paga, lead duplicado)Verificar alreadyExisted na resposta — não é erro crítico.
422Dados de entrada inválidosLer data.errors para cada campo com problema.
500Erro interno da LeadbleTentar novamente. O SDK faz retry automático com backoff.

Exemplo completo sem SDK

Para back-ends, automações ou linguagens sem o SDK.

javascript
// 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())