Skip to main content
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:
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

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: '[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.

Passo 2: Confirmar checkout com PIX

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' })
  }
})
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: '[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>
  )
}

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": "[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.
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

CausaSolucao
pixQrCodeBase64 esta vazioUse o campo pixQrCode (URL) como fallback na tag <img>
Erro de CORS ao carregar imagemUse pixQrCodeBase64 em vez da URL, pois o base64 nao depende de CORS
Checkout ja expirouVerifique expiresAt e crie uma nova sessao se necessario

Pagamento nao e confirmado

CausaSolucao
Webhook nao configuradoConfigure o endpoint no dashboard da Chargefy
Assinatura HMAC invalidaVerifique se o CHARGEFY_WEBHOOK_SECRET esta correto
Polling muito lentoReduza o intervalo para 2-3 segundos
Timeout no pollingAumente MAX_ATTEMPTS ou implemente reconexao

Erros comuns da API

CodigoErroSolucao
422Dados invalidosVerifique productPriceId e customerEmail
404Produto nao encontradoConfirme que o produto existe e esta ativo no dashboard
409Checkout ja confirmadoNao e possivel confirmar o mesmo checkout duas vezes
429Rate limit excedidoImplemente retry com backoff exponencial
500Erro internoTente 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.