Documentação da plataforma

Para quem vende e para quem integra: o produto sem código e a API, cada tópico em sua página.

Webhooks e eventos

A 3B avisa o SEU servidor a cada venda paga. Use para liberar acesso, registrar no seu CRM ou disparar os seus fluxos, sem depender do comprador ficar na página de obrigado.

Como ligar (2 minutos)

No painel, vá em Integrações → Webhook, informe a URL do seu endpoint (precisa ser https) e salve. Copie o segredo que aparece — é com ele que você confere a assinatura. Use o botão Enviar teste para disparar um evento na hora e ver a resposta do seu servidor; o payload de teste vem com "test": true, então não libere acesso com ele.

Eventos enviados hoje:

payment_intent.succeededA venda foi PAGA, na Pagar.me (real) ou na Whop (demais moedas). É o gatilho de entrega.

O nome do evento é o mesmo para as duas processadoras, então a sua integração não muda conforme a moeda da venda. O evento payment_intent.payment_failed (pagamento recusado) existia no checkout anterior e não é enviado nas vendas pela Pagar.me e pela Whop: trate só o de venda paga.

O webhook é uma notificação, não a fonte da verdade. Quem marca o pedido como pago, cobra as taxas e envia o e-mail de acesso continua sendo a 3B — se o seu servidor estiver fora do ar, a venda acontece do mesmo jeito e nós reentregamos o evento depois.

O que chega (payload)

Um POST com Content-Type: application/json. Valores monetários vêm sempre em centavos (3500 = 35,00).

{
  "id": "evt_9f1c…_paid",                 // id ESTÁVEL do evento (use p/ deduplicar)
  "type": "payment_intent.succeeded",     // venda paga
  "created": "2026-07-13T14:03:11.482Z",
  "data": {
    "order": {
      "id": "9f1c…",                      // id do pedido na 3B
      "status": "paid",                   // paid | failed
      "currency": "eur",
      "amount_total": 3500,               // CENTAVOS (35,00)
      "seller_amount": 3010,              // o que fica com você (centavos)
      "platform_fee": 490,                // taxa da 3B (centavos)
      "quantity": 1,
      "product_id": "b2a…",
      "product_name": "e-book Estrategias virales",
      "store_id": "77e…",                 // sua loja (multi-loja)
      "variant_label": null,
      "is_subscription": false,
      "api_charge": false,                // true = cobrança da API de cartão (sem produto)
      "reference": null,                  // o reference que você mandou na API
      "buyer": {
        "email": "comprador@email.com",
        "name": "Maria Silva",
        "phone": null
      },
      "decline_code": null,               // só no evento de falha (vendas antigas)
      "payment_intent_id": null,          // só em vendas antigas; nas atuais vem null
      "tracking": { "utm_source": "tiktok", "utm_campaign": "abc" },
      "created_at": "2026-07-13T14:02:47.113Z"
    }
  }
}

Cabeçalhos:

X-3B-Event-IdId estável do evento. É a sua chave de idempotência.
X-3B-Event-Typepayment_intent.succeeded
X-3B-TimestampUnix (segundos) de quando assinamos.
X-3B-Signaturet=<timestamp>,v1=<hmac-sha256 hex de "<timestamp>.<corpo cru>">

Confira a assinatura (obrigatório)

Seu endpoint é público: qualquer um pode mandar um POST fingindo uma venda. Antes de liberar qualquer coisa, valide o header X-3B-Signature — é um HMAC-SHA256 de <timestamp>.<corpo cru> com o seu segredo. Use o corpo cru: se você fizer parse e re-serializar o JSON, os bytes mudam e a assinatura não bate.

// Node/Express — confira a assinatura ANTES de confiar no evento.
// Precisa do corpo CRU (raw): JSON.parse desfaz o texto exato que assinamos.
import crypto from 'node:crypto'
import express from 'express'

const app = express()
const SECRET = process.env.WEBHOOK_SECRET_3B // painel → Integrações → Webhook

app.post('/webhooks/3b', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('X-3B-Signature') || ''            // "t=1763…,v1=9a4f…"
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const raw = req.body.toString('utf8')

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${parts.t}.${raw}`)
    .digest('hex')

  // timingSafeEqual evita vazar a assinatura por tempo de resposta.
  const ok =
    parts.v1 &&
    parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))

  if (!ok) return res.status(400).send('assinatura inválida')

  // Rejeite eventos velhos (proteção contra reenvio de um POST capturado).
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
    return res.status(400).send('evento expirado')
  }

  const event = JSON.parse(raw)

  // IDEMPOTÊNCIA: o mesmo evento pode chegar mais de uma vez.
  if (jaProcessei(event.id)) return res.status(200).send('ok')   // seu banco
  marcarProcessado(event.id)

  if (event.type === 'payment_intent.succeeded' && !event.test) {
    liberarAcesso(event.data.order)      // seu sistema
  }

  res.status(200).send('ok')             // responda 2xx RÁPIDO; processe depois
})

Retentativa e idempotência

Consideramos entregue quando o seu servidor responde 2xx. Qualquer outra resposta (ou timeout de 10s) entra na fila de retentativa: 1 min → 5 min → 30 min → 2 h → 6 h. Depois de 6 tentativas sem sucesso, desistimos e a entrega fica marcada como falha — você ainda pode reenviá-la à mão em Integrações → Webhook → Últimas entregas, onde também aparece o código e o erro que o seu servidor devolveu.

Por causa da retentativa, o mesmo evento pode chegar mais de uma vez. O id do evento (e o header X-3B-Event-Id) é estável: o mesmo pedido, no mesmo tipo de evento, sempre carrega o mesmo id. Guarde os ids já processados e ignore repetidos — senão você libera o acesso (ou cobra o cliente) duas vezes.

Responda 2xx primeiro e processe depois. Se o seu endpoint demorar mais de 10 segundos, nós tratamos como falha e reenviamos — e aí o seu sistema recebe o mesmo evento de novo, mesmo tendo processado.