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
Criar sessao de checkout
Seu backend cria uma sessao de checkout via API, informando o produto e o email do cliente.
Confirmar com metodo boleto
A sessao e confirmada com payment_method: 'boleto' e uma due_date (data de vencimento).
Receber dados do boleto
A Chargefy retorna o codigo de barras, a linha digitavel e a URL do PDF do boleto.
Exibir boleto ao cliente
Seu frontend exibe a linha digitavel com botao de copiar e o link para o PDF.
Aguardar compensacao
O cliente paga o boleto em qualquer banco, loterica ou app bancario. A compensacao leva de 1 a 3 dias uteis.
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:
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.
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 )
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.
Confirme a sessao de checkout escolhendo boleto como metodo de pagamento. O parametro due_date define a data de vencimento do boleto.
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 )
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:
Campo Descricao 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 .
criado → confirmed (boleto gerado) → succeeded (pago)
→ expired (vencido)
Status Descricao 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
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 >
)
}
# 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.
Regra Descricao Minimo 1 dia util A Chargefy exige ao menos 1 dia util entre a emissao e o vencimento Maximo 30 dias Boletos com vencimento superior a 30 dias tem taxa de conversao muito baixa Recomendado: 3 dias uteis Equilibrio entre urgencia e prazo para o cliente pagar Evitar finais de semana Prefira vencimentos em dias uteis para facilitar o pagamento Considerar feriados Bancos 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:
CPF Comportamento 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.
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
Verifique se o endpoint de webhook esta configurado no dashboard
Confirme que seu servidor responde com status 200 dentro de 30 segundos
Verifique os logs de entrega em Webhooks > Delivery
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:
Verifique o status diretamente via API
Consulte os logs de webhook no dashboard
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.