O PIX é o método de pagamento instantâneo mais popular do Brasil, com confirmação em segundos e disponibilidade 24/7. Este guia mostra como implementar o fluxo completo de checkout PIX na Chargefy, do backend ao frontend.
Visão geral do fluxo
O pagamento PIX na Chargefy segue este fluxo:
Criar sessão de checkout
Seu backend cria uma sessão de checkout via API ou SDK.
Confirmar com PIX
O checkout é confirmado com paymentMethod: 'pix', gerando um QR Code.
Exibir QR Code
Seu frontend exibe o QR Code e o código copia e cola para o cliente.
Cliente paga
O cliente escaneia o QR Code ou cola o código no app do banco.
Confirmação instantânea
A Chargefy recebe a confirmação do pagamento e notifica via webhook. Seu frontend pode fazer polling para atualizar a tela.
Pré-requisitos
npm install @chargefy/sdk
import { Chargefy } from '@chargefy/sdk'
export const chargefy = new Chargefy ({
accessToken: process . env . CHARGEFY_ACCESS_TOKEN ! ,
// Para sandbox:
// server: 'sandbox'
})
Passo 1: Criar sessão de checkout
Crie uma sessão de checkout associada ao produto/preço desejado.
import { chargefy } from './lib/chargefy.js'
const checkout = await chargefy . checkouts . create ({
productPriceId: 'price_xxxx' ,
customerEmail: '[email protected] ' ,
successUrl: 'https://meusite.com.br/pagamento/sucesso' ,
})
console . log ( 'Checkout criado:' , checkout . id )
console . log ( 'Client Secret:' , checkout . clientSecret )
Resposta:
{
"id" : "checkout_abc123" ,
"client_secret" : "cs_live_xxxxxxxxxxxx" ,
"url" : "https://checkout.chargefy.io/cs_live_xxxxxxxxxxxx" ,
"status" : "open" ,
"amount" : 9990 ,
"currency" : "BRL" ,
"expires_at" : "2026-03-12T23:59:59Z"
}
O campo amount usa centavos. O valor 9990 equivale a R$ 99,90 .
Com o clientSecret em mãos, confirme o checkout especificando PIX como método de pagamento.
const result = await chargefy . checkouts . confirm ( checkout . clientSecret , {
customerEmail: '[email protected] ' ,
customerName: 'Maria Silva' ,
paymentMethod: 'pix' ,
})
// Dados do PIX para exibir ao cliente
const {
status , // 'pending' — aguardando pagamento
paymentDetails ,
} = result
const {
pixQrCode , // URL da imagem do QR Code
pixQrCodeBase64 , // QR Code em base64 (para renderizar inline)
pixCopyPaste , // Código copia e cola (EMV)
expiresAt , // Expiração do PIX (geralmente 30 min)
} = paymentDetails
Resposta:
{
"status" : "pending" ,
"transaction_id" : "txn_xyz789" ,
"payment_details" : {
"pix_qr_code" : "https://api.chargefy.io/v1/pix/qrcode/txid_abc.png" ,
"pix_qr_code_base64" : "data:image/png;base64,iVBORw0KGgo..." ,
"pix_copy_paste" : "00020126580014br.gov.bcb.pix0136a1b2c3d4-e5f6-7890..." ,
"expires_at" : "2026-03-12T13:30:00Z"
}
}
O QR Code PIX tem prazo de validade (geralmente 30 minutos). Após expirar, o checkout precisa ser confirmado novamente para gerar um novo QR Code.
Passo 3: Exibir QR Code para o cliente
No frontend, exiba o QR Code e o código copia e cola. Aqui esta um componente React completo:
components/PixPayment.tsx
import { useState , useEffect , useCallback } from 'react'
interface PixData {
pixQrCode : string
pixQrCodeBase64 : string
pixCopyPaste : string
expiresAt : string
}
interface PixPaymentProps {
pixData : PixData
clientSecret : string
onPaymentConfirmed : () => void
}
export function PixPayment ({
pixData ,
clientSecret ,
onPaymentConfirmed ,
} : PixPaymentProps ) {
const [ copied , setCopied ] = useState ( false )
const [ status , setStatus ] = useState < 'pending' | 'succeeded' | 'expired' >( 'pending' )
const [ timeLeft , setTimeLeft ] = useState ( '' )
// Calcular tempo restante
useEffect (() => {
const expiresAt = new Date ( pixData . expiresAt ). getTime ()
const timer = setInterval (() => {
const now = Date . now ()
const diff = expiresAt - now
if ( diff <= 0 ) {
setStatus ( 'expired' )
setTimeLeft ( 'Expirado' )
clearInterval ( timer )
return
}
const minutes = Math . floor ( diff / 60000 )
const seconds = Math . floor (( diff % 60000 ) / 1000 )
setTimeLeft ( ` ${ minutes } : ${ seconds . toString (). padStart ( 2 , '0' ) } ` )
}, 1000 )
return () => clearInterval ( timer )
}, [ pixData . expiresAt ])
// Polling para verificar pagamento (a cada 3 segundos)
useEffect (() => {
if ( status !== 'pending' ) return
const poll = setInterval ( async () => {
try {
const res = await fetch ( `/api/checkout/sessions/ ${ clientSecret } /status` )
const data = await res . json ()
if ( data . status === 'succeeded' ) {
setStatus ( 'succeeded' )
clearInterval ( poll )
onPaymentConfirmed ()
}
} catch ( err ) {
console . error ( 'Erro ao verificar status:' , err )
}
}, 3000 )
return () => clearInterval ( poll )
}, [ clientSecret , status , onPaymentConfirmed ])
const handleCopy = useCallback ( async () => {
try {
await navigator . clipboard . writeText ( pixData . pixCopyPaste )
setCopied ( true )
setTimeout (() => setCopied ( false ), 3000 )
} catch {
// Fallback para navegadores sem suporte a clipboard API
const textarea = document . createElement ( 'textarea' )
textarea . value = pixData . pixCopyPaste
document . body . appendChild ( textarea )
textarea . select ()
document . execCommand ( 'copy' )
document . body . removeChild ( textarea )
setCopied ( true )
setTimeout (() => setCopied ( false ), 3000 )
}
}, [ pixData . pixCopyPaste ])
if ( status === 'succeeded' ) {
return (
< div className = "text-center p-8" >
< div className = "text-green-500 text-5xl mb-4" > ✓ </ div >
< h2 className = "text-2xl font-bold text-green-700" >
Pagamento confirmado!
</ h2 >
< p className = "text-gray-600 mt-2" >
Seu pagamento via PIX foi recebido com sucesso.
</ p >
</ div >
)
}
if ( status === 'expired' ) {
return (
< div className = "text-center p-8" >
< h2 className = "text-xl font-bold text-red-600" > QR Code expirado </ h2 >
< p className = "text-gray-600 mt-2" >
O prazo para pagamento expirou. Por favor, gere um novo QR Code.
</ p >
< button
onClick = { () => window . location . reload () }
className = "mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg"
>
Tentar novamente
</ button >
</ div >
)
}
return (
< div className = "max-w-md mx-auto p-6" >
< h2 className = "text-xl font-bold text-center mb-2" >
Pague com PIX
</ h2 >
< p className = "text-gray-500 text-center text-sm mb-6" >
Escaneie o QR Code ou copie o codigo abaixo
</ p >
{ /* QR Code */ }
< div className = "flex justify-center mb-6" >
< img
src = { pixData . pixQrCodeBase64 || pixData . pixQrCode }
alt = "QR Code PIX"
className = "w-64 h-64 border rounded-lg"
/>
</ div >
{ /* Tempo restante */ }
< div className = "text-center text-sm text-gray-500 mb-4" >
Expira em: < span className = "font-mono font-bold" > { timeLeft } </ span >
</ div >
{ /* Codigo copia e cola */ }
< div className = "bg-gray-50 rounded-lg p-4 mb-4" >
< p className = "text-xs text-gray-500 mb-2" > PIX Copia e Cola: </ p >
< div className = "flex items-center gap-2" >
< code className = "flex-1 text-xs break-all bg-white p-2 rounded border" >
{ pixData . pixCopyPaste }
</ code >
< button
onClick = { handleCopy }
className = "shrink-0 px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
>
{ copied ? 'Copiado!' : 'Copiar' }
</ button >
</ div >
</ div >
{ /* Instrucoes */ }
< div className = "text-sm text-gray-600 space-y-2" >
< p > 1. Abra o app do seu banco </ p >
< p > 2. Escolha pagar com PIX </ p >
< p > 3. Escaneie o QR Code ou cole o codigo </ p >
< p > 4. Confirme o pagamento </ p >
</ div >
{ /* Indicador de aguardando */ }
< div className = "flex items-center justify-center gap-2 mt-6 text-blue-600" >
< svg className = "animate-spin h-4 w-4" viewBox = "0 0 24 24" >
< circle
className = "opacity-25"
cx = "12" cy = "12" r = "10"
stroke = "currentColor"
strokeWidth = "4"
fill = "none"
/>
< path
className = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</ svg >
< span className = "text-sm" > Aguardando pagamento... </ span >
</ div >
</ div >
)
}
Passo 4: Aguardar confirmação do pagamento
Existem duas abordagens para saber quando o pagamento foi confirmado: webhook (recomendado) e polling .
Abordagem 1: Webhook (recomendado)
Configure um endpoint de webhook para receber a notificacao da Chargefy quando o pagamento PIX for confirmado.
src/routes/webhooks-pix.ts
import { Router , raw } from 'express'
import crypto from 'crypto'
export const pixWebhooksRouter = Router ()
pixWebhooksRouter . use ( raw ({ type: 'application/json' }))
function verifySignature ( payload : Buffer , signature : string , secret : string ) : boolean {
const expected = crypto . createHmac ( 'sha256' , secret ). update ( payload ). digest ( 'hex' )
return crypto . timingSafeEqual ( Buffer . from ( signature ), Buffer . from ( expected ))
}
pixWebhooksRouter . post ( '/' , ( req , res ) => {
const signature = req . headers [ 'webhook-signature' ] as string
const secret = process . env . CHARGEFY_WEBHOOK_SECRET !
if ( ! signature || ! verifySignature ( req . body , signature , secret )) {
return res . status ( 401 ). json ({ error: 'Assinatura invalida' })
}
const event = JSON . parse ( req . body . toString ())
switch ( event . type ) {
case 'checkout.updated' : {
const checkout = event . data
if ( checkout . status === 'succeeded' && checkout . paymentMethod === 'pix' ) {
console . log ( `PIX confirmado! Checkout: ${ checkout . id } ` )
console . log ( `Valor: R$ ${ ( checkout . amount / 100 ). toFixed ( 2 ) } ` )
console . log ( `Cliente: ${ checkout . customerEmail } ` )
// Liberar acesso, enviar email de confirmacao, etc.
handlePixPaymentConfirmed ( checkout )
}
if ( checkout . status === 'expired' ) {
console . log ( `PIX expirado. Checkout: ${ checkout . id } ` )
handlePixPaymentExpired ( checkout )
}
break
}
case 'subscription.active' : {
const subscription = event . data
console . log ( `Assinatura ativa: ${ subscription . id } ` )
// Processar entrega do produto/servico
break
}
}
res . status ( 200 ). json ({ received: true })
})
async function handlePixPaymentConfirmed ( checkout : any ) {
// Exemplo: atualizar cobranca no banco de dados
// await db.sales.update({
// where: { checkoutId: checkout.id },
// data: { status: 'paid', paidAt: new Date() },
// })
// Exemplo: enviar email de confirmacao
// await sendEmail({
// to: checkout.customerEmail,
// subject: 'Pagamento PIX confirmado!',
// template: 'pix-confirmed',
// })
}
async function handlePixPaymentExpired ( checkout : any ) {
// Limpar recursos reservados, notificar cliente, etc.
}
Responda ao webhook com status 200 o mais rapido possivel . Processe logica pesada (emails, integracoes) de forma assincrona usando filas como BullMQ .
Abordagem 2: Polling
Se voce nao pode configurar webhooks, use polling para verificar o status periodicamente.
src/routes/checkout-status.ts
import { Router } from 'express'
import { chargefy } from '../lib/chargefy.js'
export const statusRouter = Router ()
// Endpoint para o frontend fazer polling
statusRouter . get ( '/sessions/:clientSecret/status' , async ( req , res ) => {
try {
const checkout = await chargefy . checkouts . getByClientSecret (
req . params . clientSecret
)
res . json ({
status: checkout . status ,
paymentMethod: checkout . paymentMethod ,
amount: checkout . amount ,
paidAt: checkout . paidAt ,
})
} catch ( error ) {
res . status ( 404 ). json ({ error: 'Checkout nao encontrado' })
}
})
async function pollPixStatus ( clientSecret : string ) : Promise < string > {
const MAX_ATTEMPTS = 200 // ~10 minutos com intervalo de 3s
const INTERVAL_MS = 3000
for ( let i = 0 ; i < MAX_ATTEMPTS ; i ++ ) {
const res = await fetch ( `/api/checkout/sessions/ ${ clientSecret } /status` )
const data = await res . json ()
if ( data . status === 'succeeded' ) {
return 'succeeded'
}
if ( data . status === 'expired' || data . status === 'failed' ) {
return data . status
}
await new Promise ( resolve => setTimeout ( resolve , INTERVAL_MS ))
}
return 'timeout'
}
O polling consome mais recursos do que webhooks. Em producao, sempre prefira webhooks como mecanismo principal de confirmacao e use polling apenas como fallback no frontend para atualizar a interface do usuario.
Passo 5: Tratar sucesso e falha
Pagamento confirmado (sucesso)
async function onPixSuccess ( checkout : {
id : string
customerEmail : string
amount : number
}) {
// 1. Registrar pagamento no seu banco de dados
await db . payments . create ({
data: {
checkoutId: checkout . id ,
email: checkout . customerEmail ,
amount: checkout . amount ,
method: 'pix' ,
status: 'paid' ,
paidAt: new Date (),
},
})
// 2. Liberar acesso ao produto/servico
await grantAccess ( checkout . customerEmail , checkout . id )
// 3. Enviar confirmacao por email
await sendConfirmationEmail ( checkout . customerEmail , {
checkoutId: checkout . id ,
amount: `R$ ${ ( checkout . amount / 100 ). toFixed ( 2 ) } ` ,
})
// 4. Emitir nota fiscal (se aplicavel)
await issueInvoice ( checkout . id )
}
PIX expirado
async function onPixExpired ( checkoutId : string ) {
// Atualizar status no banco
await db . payments . update ({
where: { checkoutId },
data: { status: 'expired' },
})
// Opcional: enviar email lembrando o cliente
// await sendReminderEmail(...)
}
Erros na criacao do PIX
async function createPixCheckout ( priceId : string , email : string ) {
try {
const checkout = await chargefy . checkouts . create ({
productPriceId: priceId ,
customerEmail: email ,
})
const result = await chargefy . checkouts . confirm ( checkout . clientSecret , {
customerEmail: email ,
customerName: 'Cliente' ,
paymentMethod: 'pix' ,
})
return { success: true , data: result }
} catch ( error : any ) {
// Tratar erros especificos
if ( error . status === 422 ) {
return {
success: false ,
error: 'Dados invalidos. Verifique o email e tente novamente.' ,
}
}
if ( error . status === 404 ) {
return {
success: false ,
error: 'Produto ou preco nao encontrado.' ,
}
}
if ( error . status === 429 ) {
return {
success: false ,
error: 'Muitas tentativas. Aguarde um momento e tente novamente.' ,
}
}
return {
success: false ,
error: 'Erro ao processar pagamento. Tente novamente.' ,
}
}
}
Exemplo completo
Backend (Express)
import 'dotenv/config'
import express from 'express'
import { Chargefy } from '@chargefy/sdk'
import crypto from 'crypto'
const app = express ()
const PORT = process . env . PORT || 3001
const chargefy = new Chargefy ({
accessToken: process . env . CHARGEFY_ACCESS_TOKEN ! ,
})
// ─── Webhook (ANTES do express.json) ────────────────────────────
app . post ( '/webhooks' , express . raw ({ type: 'application/json' }), ( req , res ) => {
const signature = req . headers [ 'webhook-signature' ] as string
const secret = process . env . CHARGEFY_WEBHOOK_SECRET !
if ( ! signature ) {
return res . status ( 401 ). json ({ error: 'Assinatura ausente' })
}
const expected = crypto . createHmac ( 'sha256' , secret ). update ( req . body ). digest ( 'hex' )
const isValid = crypto . timingSafeEqual ( Buffer . from ( signature ), Buffer . from ( expected ))
if ( ! isValid ) {
return res . status ( 401 ). json ({ error: 'Assinatura invalida' })
}
const event = JSON . parse ( req . body . toString ())
if ( event . type === 'checkout.updated' && event . data . status === 'succeeded' ) {
console . log ( `Pagamento PIX confirmado: ${ event . data . id } ` )
console . log ( `Valor: R$ ${ ( event . data . amount / 100 ). toFixed ( 2 ) } ` )
// TODO: liberar acesso, enviar email, etc.
}
res . status ( 200 ). json ({ received: true })
})
// ─── JSON parser ────────────────────────────────────────────────
app . use ( express . json ())
app . use ( express . static ( 'public' ))
// ─── Criar checkout PIX ─────────────────────────────────────────
app . post ( '/api/pix/create' , async ( req , res ) => {
try {
const { productPriceId , customerEmail , customerName } = req . body
// 1. Criar sessao de checkout
const checkout = await chargefy . checkouts . create ({
productPriceId ,
customerEmail ,
successUrl: ` ${ req . headers . origin } /sucesso` ,
})
// 2. Confirmar com PIX
const result = await chargefy . checkouts . confirm ( checkout . clientSecret , {
customerEmail ,
customerName ,
paymentMethod: 'pix' ,
})
// 3. Retornar dados do PIX para o frontend
res . json ({
clientSecret: checkout . clientSecret ,
pixQrCode: result . paymentDetails ?. pixQrCode ,
pixQrCodeBase64: result . paymentDetails ?. pixQrCodeBase64 ,
pixCopyPaste: result . paymentDetails ?. pixCopyPaste ,
expiresAt: result . paymentDetails ?. expiresAt ,
amount: checkout . amount ,
})
} catch ( error ) {
console . error ( 'Erro ao criar PIX:' , error )
res . status ( 400 ). json ({ error: 'Falha ao gerar pagamento PIX' })
}
})
// ─── Consultar status ───────────────────────────────────────────
app . get ( '/api/pix/status/:clientSecret' , async ( req , res ) => {
try {
const checkout = await chargefy . checkouts . getByClientSecret (
req . params . clientSecret
)
res . json ({
status: checkout . status ,
amount: checkout . amount ,
paidAt: checkout . paidAt ,
})
} catch ( error ) {
res . status ( 404 ). json ({ error: 'Checkout nao encontrado' })
}
})
app . listen ( PORT , () => {
console . log ( `Servidor PIX rodando em http://localhost: ${ PORT } ` )
})
Frontend (React)
import { useState } from 'react'
import { PixPayment } from './components/PixPayment'
interface PixResponse {
clientSecret : string
pixQrCode : string
pixQrCodeBase64 : string
pixCopyPaste : string
expiresAt : string
amount : number
}
export default function App () {
const [ pixData , setPixData ] = useState < PixResponse | null >( null )
const [ loading , setLoading ] = useState ( false )
const [ error , setError ] = useState < string | null >( null )
async function handleBuyWithPix () {
setLoading ( true )
setError ( null )
try {
const res = await fetch ( '/api/pix/create' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
productPriceId: 'price_xxxx' ,
customerEmail: '[email protected] ' ,
customerName: 'Maria Silva' ,
}),
})
if ( ! res . ok ) {
throw new Error ( 'Falha ao gerar PIX' )
}
const data : PixResponse = await res . json ()
setPixData ( data )
} catch ( err ) {
setError ( 'Erro ao gerar pagamento PIX. Tente novamente.' )
} finally {
setLoading ( false )
}
}
if ( pixData ) {
return (
< PixPayment
pixData = { pixData }
clientSecret = { pixData . clientSecret }
onPaymentConfirmed = { () => {
// Redirecionar para pagina de sucesso
window . location . href = '/sucesso'
} }
/>
)
}
return (
< div className = "max-w-md mx-auto p-8 text-center" >
< h1 className = "text-2xl font-bold mb-2" > Plano Pro </ h1 >
< p className = "text-3xl font-bold text-green-600 mb-6" > R$ 99,90 </ p >
{ error && (
< div className = "bg-red-50 text-red-600 p-3 rounded-lg mb-4 text-sm" >
{ error }
</ div >
) }
< button
onClick = { handleBuyWithPix }
disabled = { loading }
className = "w-full py-3 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 transition-colors"
>
{ loading ? 'Gerando PIX...' : 'Pagar com PIX' }
</ button >
</ div >
)
}
# 1. Criar pagamento PIX
curl -X POST http://localhost:3001/api/pix/create \
-H "Content-Type: application/json" \
-d '{
"productPriceId": "price_xxxx",
"customerEmail": "[email protected] ",
"customerName": "Maria Silva"
}'
# 2. Verificar status (substituir cs_xxx pelo clientSecret retornado)
curl http://localhost:3001/api/pix/status/cs_xxx
Testando no sandbox
O ambiente sandbox da Chargefy permite testar o fluxo completo de PIX sem movimentar dinheiro real.
Configurar modo sandbox
Use o token de sandbox e configure o cliente: const chargefy = new Chargefy ({
accessToken: process . env . CHARGEFY_ACCESS_TOKEN ! , // token de sandbox
server: 'sandbox' ,
})
Criar pagamento PIX normalmente
O fluxo e identico ao de producao. O QR Code gerado sera de teste.
Simular pagamento
No sandbox, use a API de simulacao para confirmar o pagamento: curl -X POST https://sandbox-api.chargefy.io/v1/test/pix/simulate-payment \
-H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN " \
-H "Content-Type: application/json" \
-d '{
"checkout_id": "checkout_abc123"
}'
Verificar webhook
O webhook checkout.updated sera disparado normalmente no sandbox.
No sandbox, o PIX e confirmado instantaneamente apos a simulacao. Em producao, a confirmacao depende do processamento do Banco Central, normalmente levando de 5 a 30 segundos.
Troubleshooting
QR Code nao aparece
Causa Solucao pixQrCodeBase64 esta vazioUse o campo pixQrCode (URL) como fallback na tag <img> Erro de CORS ao carregar imagem Use pixQrCodeBase64 em vez da URL, pois o base64 nao depende de CORS Checkout ja expirou Verifique expiresAt e crie uma nova sessao se necessario
Pagamento nao e confirmado
Causa Solucao Webhook nao configurado Configure o endpoint no dashboard da Chargefy Assinatura HMAC invalida Verifique se o CHARGEFY_WEBHOOK_SECRET esta correto Polling muito lento Reduza o intervalo para 2-3 segundos Timeout no polling Aumente MAX_ATTEMPTS ou implemente reconexao
Erros comuns da API
Codigo Erro Solucao 422Dados invalidos Verifique productPriceId e customerEmail 404Produto nao encontrado Confirme que o produto existe e esta ativo no dashboard 409Checkout ja confirmado Nao e possivel confirmar o mesmo checkout duas vezes 429Rate limit excedido Implemente retry com backoff exponencial 500Erro interno Tente novamente em alguns segundos. Se persistir, entre em contato com o suporte
PIX expirado antes do pagamento
// Detectar expiracao e oferecer novo QR Code
async function handleExpiredPix ( productPriceId : string , email : string ) {
const newCheckout = await chargefy . checkouts . create ({
productPriceId ,
customerEmail: email ,
})
const newResult = await chargefy . checkouts . confirm ( newCheckout . clientSecret , {
customerEmail: email ,
customerName: 'Cliente' ,
paymentMethod: 'pix' ,
})
return {
clientSecret: newCheckout . clientSecret ,
pixData: newResult . paymentDetails ,
}
}
Proximos passos
Webhooks Configure webhooks para receber notificacoes em tempo real.
Sandbox Teste pagamentos sem movimentar dinheiro real.
SDK TypeScript Referencia completa do SDK.
Guia Express Integracao completa com Express/Node.js.