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.

info
Conceito Chave: Pagamentos PIX são assíncronos. O usuário gera o QR Code, paga no banco, e o Pagar.me notifica seu backend via webhook. Nunca confie em confirmações do frontend!

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

  1. Acesse pagar.me e crie uma conta
  2. Acesse Dashboard → Configurações → API Keys
  3. Copie sua Secret Key (sk_test_... para testes)
  4. Configure o webhook URL: https://seusite.com/api/webhooks/pagarme
  5. 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
warning
Nunca comite as chaves no Git! Use .env e adicione ao .gitignore.

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

warning
Pré-requisito: Instale a biblioteca do Pagar.me via Composer: 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!)

error
SEGURANÇA: Webhooks são endpoints públicos. SEMPRE valide a assinatura do Pagar.me para garantir que a requisição é legítima e não um atacante tentando liberar cursos gratuitamente!
// 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:

  1. Abra o app do seu banco
  2. Escolha "Pagar com PIX"
  3. Escaneie o QR Code ou cole o código
  4. 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
rocket_launch
Próximo Passo: Agora que você domina PIX, vamos integrar Stripe para cartão de crédito, permitindo vendas internacionais e recorrentes (assinaturas)!