pix 12. Integração com PIX (Pagar.me) - Pagamentos Instantâneos
PIX revolucionou os pagamentos no Brasil: instantâneo, gratuito e disponível 24/7. Para vender cursos, assinaturas ou qualquer produto digital, PIX é essencial. Nesta seção, você vai aprender a integrar o Pagar.me (gateway de pagamento brasileiro) para processar PIX com QR Code e receber webhooks quando o pagamento for confirmado.
A integração funciona assim: seu backend PHP gera um pedido (order), o Pagar.me retorna um QR Code PIX, o usuário paga pelo app do banco, e o Pagar.me envia um webhook notificando que o pagamento foi aprovado. Seu sistema então libera o acesso ao curso automaticamente.
Importante: Esta integração funciona em produção real. Você precisará de uma conta no Pagar.me (gratuita para testes) e configurar chaves de API. O fluxo completo está documentado aqui, desde a criação do pedido até a confirmação automática via webhook.
timeline Fluxo Completo de Pagamento PIX
┌─────────────────────────────────────────────────────────────┐
│ FLUXO DE PAGAMENTO PIX (PAGAR.ME) │
└─────────────────────────────────────────────────────────────┘
1. USUÁRIO SELECIONA CURSO
React Frontend → Exibe curso com botão "Comprar com PIX"
2. CRIAR PEDIDO
React → POST /api/orders/create
{
courseId: 123,
userId: 456,
paymentMethod: "pix"
}
3. BACKEND GERA PIX NO PAGAR.ME
PHP Backend → Pagar.me API
POST https://api.pagar.me/core/v5/orders
{
customer: { name, email, document },
items: [{ amount: 49900, description: "Curso X" }],
payments: [{ pix: { expires_in: 3600 } }]
}
4. PAGAR.ME RETORNA QR CODE
Response:
{
id: "order_abc123",
charges: [{
last_transaction: {
qr_code: "00020126580014br.gov.bcb.pix...",
qr_code_url: "https://pagar.me/qr/abc123.png"
}
}]
}
5. EXIBIR QR CODE PARA USUÁRIO
React → Mostra QR Code + botão "Copiar código PIX"
Frontend fica aguardando confirmação (polling ou WebSocket)
6. USUÁRIO PAGA NO BANCO
Cliente abre app do banco → Escaneia QR Code → Confirma pagamento
(Pagamento é instantâneo, leva 1-5 segundos)
7. PAGAR.ME ENVIA WEBHOOK
Pagar.me → POST /api/webhooks/pagarme
{
type: "charge.paid",
data: {
id: "charge_xyz",
order_id: "order_abc123",
status: "paid"
}
}
8. BACKEND PROCESSA WEBHOOK
PHP → Valida assinatura do webhook
PHP → Atualiza order.status = "paid"
PHP → Libera acesso ao curso
PHP → Envia email de confirmação
9. FRONTEND RECEBE CONFIRMAÇÃO
React → Polling detecta mudança de status
React → Redireciona para página de sucesso
React → Usuário pode acessar o curso imediatamente
settings Configuração Inicial - Pagar.me
key 1. Criar Conta Pagar.me
- Acesse pagar.me e crie uma conta
- Acesse Dashboard → Configurações → API Keys
- Copie sua Secret Key (sk_test_... para testes)
- Configure o webhook URL:
https://seusite.com/api/webhooks/pagarme - Salve o Webhook Secret para validação
lock 2. Variáveis de Ambiente
# .env PAGARME_API_KEY=sk_test_XXXXXXXXXXXXX PAGARME_WEBHOOK_SECRET=whsec_YYYYYYYYYYYYYY PAGARME_API_URL=https://api.pagar.me/core/v5
storage Estrutura de Banco de Dados
-- Tabela de Pedidos (Orders)
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
course_id INT NOT NULL,
amount INT NOT NULL COMMENT 'Valor em centavos (ex: 49900 = R$ 499,00)',
currency VARCHAR(3) DEFAULT 'BRL',
status ENUM('pending', 'paid', 'failed', 'cancelled', 'refunded') DEFAULT 'pending',
payment_method VARCHAR(20) NOT NULL COMMENT 'pix, credit_card, boleto',
-- Dados do Pagar.me
pagarme_order_id VARCHAR(100) UNIQUE,
pagarme_charge_id VARCHAR(100),
-- Dados PIX
pix_qr_code TEXT,
pix_qr_code_url TEXT,
pix_expires_at DATETIME,
-- Metadados
metadata JSON COMMENT 'Dados adicionais do pedido',
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
paid_at TIMESTAMP NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_pagarme_order_id (pagarme_order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Tabela de Webhooks (Log de notificações)
CREATE TABLE webhooks_log (
id INT AUTO_INCREMENT PRIMARY KEY,
source VARCHAR(50) NOT NULL COMMENT 'pagarme, stripe, etc',
event_type VARCHAR(100) NOT NULL,
payload JSON NOT NULL,
processed BOOLEAN DEFAULT FALSE,
processed_at TIMESTAMP NULL,
error_message TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_source_type (source, event_type),
INDEX idx_processed (processed)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
code Backend PHP - Criar Pedido PIX
composer require pagarme/pagarme-php
// api/orders/create.php
'Não autenticado']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Método não permitido']);
exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$userId = $_SESSION['user_id'];
$courseId = $data['courseId'] ?? null;
if (!$courseId) {
http_response_code(400);
echo json_encode(['error' => 'courseId é obrigatório']);
exit;
}
try {
$db = getDBConnection();
// 1. Buscar dados do curso
$stmt = $db->prepare("SELECT id, title, price FROM courses WHERE id = ?");
$stmt->execute([$courseId]);
$course = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$course) {
http_response_code(404);
echo json_encode(['error' => 'Curso não encontrado']);
exit;
}
// 2. Buscar dados do usuário
$stmt = $db->prepare("SELECT id, name, email, cpf FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 3. Criar pedido no banco de dados local
$stmt = $db->prepare("
INSERT INTO orders (user_id, course_id, amount, payment_method, status)
VALUES (?, ?, ?, 'pix', 'pending')
");
$stmt->execute([$userId, $courseId, $course['price']]);
$orderId = $db->lastInsertId();
// 4. Configurar cliente Pagar.me
$pagarme = new Client($_ENV['PAGARME_API_KEY']);
// 5. Criar pedido no Pagar.me
$orderData = [
'customer' => [
'name' => $user['name'],
'email' => $user['email'],
'document' => preg_replace('/[^0-9]/', '', $user['cpf']), // Apenas números
'type' => 'individual',
'document_type' => 'CPF'
],
'items' => [
[
'amount' => (int) $course['price'], // Centavos
'description' => $course['title'],
'quantity' => 1,
'code' => "course_{$courseId}"
]
],
'payments' => [
[
'payment_method' => 'pix',
'pix' => [
'expires_in' => 3600 // 1 hora
]
]
],
'metadata' => [
'order_id' => $orderId,
'user_id' => $userId,
'course_id' => $courseId
]
];
$pagarmeOrder = $pagarme->orders()->create($orderData);
// 6. Extrair dados do PIX
$charge = $pagarmeOrder->charges[0];
$pixData = $charge->last_transaction;
// 7. Atualizar pedido com dados do Pagar.me
$stmt = $db->prepare("
UPDATE orders SET
pagarme_order_id = ?,
pagarme_charge_id = ?,
pix_qr_code = ?,
pix_qr_code_url = ?,
pix_expires_at = ?
WHERE id = ?
");
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour'));
$stmt->execute([
$pagarmeOrder->id,
$charge->id,
$pixData->qr_code,
$pixData->qr_code_url,
$expiresAt,
$orderId
]);
// 8. Retornar dados para o frontend
echo json_encode([
'success' => true,
'order' => [
'id' => $orderId,
'amount' => $course['price'],
'currency' => 'BRL',
'status' => 'pending',
'pix' => [
'qr_code' => $pixData->qr_code,
'qr_code_url' => $pixData->qr_code_url,
'expires_at' => $expiresAt
]
]
]);
} catch (Exception $e) {
error_log("Erro ao criar pedido PIX: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'error' => 'Erro ao processar pagamento',
'message' => $e->getMessage()
]);
}
webhook Backend PHP - Webhook Handler (CRÍTICO!)
// api/webhooks/pagarme.php
'Assinatura inválida']);
exit;
}
$event = json_decode($payload, true);
$db = getDBConnection();
// 3. Registrar webhook no log
$stmt = $db->prepare("
INSERT INTO webhooks_log (source, event_type, payload)
VALUES ('pagarme', ?, ?)
");
$stmt->execute([$event['type'], $payload]);
$webhookLogId = $db->lastInsertId();
// 4. Processar evento
switch ($event['type']) {
case 'charge.paid':
handleChargePaid($db, $event, $webhookLogId);
break;
case 'charge.failed':
handleChargeFailed($db, $event, $webhookLogId);
break;
case 'charge.refunded':
handleChargeRefunded($db, $event, $webhookLogId);
break;
default:
error_log("Evento não tratado: {$event['type']}");
}
// 5. Marcar webhook como processado
$stmt = $db->prepare("
UPDATE webhooks_log SET processed = TRUE, processed_at = NOW()
WHERE id = ?
");
$stmt->execute([$webhookLogId]);
http_response_code(200);
echo json_encode(['success' => true]);
} catch (Exception $e) {
error_log("Erro ao processar webhook: " . $e->getMessage());
// Registrar erro no log
if (isset($webhookLogId)) {
$stmt = $db->prepare("
UPDATE webhooks_log SET error_message = ?
WHERE id = ?
");
$stmt->execute([$e->getMessage(), $webhookLogId]);
}
http_response_code(500);
echo json_encode(['error' => 'Erro ao processar webhook']);
}
/**
* Handler: Pagamento Aprovado
*/
function handleChargePaid($db, $event, $webhookLogId) {
$chargeId = $event['data']['id'];
$orderId = $event['data']['order']['id']; // ID do Pagar.me
// Buscar pedido no banco
$stmt = $db->prepare("SELECT * FROM orders WHERE pagarme_order_id = ?");
$stmt->execute([$orderId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
throw new Exception("Pedido não encontrado: {$orderId}");
}
// Evitar processamento duplicado
if ($order['status'] === 'paid') {
error_log("Pedido já foi pago: {$order['id']}");
return;
}
// Iniciar transação
$db->beginTransaction();
try {
// Atualizar status do pedido
$stmt = $db->prepare("
UPDATE orders SET
status = 'paid',
paid_at = NOW()
WHERE id = ?
");
$stmt->execute([$order['id']]);
// Liberar acesso ao curso
$stmt = $db->prepare("
INSERT INTO user_courses (user_id, course_id, purchased_at)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE purchased_at = NOW()
");
$stmt->execute([$order['user_id'], $order['course_id']]);
// Registrar log de auditoria
$stmt = $db->prepare("
INSERT INTO audit_log (user_id, action, details)
VALUES (?, 'course_purchased', ?)
");
$stmt->execute([
$order['user_id'],
json_encode([
'order_id' => $order['id'],
'course_id' => $order['course_id'],
'amount' => $order['amount'],
'webhook_log_id' => $webhookLogId
])
]);
$db->commit();
// Enviar email de confirmação (fora da transação)
sendPurchaseConfirmationEmail($order['user_id'], $order['course_id']);
error_log("Pagamento processado com sucesso: Order #{$order['id']}");
} catch (Exception $e) {
$db->rollBack();
throw $e;
}
}
/**
* Handler: Pagamento Falhou
*/
function handleChargeFailed($db, $event, $webhookLogId) {
$orderId = $event['data']['order']['id'];
$stmt = $db->prepare("
UPDATE orders SET status = 'failed'
WHERE pagarme_order_id = ?
");
$stmt->execute([$orderId]);
error_log("Pagamento falhou: {$orderId}");
}
/**
* Handler: Pagamento Reembolsado
*/
function handleChargeRefunded($db, $event, $webhookLogId) {
$orderId = $event['data']['order']['id'];
$stmt = $db->prepare("SELECT * FROM orders WHERE pagarme_order_id = ?");
$stmt->execute([$orderId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if ($order) {
$db->beginTransaction();
try {
// Atualizar status
$stmt = $db->prepare("UPDATE orders SET status = 'refunded' WHERE id = ?");
$stmt->execute([$order['id']]);
// Remover acesso ao curso
$stmt = $db->prepare("
DELETE FROM user_courses
WHERE user_id = ? AND course_id = ?
");
$stmt->execute([$order['user_id'], $order['course_id']]);
$db->commit();
error_log("Reembolso processado: Order #{$order['id']}");
} catch (Exception $e) {
$db->rollBack();
throw $e;
}
}
}
/**
* Enviar email de confirmação
*/
function sendPurchaseConfirmationEmail($userId, $courseId) {
// Implementar envio de email
// Pode usar PHPMailer, SendGrid, etc.
error_log("TODO: Enviar email de confirmação para user_id={$userId}, course_id={$courseId}");
}
code Frontend React - Componente de Pagamento PIX
// components/PixPayment.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';
import QRCode from 'react-qr-code';
const API_URL = import.meta.env.VITE_API_URL;
export const PixPayment = ({ courseId, courseName, coursePrice }) => {
const [loading, setLoading] = useState(false);
const [pixData, setPixData] = useState(null);
const [orderId, setOrderId] = useState(null);
const [status, setStatus] = useState('idle'); // idle, pending, paid, failed
const [timeLeft, setTimeLeft] = useState(0);
// Criar pedido PIX
const createPixOrder = async () => {
setLoading(true);
try {
const response = await axios.post(
`${API_URL}/orders/create`,
{ courseId },
{ withCredentials: true }
);
setOrderId(response.data.order.id);
setPixData(response.data.order.pix);
setStatus('pending');
// Calcular tempo restante
const expiresAt = new Date(response.data.order.pix.expires_at);
const now = new Date();
const diff = Math.floor((expiresAt - now) / 1000);
setTimeLeft(diff);
// Iniciar polling para verificar pagamento
startPolling(response.data.order.id);
} catch (error) {
console.error('Erro ao criar pedido:', error);
alert('Erro ao gerar PIX. Tente novamente.');
setStatus('failed');
} finally {
setLoading(false);
}
};
// Polling para verificar status do pagamento
const startPolling = (orderId) => {
const interval = setInterval(async () => {
try {
const response = await axios.get(
`${API_URL}/orders/${orderId}/status`,
{ withCredentials: true }
);
if (response.data.status === 'paid') {
setStatus('paid');
clearInterval(interval);
// Redirecionar após 2 segundos
setTimeout(() => {
window.location.href = `/courses/${courseId}`;
}, 2000);
}
} catch (error) {
console.error('Erro ao verificar status:', error);
}
}, 3000); // Verificar a cada 3 segundos
// Limpar interval após 1 hora
setTimeout(() => clearInterval(interval), 3600000);
};
// Copiar código PIX
const copyPixCode = () => {
navigator.clipboard.writeText(pixData.qr_code);
alert('Código PIX copiado!');
};
// Countdown timer
useEffect(() => {
if (timeLeft > 0) {
const timer = setTimeout(() => {
setTimeLeft(timeLeft - 1);
}, 1000);
return () => clearTimeout(timer);
} else if (timeLeft === 0 && status === 'pending') {
setStatus('expired');
}
}, [timeLeft, status]);
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const formatPrice = (cents) => {
return (cents / 100).toLocaleString('pt-BR', {
style: 'currency',
currency: 'BRL'
});
};
// Render: Estado Inicial
if (status === 'idle') {
return (
{courseName}
{formatPrice(coursePrice)}
);
}
// Render: PIX Gerado (Aguardando Pagamento)
if (status === 'pending') {
return (
Pague com PIX
Escaneie o QR Code ou copie o código
schedule
Expira em: {formatTime(timeLeft)}
Como pagar:
- Abra o app do seu banco
- Escolha "Pagar com PIX"
- Escaneie o QR Code ou cole o código
- Confirme o pagamento
Aguardando pagamento...
);
}
// Render: Pagamento Confirmado
if (status === 'paid') {
return (
check_circle
Pagamento Confirmado!
Seu acesso ao curso foi liberado.
Redirecionando...
);
}
// Render: Expirado
if (status === 'expired') {
return (
schedule
PIX Expirado
O tempo limite para pagamento foi atingido.
);
}
// Render: Erro
return (
error
Erro ao Processar Pagamento
);
};
api Endpoint: Verificar Status do Pedido
// api/orders/[id]/status.php
'Não autenticado']);
exit;
}
$orderId = $_GET['id'] ?? null;
if (!$orderId) {
http_response_code(400);
echo json_encode(['error' => 'ID do pedido é obrigatório']);
exit;
}
try {
$db = getDBConnection();
// Buscar pedido (apenas do usuário logado)
$stmt = $db->prepare("
SELECT id, status, amount, paid_at
FROM orders
WHERE id = ? AND user_id = ?
");
$stmt->execute([$orderId, $_SESSION['user_id']]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
http_response_code(404);
echo json_encode(['error' => 'Pedido não encontrado']);
exit;
}
echo json_encode([
'id' => $order['id'],
'status' => $order['status'],
'amount' => $order['amount'],
'paid_at' => $order['paid_at']
]);
} catch (Exception $e) {
error_log("Erro ao buscar status: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Erro ao buscar status']);
}
check_circle Checklist de Implementação
✅ Backend
- Criar conta no Pagar.me e obter API keys
- Configurar variáveis de ambiente (.env)
- Criar tabelas orders e webhooks_log
- Implementar endpoint POST /orders/create
- Implementar endpoint POST /webhooks/pagarme
- Implementar endpoint GET /orders/:id/status
- Validar assinatura do webhook
- Testar em ambiente de sandbox
✅ Frontend
- Instalar react-qr-code:
npm install react-qr-code - Criar componente PixPayment
- Implementar polling para verificar status
- Adicionar countdown timer (expiração)
- Adicionar botão "Copiar código PIX"
- Exibir feedback visual (loading, success, error)
- Redirecionar após pagamento confirmado
- Testar fluxo completo