Visão geral do fluxo
O pagamento PIX na Chargefy segue este fluxo:1
Criar sessão de checkout
Seu backend cria uma sessão de checkout via API ou SDK.
2
Confirmar com PIX
O checkout é confirmado com
paymentMethod: 'pix', gerando um QR Code.3
Exibir QR Code
Seu frontend exibe o QR Code e o código copia e cola para o cliente.
4
Cliente paga
O cliente escaneia o QR Code ou cola o código no app do banco.
5
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
- Node.js 18+
- Uma conta Chargefy com Organization Access Token
- Pelo menos um produto criado no dashboard
- Webhook endpoint configurado para receber notificações (recomendado)
npm install @chargefy/sdk
src/lib/chargefy.ts
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: 'cliente@email.com',
successUrl: 'https://meusite.com.br/pagamento/sucesso',
})
console.log('Checkout criado:', 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": "cliente@email.com",
"success_url": "https://meusite.com.br/pagamento/sucesso"
}'
{
"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.Passo 2: Confirmar checkout com PIX
Com oclientSecret em mãos, confirme o checkout especificando PIX como método de pagamento.
const result = await chargefy.checkouts.confirm(checkout.clientSecret, {
customerEmail: 'cliente@email.com',
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
curl -X POST https://api.chargefy.io/v1/checkouts/cs_live_xxxxxxxxxxxx/confirm \
-H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customer_email": "cliente@email.com",
"customer_name": "Maria Silva",
"payment_method": "pix"
}'
{
"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' })
}
})
Polling no frontend
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)
src/index.ts
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)
src/App.tsx
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: 'cliente@email.com',
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>
)
}
Testar com curl
# 1. Criar pagamento PIX
curl -X POST http://localhost:3001/api/pix/create \
-H "Content-Type: application/json" \
-d '{
"productPriceId": "price_xxxx",
"customerEmail": "maria@email.com",
"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.1
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',
})
2
Criar pagamento PIX normalmente
O fluxo e identico ao de producao. O QR Code gerado sera de teste.
3
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"
}'
4
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 vazio | Use 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 |
|---|---|---|
422 | Dados invalidos | Verifique productPriceId e customerEmail |
404 | Produto nao encontrado | Confirme que o produto existe e esta ativo no dashboard |
409 | Checkout ja confirmado | Nao e possivel confirmar o mesmo checkout duas vezes |
429 | Rate limit excedido | Implemente retry com backoff exponencial |
500 | Erro 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.

