Skip to main content
O Boleto Bancário continua sendo um dos métodos de pagamento mais utilizados no Brasil, especialmente em transacoes B2B e por clientes que nao possuem cartao de credito. Este guia cobre o fluxo completo: desde a criacao do checkout ate a confirmacao do pagamento via webhook.

Visao geral do fluxo

1

Criar sessao de checkout

Seu backend cria uma sessao de checkout via API, informando o produto e o email do cliente.
2

Confirmar com metodo boleto

A sessao e confirmada com payment_method: 'boleto' e uma due_date (data de vencimento).
3

Receber dados do boleto

A Chargefy retorna o codigo de barras, a linha digitavel e a URL do PDF do boleto.
4

Exibir boleto ao cliente

Seu frontend exibe a linha digitavel com botao de copiar e o link para o PDF.
5

Aguardar compensacao

O cliente paga o boleto em qualquer banco, loterica ou app bancario. A compensacao leva de 1 a 3 dias uteis.
6

Receber webhook de confirmacao

Quando o pagamento e compensado, a Chargefy dispara o webhook checkout.updated para seu endpoint.

Pre-requisitos

npm install @chargefy/sdk
Configure o token de acesso:
.env
CHARGEFY_ACCESS_TOKEN=<supabase_jwt>
CHARGEFY_WEBHOOK_SECRET=whsec_seu_secret_aqui
Nunca commite o arquivo .env no repositorio. Adicione-o ao .gitignore.

Passo 1: Criar sessao de checkout

Crie uma sessao de checkout informando o produto e o email do cliente.

Com SDK

import { Chargefy } from '@chargefy/sdk'

const chargefy = new Chargefy({
  accessToken: process.env.CHARGEFY_ACCESS_TOKEN!,
})

const checkout = await chargefy.checkouts.create({
  productPriceId: 'price_xxxx',
  customerEmail: '[email protected]',
  successUrl: 'https://meusite.com.br/pagamento/sucesso',
})

console.log('Checkout ID:', checkout.id)
console.log('Client Secret:', checkout.clientSecret)

Com cURL

curl -X POST https://api.chargefy.io/v1/checkouts \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "product_price_id": "price_xxxx",
    "customer_email": "[email protected]",
    "success_url": "https://meusite.com.br/pagamento/sucesso"
  }'
A resposta inclui o id e o client_secret necessarios para confirmar o pagamento.

Passo 2: Confirmar com metodo boleto

Confirme a sessao de checkout escolhendo boleto como metodo de pagamento. O parametro due_date define a data de vencimento do boleto.

Com SDK

const result = await chargefy.checkouts.confirm(checkout.clientSecret, {
  customerName: 'Maria Souza',
  customerEmail: '[email protected]',
  customerTaxId: '123.456.789-00', // CPF ou CNPJ
  paymentMethod: 'boleto',
  boleto: {
    dueDate: '2026-03-17', // YYYY-MM-DD — minimo 1 dia util
  },
})

console.log('Status:', result.status) // 'confirmed'
console.log('Codigo de barras:', result.paymentDetails?.boletoBarcode)
console.log('Linha digitavel:', result.paymentDetails?.boletoDigitableLine)
console.log('PDF:', result.paymentDetails?.boletoPdfUrl)
console.log('Vencimento:', result.paymentDetails?.boletoDueDate)

Com cURL

curl -X POST https://api.chargefy.io/v1/checkouts/cs_xxxx/confirm \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Maria Souza",
    "customer_email": "[email protected]",
    "customer_tax_id": "123.456.789-00",
    "payment_method": "boleto",
    "boleto": {
      "due_date": "2026-03-17"
    }
  }'

Resposta

{
  "id": "checkout_xxxx",
  "status": "confirmed",
  "payment_method": "boleto",
  "amount": 15000,
  "currency": "BRL",
  "payment_details": {
    "boleto_barcode": "23793.38128 60000.000003 00000.000400 1 84340000015000",
    "boleto_digitable_line": "23793381286000000000300000004001843400001500",
    "boleto_pdf_url": "https://api.chargefy.io/v1/boletos/bol_xxxx/pdf",
    "boleto_due_date": "2026-03-17"
  }
}
O campo amount esta em centavos. O valor 15000 corresponde a R$ 150,00.

Passo 3: Obter dados do boleto

A resposta da confirmacao inclui todos os dados necessarios para exibir o boleto ao cliente:
CampoDescricao
boleto_barcodeCodigo de barras numerico (47 digitos) — usado por leitores de codigo de barras
boleto_digitable_lineLinha digitavel formatada — o cliente digita manualmente no app do banco
boleto_pdf_urlURL do PDF do boleto completo para download ou impressao
boleto_due_dateData de vencimento no formato YYYY-MM-DD
Armazene o boleto_pdf_url e a boleto_digitable_line no seu banco de dados para que o cliente possa acessar novamente caso feche a pagina.

Passo 4: Exibir boleto ao cliente

Crie um componente React que exibe a linha digitavel com botao de copiar e o link para o PDF.
import { useState } from 'react'

interface BoletoDisplayProps {
  digitableLine: string
  pdfUrl: string
  dueDate: string
  amount: number // em centavos
}

export function BoletoDisplay({
  digitableLine,
  pdfUrl,
  dueDate,
  amount,
}: BoletoDisplayProps) {
  const [copied, setCopied] = useState(false)

  const formattedAmount = new Intl.NumberFormat('pt-BR', {
    style: 'currency',
    currency: 'BRL',
  }).format(amount / 100)

  const formattedDueDate = new Intl.DateTimeFormat('pt-BR', {
    day: '2-digit',
    month: '2-digit',
    year: 'numeric',
  }).format(new Date(dueDate + 'T12:00:00'))

  async function handleCopy() {
    try {
      await navigator.clipboard.writeText(digitableLine)
      setCopied(true)
      setTimeout(() => setCopied(false), 3000)
    } catch {
      // Fallback para navegadores mais antigos
      const textarea = document.createElement('textarea')
      textarea.value = digitableLine
      document.body.appendChild(textarea)
      textarea.select()
      document.execCommand('copy')
      document.body.removeChild(textarea)
      setCopied(true)
      setTimeout(() => setCopied(false), 3000)
    }
  }

  return (
    <div className="max-w-lg mx-auto p-6 bg-white rounded-lg shadow-md">
      <div className="text-center mb-6">
        <h2 className="text-xl font-bold text-gray-900">
          Boleto Bancario
        </h2>
        <p className="text-2xl font-bold text-green-600 mt-2">
          {formattedAmount}
        </p>
        <p className="text-sm text-gray-500 mt-1">
          Vencimento: {formattedDueDate}
        </p>
      </div>

      {/* Linha digitavel */}
      <div className="mb-4">
        <label className="block text-sm font-medium text-gray-700 mb-2">
          Linha digitavel
        </label>
        <div className="flex items-center gap-2">
          <code className="flex-1 p-3 bg-gray-50 border rounded text-sm font-mono break-all">
            {digitableLine}
          </code>
          <button
            onClick={handleCopy}
            className={`px-4 py-3 rounded font-medium text-sm whitespace-nowrap ${
              copied
                ? 'bg-green-100 text-green-700'
                : 'bg-blue-600 text-white hover:bg-blue-700'
            }`}
          >
            {copied ? 'Copiado!' : 'Copiar'}
          </button>
        </div>
      </div>

      {/* Botao PDF */}
      <a
        href={pdfUrl}
        target="_blank"
        rel="noopener noreferrer"
        className="block w-full text-center py-3 bg-gray-100 hover:bg-gray-200 rounded font-medium text-gray-700 transition"
      >
        Baixar PDF do Boleto
      </a>

      {/* Aviso de compensacao */}
      <div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded">
        <p className="text-sm text-yellow-800">
          Apos o pagamento, a compensacao leva de{' '}
          <strong>1 a 3 dias uteis</strong>. Voce recebera um email de
          confirmacao assim que o pagamento for processado.
        </p>
      </div>
    </div>
  )
}

Uso do componente

// Apos confirmar o checkout, passe os dados do boleto para o componente
<BoletoDisplay
  digitableLine={result.paymentDetails.boletoDigitableLine}
  pdfUrl={result.paymentDetails.boletoPdfUrl}
  dueDate={result.paymentDetails.boletoDueDate}
  amount={result.amount}
/>

Passo 5: Aguardar compensacao do pagamento

Diferente do PIX (instantaneo) e do cartao de credito, o boleto tem compensacao assincrona de 1 a 3 dias uteis. A abordagem recomendada e via webhooks.

Configurar webhook

Configure um endpoint de webhook no dashboard da Chargefy para receber os eventos:
  • checkout.updated — status do checkout muda
  • subscription.active — pagamento confirmado (compensado)

Implementacao do webhook handler

import { Router, raw } from 'express'
import crypto from 'crypto'

export const webhooksRouter = Router()

webhooksRouter.use(raw({ type: 'application/json' }))

function verifyWebhookSignature(
  payload: Buffer,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

webhooksRouter.post('/', (req, res) => {
  const signature = req.headers['webhook-signature'] as string
  const webhookSecret = process.env.CHARGEFY_WEBHOOK_SECRET!

  if (!signature) {
    return res.status(401).json({ error: 'Assinatura ausente' })
  }

  const isValid = verifyWebhookSignature(req.body, signature, webhookSecret)
  if (!isValid) {
    return res.status(401).json({ error: 'Assinatura invalida' })
  }

  const event = JSON.parse(req.body.toString())

  switch (event.type) {
    case 'checkout.updated':
      handleCheckoutUpdated(event.data)
      break
  }

  // Sempre responder 200 rapidamente
  res.status(200).json({ received: true })
})

function handleCheckoutUpdated(data: any) {
  if (data.status === 'succeeded') {
    console.log('Boleto pago! Checkout:', data.id)

    // Aqui voce deve:
    // 1. Buscar a cobranca no seu banco de dados
    // 2. Atualizar o status para "pago"
    // 3. Liberar acesso ao produto/servico
    // 4. Enviar email de confirmacao ao cliente
  }

  if (data.status === 'expired') {
    console.log('Boleto vencido:', data.id)
    // Notificar cliente, oferecer novo boleto
  }
}
Responda ao webhook com status 200 o mais rapido possivel. Processe logica pesada (emails, integracoes) de forma assincrona usando filas como BullMQ.

Fluxo de status do checkout com boleto

criado → confirmed (boleto gerado) → succeeded (pago)
                                   → expired (vencido)
StatusDescricao
confirmedBoleto gerado, aguardando pagamento do cliente
succeededPagamento compensado pelo banco
expiredBoleto venceu sem pagamento

Passo 6: Tratar vencimento e cancelamento

Boletos vencidos nao podem mais ser pagos. Implemente logica para lidar com essa situacao.

Verificar status do checkout

// Consultar status periodicamente ou apos receber webhook
const checkout = await chargefy.checkouts.getByClientSecret(clientSecret)

switch (checkout.status) {
  case 'confirmed':
    // Boleto emitido, aguardando pagamento
    console.log('Aguardando pagamento do boleto...')
    break

  case 'succeeded':
    // Pagamento confirmado
    console.log('Pagamento confirmado!')
    await ativarProdutoParaCliente(checkout.customerId)
    break

  case 'expired':
    // Boleto venceu
    console.log('Boleto vencido.')
    await notificarClienteBoletoVencido(checkout.customerEmail)
    break
}

Gerar novo boleto apos vencimento

Quando um boleto vence, crie uma nova sessao de checkout para o mesmo cliente:
async function reemitirBoleto(
  productPriceId: string,
  customerEmail: string,
  customerName: string,
  customerTaxId: string
) {
  // Criar nova sessao
  const novoCheckout = await chargefy.checkouts.create({
    productPriceId,
    customerEmail,
    successUrl: 'https://meusite.com.br/pagamento/sucesso',
  })

  // Confirmar com novo vencimento
  const result = await chargefy.checkouts.confirm(novoCheckout.clientSecret, {
    customerName,
    customerEmail,
    customerTaxId,
    paymentMethod: 'boleto',
    boleto: {
      dueDate: calcularProximoVencimento(), // ex: 3 dias uteis a partir de hoje
    },
  })

  return result
}

function calcularProximoVencimento(): string {
  const hoje = new Date()
  let diasAdicionados = 0

  while (diasAdicionados < 3) {
    hoje.setDate(hoje.getDate() + 1)
    const diaSemana = hoje.getDay()
    // Pular sabados (6) e domingos (0)
    if (diaSemana !== 0 && diaSemana !== 6) {
      diasAdicionados++
    }
  }

  return hoje.toISOString().split('T')[0]
}
Nao tente reutilizar um checkout com status expired. Sempre crie uma nova sessao de checkout.

Exemplo completo

Rota Express para checkout com boleto

src/routes/boleto-checkout.ts
import { Router } from 'express'
import { Chargefy } from '@chargefy/sdk'

const chargefy = new Chargefy({
  accessToken: process.env.CHARGEFY_ACCESS_TOKEN!,
})

export const boletoRouter = Router()

// Criar e confirmar checkout com boleto em um unico endpoint
boletoRouter.post('/gerar', async (req, res) => {
  try {
    const {
      productPriceId,
      customerName,
      customerEmail,
      customerTaxId,
      dueDate,
    } = req.body

    // Validar data de vencimento
    const due = new Date(dueDate)
    const hoje = new Date()
    hoje.setHours(0, 0, 0, 0)

    if (due <= hoje) {
      return res.status(400).json({
        error: 'A data de vencimento deve ser posterior a hoje',
      })
    }

    const diffDias = Math.ceil(
      (due.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24)
    )

    if (diffDias > 30) {
      return res.status(400).json({
        error: 'A data de vencimento nao pode ser superior a 30 dias',
      })
    }

    // 1. Criar sessao de checkout
    const checkout = await chargefy.checkouts.create({
      productPriceId,
      customerEmail,
      successUrl: `${process.env.FRONTEND_URL}/pagamento/sucesso`,
    })

    // 2. Confirmar com boleto
    const result = await chargefy.checkouts.confirm(checkout.clientSecret, {
      customerName,
      customerEmail,
      customerTaxId,
      paymentMethod: 'boleto',
      boleto: { dueDate },
    })

    // 3. Retornar dados do boleto
    res.json({
      checkoutId: checkout.id,
      status: result.status,
      boleto: {
        barcode: result.paymentDetails?.boletoBarcode,
        digitableLine: result.paymentDetails?.boletoDigitableLine,
        pdfUrl: result.paymentDetails?.boletoPdfUrl,
        dueDate: result.paymentDetails?.boletoDueDate,
      },
      amount: result.amount,
      currency: result.currency,
    })
  } catch (error: any) {
    console.error('Erro ao gerar boleto:', error)
    res.status(400).json({
      error: error.message || 'Falha ao gerar boleto',
    })
  }
})

// Consultar status do boleto
boletoRouter.get('/status/:clientSecret', async (req, res) => {
  try {
    const checkout = await chargefy.checkouts.getByClientSecret(
      req.params.clientSecret
    )

    res.json({
      status: checkout.status,
      paymentMethod: checkout.paymentMethod,
      amount: checkout.amount,
    })
  } catch (error) {
    res.status(404).json({ error: 'Checkout nao encontrado' })
  }
})

Pagina React completa

src/pages/BoletoCheckoutPage.tsx
import { useState } from 'react'
import { BoletoDisplay } from '../components/BoletoDisplay'

interface BoletoData {
  barcode: string
  digitableLine: string
  pdfUrl: string
  dueDate: string
}

export function BoletoCheckoutPage() {
  const [loading, setLoading] = useState(false)
  const [boleto, setBoleto] = useState<BoletoData | null>(null)
  const [amount, setAmount] = useState(0)
  const [error, setError] = useState('')

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setLoading(true)
    setError('')

    const formData = new FormData(e.currentTarget)

    try {
      const response = await fetch('/api/boleto/gerar', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          productPriceId: formData.get('productPriceId'),
          customerName: formData.get('customerName'),
          customerEmail: formData.get('customerEmail'),
          customerTaxId: formData.get('customerTaxId'),
          dueDate: formData.get('dueDate'),
        }),
      })

      if (!response.ok) {
        const data = await response.json()
        throw new Error(data.error || 'Erro ao gerar boleto')
      }

      const data = await response.json()
      setBoleto(data.boleto)
      setAmount(data.amount)
    } catch (err: any) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }

  if (boleto) {
    return (
      <BoletoDisplay
        digitableLine={boleto.digitableLine}
        pdfUrl={boleto.pdfUrl}
        dueDate={boleto.dueDate}
        amount={amount}
      />
    )
  }

  return (
    <form onSubmit={handleSubmit} className="max-w-lg mx-auto p-6 space-y-4">
      <h1 className="text-2xl font-bold">Pagar com Boleto</h1>

      {error && (
        <div className="p-3 bg-red-50 text-red-700 rounded">{error}</div>
      )}

      <input type="hidden" name="productPriceId" value="price_xxxx" />

      <div>
        <label className="block text-sm font-medium mb-1">Nome completo</label>
        <input
          name="customerName"
          required
          className="w-full p-2 border rounded"
          placeholder="Maria Souza"
        />
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">Email</label>
        <input
          name="customerEmail"
          type="email"
          required
          className="w-full p-2 border rounded"
          placeholder="[email protected]"
        />
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">CPF ou CNPJ</label>
        <input
          name="customerTaxId"
          required
          className="w-full p-2 border rounded"
          placeholder="123.456.789-00"
        />
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">
          Data de vencimento
        </label>
        <input
          name="dueDate"
          type="date"
          required
          className="w-full p-2 border rounded"
        />
      </div>

      <button
        type="submit"
        disabled={loading}
        className="w-full py-3 bg-blue-600 text-white rounded font-medium hover:bg-blue-700 disabled:opacity-50"
      >
        {loading ? 'Gerando boleto...' : 'Gerar Boleto'}
      </button>
    </form>
  )
}

Testar com cURL

# Gerar boleto
curl -X POST http://localhost:3001/api/boleto/gerar \
  -H "Content-Type: application/json" \
  -d '{
    "productPriceId": "price_xxxx",
    "customerName": "Maria Souza",
    "customerEmail": "[email protected]",
    "customerTaxId": "123.456.789-00",
    "dueDate": "2026-03-17"
  }'

# Consultar status
curl http://localhost:3001/api/boleto/status/cs_xxxx

Boas praticas para data de vencimento

A data de vencimento (due_date) e um dos parametros mais importantes ao gerar um boleto. Escolher mal pode resultar em boletos nao pagos ou cancelados.
RegraDescricao
Minimo 1 dia utilA Chargefy exige ao menos 1 dia util entre a emissao e o vencimento
Maximo 30 diasBoletos com vencimento superior a 30 dias tem taxa de conversao muito baixa
Recomendado: 3 dias uteisEquilibrio entre urgencia e prazo para o cliente pagar
Evitar finais de semanaPrefira vencimentos em dias uteis para facilitar o pagamento
Considerar feriadosBancos nao processam boletos em feriados nacionais

Funcao utilitaria para calculo de vencimento

/**
 * Calcula uma data de vencimento valida considerando dias uteis.
 * @param diasUteis Numero de dias uteis a partir de hoje (padrao: 3)
 * @returns Data no formato YYYY-MM-DD
 */
export function calcularVencimentoBoleto(diasUteis: number = 3): string {
  const data = new Date()
  let adicionados = 0

  while (adicionados < diasUteis) {
    data.setDate(data.getDate() + 1)
    const dia = data.getDay()
    if (dia !== 0 && dia !== 6) {
      adicionados++
    }
  }

  return data.toISOString().split('T')[0]
}

// Exemplos:
// calcularVencimentoBoleto(1)  → proximo dia util
// calcularVencimentoBoleto(3)  → 3 dias uteis (padrao)
// calcularVencimentoBoleto(10) → 10 dias uteis
Para um calculo mais preciso em producao, considere integrar uma biblioteca de feriados brasileiros como brasil-feriados para pular feriados nacionais e estaduais.

Testando no sandbox

O ambiente de sandbox da Chargefy permite testar o fluxo completo de boleto sem movimentar dinheiro real.

Configurar ambiente sandbox

const chargefy = new Chargefy({
  accessToken: process.env.CHARGEFY_ACCESS_TOKEN!,
  server: 'sandbox',
})

Simular pagamento de boleto

No sandbox, voce pode simular a compensacao de um boleto via API:
# Simular pagamento de um boleto no sandbox
curl -X POST https://sandbox-api.chargefy.io/v1/testing/boletos/bol_xxxx/pay \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN"
Isso dispara o fluxo completo de compensacao, incluindo o webhook checkout.updated, permitindo testar seu handler sem esperar dias.

CPFs de teste

Use estes CPFs no sandbox para diferentes cenarios:
CPFComportamento
111.111.111-11Boleto gerado com sucesso
222.222.222-22Boleto rejeitado pelo banco
333.333.333-33Boleto vence automaticamente apos 1 minuto
Use o endpoint de webhooks de teste no dashboard para reenviar eventos e depurar seu handler.

Problemas comuns e solucao

Erro: “due_date must be at least 1 business day in the future”

A data de vencimento e hoje ou uma data passada. Use a funcao calcularVencimentoBoleto() para garantir uma data valida.

Erro: “invalid tax_id format”

O CPF ou CNPJ esta em formato invalido. Aceite ambos os formatos (com e sem pontuacao) e normalize antes de enviar:
function normalizarTaxId(taxId: string): string {
  return taxId.replace(/[.\-\/]/g, '')
}

// "123.456.789-00" → "12345678900"
// "12.345.678/0001-90" → "12345678000190"

Boleto gerado mas webhook nao chega

  1. Verifique se o endpoint de webhook esta configurado no dashboard
  2. Confirme que seu servidor responde com status 200 dentro de 30 segundos
  3. Verifique os logs de entrega em Webhooks > Delivery
  4. No sandbox, use o endpoint de teste para simular a compensacao

Boleto pago mas status nao atualiza

A compensacao bancaria leva de 1 a 3 dias uteis. Nao e instantanea. Se apos 3 dias uteis o status nao atualizar:
  1. Verifique o status diretamente via API
  2. Consulte os logs de webhook no dashboard
  3. Entre em contato com o suporte

Cliente nao consegue pagar o boleto

  • Verifique se o boleto nao venceu
  • Confirme que a linha digitavel esta completa (sem caracteres cortados)
  • Sugira ao cliente baixar o PDF e pagar por leitura de codigo de barras

Proximos passos

Checkout PIX

Guia completo do fluxo de pagamento PIX com confirmacao instantanea.

Webhooks

Documentacao completa de webhooks e todos os eventos disponiveis.

Metodos de Pagamento

Comparativo entre PIX, Cartao e Boleto.

Sandbox

Como testar pagamentos sem cobrar de verdade.