Search by

phpay-io / phpay

mariolucasdev

Biblioteca PHP para integração simples e padronizada com gateways de pagamento brasileiros.

Package info

github.com/phpay-io/phpay

pkg:composer/phpay-io/phpay

Fund package maintenance!

mariolucasdev

Statistics

Installs: 1 577

Dependents: 0

Suggesters: 0

Stars: 63

Open Issues: 4

v2.2.0 2026-09-21 15:39 UTC

README

capa-redes

Versão estável Versão do PHP Downloads Testes Licença

Uma interface só para os gateways de pagamento brasileiros.

Sumário

Por que o PHPay

Cada gateway brasileiro resolve os mesmos problemas de um jeito diferente: um chama de payment, outro de order, outro de charge. Um quer reais, outro quer centavos. Um separa ambiente por URL, outro pelo prefixo do token.

O PHPay normaliza isso numa interface só, sem esconder o que é genuinamente diferente. Quando um gateway não oferece um recurso, ele não finge que oferece — ele declara o que sabe fazer, e você descobre em tempo de análise estática, não em produção.

use PHPay\Asaas\AsaasGateway;
use PHPay\PHPay;

$phpay = PHPay::gateway(new AsaasGateway(TOKEN));

$phpay->charge()->setCharge($cobranca)->setCustomerId($clienteId)->create();

Trocar de gateway é trocar a linha do construtor.

Gateways suportados

Gateway Clientes Cobranças Assinaturas Webhooks Chaves Pix
Asaas
Woovi/OpenPix
Efí
Mercado Pago
PagBank
Pagar.me
AbacatePay
Cielo
Rede

As interfaces correspondentes são SupportsCustomers, SupportsCharges, SupportsSubscriptions, SupportsWebhooks e SupportsPixKeys.

Duas colunas merecem explicação, porque a ausência de ✅ não quer dizer que o gateway não aceita Pix ou não manda webhook:

  • SupportsPixKeys significa gerenciar chaves Pix e QR Code estático, o que só um PSP que emite chave própria oferece. Nos outros gateways, Pix é forma de pagamento de uma cobrança — e todos aceitam.
  • SupportsWebhooks significa cadastrar endpoints pela API. Nos outros, o cadastro é no painel; a notificação vai por cobrança, no campo notification_url. O Pagar.me ainda deixa consultar e reenviar entregas, através de webhookDeliveries().

Requisitos

Para usar a biblioteca PHP ^8.1, ext-curl, ext-json
Para desenvolver o PHPay PHP ^8.2 (Pest 3 e Termwind 2 exigem)

A compatibilidade com PHP 8.1 é verificada estaticamente pelo PHPStan a cada build, com phpVersion mínimo configurado.

Instalação

composer require phpay-io/phpay

Início rápido

Uma cobrança Pix no Asaas, do zero:

use PHPay\Asaas\AsaasGateway;
use PHPay\Exceptions\PHPayException;
use PHPay\PHPay;

$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX));

try {
    $cobranca = $phpay->charge()
        ->setCharge([
            'billingType' => 'PIX',
            'value'       => 100.50,
            'dueDate'     => date('Y-m-d', strtotime('+3 days')),
            'description' => 'Assinatura PHPay',
        ])
        ->setCustomer([
            'name'    => 'Mário Lucas',
            'cpfCnpj' => '12345678901',
        ])
        ->create();

    print_r($phpay->charge()->getQrCodePix($cobranca['id']));
} catch (PHPayException $e) {
    echo $e->getMessage();
}

Um array devolvido é sempre uma resposta de sucesso. Qualquer falha vira exceção — veja Tratamento de erros.

Conceitos

Três coisas valem entender uma vez; depois todo gateway se comporta igual.

Capacidades

GatewayInterface carrega só a identidade do gateway. Cada recurso é uma interface que o gateway implementa se, e só se, oferecer:

use PHPay\Contracts\Capability;

$phpay = PHPay::gateway(new RedeGateway(REDE_PV, REDE_TOKEN));

$phpay->name();                                 // 'Rede'
$phpay->supports(Capability::SUBSCRIPTIONS);    // false
$phpay->capabilities();                          // [Capability::CHARGES]

Chamar um recurso que o gateway não oferece lança uma exceção que diz o que ele oferece:

$phpay->pix();
// NotImplementedException: Rede não suporta chaves Pix.
//                          Capacidades disponíveis: cobranças.

Se você segurar o gateway concreto em vez da facade, o erro sobe para tempo de análise — o PHPStan acusa que o método não existe naquele tipo:

$rede = new RedeGateway(REDE_PV, REDE_TOKEN);

$rede->charge();   // ✅
$rede->pix();      // ❌ o método não existe nesse gateway

Para injeção de dependência, tipe a capacidade em vez do gateway:

use PHPay\Contracts\SupportsCharges;

public function __construct(private SupportsCharges $gateway) {}

Tratamento de erros

Toda exceção da biblioteca implementa PHPay\Exceptions\PHPayException, então um catch cobre a integração inteira:

Exceção Estende Quando acontece
ValidationException InvalidArgumentException Payload inválido, antes de qualquer HTTP
ApiException RuntimeException O gateway recusou, ou está inacessível
NotImplementedException BadMethodCallException O gateway não oferece o recurso
use PHPay\Exceptions\ApiException;
use PHPay\Exceptions\PHPayException;
use PHPay\Exceptions\ValidationException;

try {
    $cobranca = $phpay->charge()->setCharge($dados)->create();
} catch (ValidationException $e) {
    // payload inválido: nenhuma requisição foi feita
} catch (ApiException $e) {
    $e->getStatusCode();       // 400, 401, 404… ou 0 se nem chegou ao gateway
    $e->getResponse();         // corpo devolvido pelo gateway
    $e->getGateway();          // 'Asaas', 'Pagar.me', …
    $e->isConnectionError();   // true em timeout, DNS, TLS — vale retry
} catch (PHPayException $e) {
    // qualquer outra falha do PHPay
}

ApiException já resume os formatos de erro de cada gateway, então getMessage() traz a descrição legível, não um dump.

Ambientes e credenciais

Os gateways discordam sobre como separar teste de produção, e o PHPay segue o que cada um faz em vez de inventar um padrão:

Gateway Como o ambiente é decidido
Asaas $sandbox no construtor — troca a URL
PagBank $sandbox no construtor — troca a URL
Cielo $sandbox no construtor — troca as duas URLs
Efí $sandbox no construtor — troca as duas URLs (Cobranças e Pix)
Rede $sandbox no construtor — troca as duas URLs e o caminho do token
Mercado Pago Prefixo do token (TEST-); host único, sem $sandbox
Pagar.me Prefixo da chave (sk_test_); host único, sem $sandbox
Woovi $sandbox no construtor — o sandbox tem domínio próprio
AbacatePay Pela chave usada; host único, sem prefixo — a cobrança informa em devMode

Nos dois últimos, isSandbox() diz em qual ambiente você está:

(new MercadoPagoGateway($token))->isSandbox();
(new PagarMeGateway($secretKey))->isSandbox();

Nunca versione credenciais. Os arquivos examples/*/credentials.php são ignorados pelo git por padrão.

Cliente

O mesmo campo tem seis grafias entre os gateways: cpfCnpj no Asaas, tax_id no PagBank, document no Pagar.me, taxId no AbacatePay, taxID no Woovi, cpf_cnpj no Efí. Código escrito para um não migra para outro, e nada no tipo avisa.

Customer é a forma única. Cada gateway mapeia para o formato dele:

use PHPay\Support\Customer;

$cliente = Customer::make(
    name: 'Mário Lucas',
    document: '123.456.789-01',     // pontuação é limpa
    email: 'fale@phpay.io',
    phone: '(11) 94002-8922',
);

$phpay->charge()->setCustomer($cliente);   // funciona nos nove

Ele também carrega o que cada gateway deriva do cliente, e que antes ficava espalhado:

$cliente->isIndividual();   // CPF: o Pagar.me precisa como type: 'individual'
$cliente->documentType();   // 'CPF' | 'CNPJ': a Cielo quer em IdentityType
$cliente->firstName();      // o Mercado Pago quer nome e sobrenome separados
$cliente->phoneParts();     // ['country' => '55', 'area' => '11', ...] para o PagBank

Para um cliente que já existe no gateway, ou para campos que só aquele gateway tem:

$cliente->withId('cus_000006337812');            // reaproveita em vez de criar
$cliente->withExtra(['externalReference' => 'x']); // vai junto no payload

O withExtra() existe para o que é específico de um gateway — endereço, data de nascimento, referência externa. Nenhum campo obrigatório de nenhum dos nove gateways precisa dele: o value object cobre todos.

Unidade monetária

Os gateways discordam sobre a unidade, e errar não quebra a integração — ela cobra o valor errado. Mandar 100.50 num gateway de centavos cobra R$ 1,00, e você só descobre na conciliação.

Use Money e o problema deixa de existir: você diz a unidade que tem, o gateway pede a unidade que precisa, e nenhum dos dois pode errar.

use PHPay\Support\Money;

$valor = Money::reais(100.50);     // ou Money::centavos(10050)

$asaas->charge()->setAmount($valor);          // vira 100.50
$pagbank->charge()->addItem('Item', $valor);  // vira 10050

Também aceita string, que é o que um campo de formulário costuma entregar:

Money::reais('100,50');      // notação brasileira
Money::reais('1.234,56');    // com separador de milhar
Money::reais('100.50');      // notação com ponto

E tem o que um total precisa:

$unitario = Money::reais(59.90);

$unitario->multiply(2);              // R$ 119,80
$unitario->plus(Money::reais(10));   // R$ 69,90
$unitario->format();                 // 'R$ 59,90'

Money::reais(10 / 3) lança exceção em vez de arredondar. Arredondamento silencioso é como nascem erros de um centavo na conciliação — arredonde você mesmo, ou use Money::centavos() para ser exato.

Passando número cru

Continua funcionando, e cada gateway lê na unidade que sempre esperou:

Gateway Unidade do número cru R$ 100,50
Asaas, Mercado Pago Reais (decimal) 100.50
PagBank, Pagar.me, Cielo, Rede, AbacatePay, Woovi, Efí Centavos (inteiro) 10050

Nos que usam centavos, o PHPay recusa decimal na validação. Mas é justamente essa tabela que o Money torna desnecessária — prefira o value object.

A exceção é a API Pix do Efí, que não aceita número cru, só Money. Ela quer reais ("100.50") enquanto a API de Cobranças do mesmo gateway quer centavos, e um número solto ali seria a ambiguidade que o value object existe para eliminar.

Gateways

Cada seção cobre só o que é específico daquele gateway. Tudo que vale para todos está em Conceitos.

Asaas

Um dos dois com as cinco capacidades, ao lado do Woovi. É PSP, então emite chave Pix própria e gerencia webhooks por API.

use PHPay\Asaas\AsaasGateway;

$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX));

Cobranças

$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->charge();

/* cria a cobrança, criando também o cliente */
$phpay->setCharge($cobranca)->setCustomer($cliente)->create();

/* reaproveita um cliente que já existe — evita cadastro duplicado */
$phpay->setCharge($cobranca)->setCustomerId('cus_000006337812')->create();

$phpay->find($id);
$phpay->getAll();
$phpay->setQueryParams(['limit' => 2])->getAll();
$phpay->update($id, $dados);
$phpay->destroy($id);
$phpay->restore($id);

$phpay->getStatus($id);
$phpay->getDigitableLine($id);
$phpay->getQrCodePix($id);

$phpay->confirmReceipt($id, [
    'paymentDate'    => date('Y-m-d'),
    'value'          => 100.00,
    'notifyCustomer' => true,
]);
$phpay->undoConfirmReceipt($id);

Clientes

$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX));

$cliente = $phpay->customer(['name' => 'Mário Lucas', 'cpfCnpj' => '12345678901'])->create();

$phpay->customer()->find($cliente['id']);
$phpay->customer()->setFilter(['cpfCnpj' => '12345678901'])->getAll();
$phpay->customer(['name' => 'Novo Nome'])->update($cliente['id']);
$phpay->customer()->getNotifications($cliente['id']);
$phpay->customer()->destroy($cliente['id']);
$phpay->customer()->restore($cliente['id']);

Assinaturas

A assinatura gera uma cobrança por ciclo, e cada uma é uma cobrança comum: aparece em getPayments() e é tratada pelo recurso de cobrança.

use PHPay\Asaas\Enums\SubscriptionCycleEnum;

$assinaturas = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->subscription();

$assinatura = $assinaturas
    ->setCustomer($cliente)                          // ou setCustomerId('cus_...')
    ->setAmount(Money::reais('49,90'))
    ->setCycle(SubscriptionCycleEnum::MONTHLY)
    ->setSubscription([
        'billingType' => 'BOLETO',
        'nextDueDate' => '2026-10-10',
        'description' => 'Plano mensal',
    ])
    ->create();

O create([...]) com o payload inteiro continua funcionando, e o array sobrescreve o que os setters montaram. O cycle é obrigatório: sem ele o Asaas recusa, e o PHPay barra antes.

Consulta e ciclo de vida:

$assinaturas->setQueryParams(['customer' => 'cus_...', 'status' => 'ACTIVE'])->getAll();
$assinaturas->find($id);

/* muda as próximas cobranças; com updatePendingPayments, as pendentes também */
$assinaturas->update($id, ['description' => 'Plano anual', 'updatePendingPayments' => true]);

/* pausa: para de gerar cobranças e mantém as que existem */
$assinaturas->deactivate($id);
$assinaturas->reactivate($id, '2026-11-10');   // o Asaas exige um novo vencimento

/* remove: as cobranças pendentes e vencidas vão junto; as pagas ficam */
$assinaturas->destroy($id);

Cobranças, carnê e cartão:

$assinaturas->getPayments($id, ['status' => 'PENDING']);

/* o carnê vem como os bytes do PDF */
file_put_contents('carne.pdf', $assinaturas->paymentBook($id, month: 12, year: 2026));

/* troca o cartão sem cobrar — as cobranças pendentes passam para o novo */
$assinaturas->updateCreditCard($id, [
    'creditCardToken' => $token,        // ou creditCard + creditCardHolderInfo
    'remoteIp'        => $ipDoComprador,
]);

Nota fiscal emitida automaticamente para cada cobrança:

$assinaturas->createInvoiceSettings($id, [
    'municipalServiceName' => 'Desenvolvimento de software',
    'effectiveDatePeriod'  => 'ON_PAYMENT_CONFIRMATION',
    'taxes'                => [   // os sete são obrigatórios; 0 quando não houver
        'retainIss' => false,
        'iss'       => 2,
        'pis'       => 0.65,
        'cofins'    => 3,
        'csll'      => 0,
        'inss'      => 0,
        'ir'        => 0,
    ],
]);

$assinaturas->getInvoiceSettings($id);
$assinaturas->updateInvoiceSettings($id, [...]);
$assinaturas->destroyInvoiceSettings($id);
$assinaturas->getInvoices($id);   // as notas já emitidas

Webhooks e chaves Pix

$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX));

/* webhooks com CRUD completo — exclusividade do Asaas */
$phpay->webhook(WEBHOOK)->create();
$phpay->webhook()->getAll();
$phpay->webhook()->update($id, $dados);
$phpay->webhook()->destroy($id);

/* chaves Pix e QR Code estático */
$chave = $phpay->pix()->createKey();
$phpay->pix()->getAll();
$phpay->pix()->staticQrCode(['addressKey' => $chave['key'], 'value' => 25.00]);
$phpay->pix()->destroy($chave['id']);

Mercado Pago

Ambiente pelo prefixo do token. POST /v1/payments exige o header X-Idempotency-Key: o PHPay gera uma chave por chamada, e setIdempotencyKey() deixa você fixar a sua — assim um retry da mesma operação de negócio não cria duas cobranças.

use PHPay\MercadoPago\Enums\PaymentMethodEnum;
use PHPay\MercadoPago\MercadoPagoGateway;

$gateway = new MercadoPagoGateway(ACCESS_TOKEN);

$cobranca = PHPay::gateway($gateway)->charge()
    ->setCharge([
        'transaction_amount' => 100.50,
        'payment_method_id'  => PaymentMethodEnum::PIX->value,
        'notification_url'   => 'https://exemplo.test/webhook/mercadopago',
    ])
    ->setPayer(['email' => 'comprador@exemplo.test'])
    ->setIdempotencyKey('pedido-123456')
    ->create();

$phpay->getPixCode($cobranca['id']);

Assinaturas usam /preapproval, com ou sem plano associado:

$phpay = PHPay::gateway($gateway)->subscription();

$phpay->setPayerEmail('comprador@exemplo.test')->create([
    'reason'         => 'Assinatura PHPay',
    'back_url'       => 'https://exemplo.test/retorno',
    'auto_recurring' => [
        'frequency'          => 1,
        'frequency_type'     => 'months',
        'transaction_amount' => 100.50,
        'currency_id'        => 'BRL',
    ],
]);

/* com plano, a recorrência vem do plano */
$phpay->setPayerEmail('comprador@exemplo.test')
    ->setPlan('2c938084726fca480172750000000000')
    ->create(['back_url' => 'https://exemplo.test/retorno']);

O recurso Customer do Mercado Pago existe para cartões salvos — não é pré-requisito para cobrar, já que o pagamento carrega payer.email direto. A API também não oferece exclusão de cliente.

PagBank

Duas particularidades, ambas resolvidas pela biblioteca.

Duas APIs em hosts diferentes. Pedidos vivem em api.pagseguro.com, assinaturas em api.assinaturas.pagseguro.com. Cada recurso boota o client da API certa — você não precisa saber disso.

Pix não é uma cobrança. Entra como qr_codes do pedido, e só um por pedido. A conta precisa ter uma chave Pix ativa.

use PHPay\PagBank\PagBankGateway;

$phpay = PHPay::gateway(new PagBankGateway(TOKEN_PAGBANK_SANDBOX))->charge();

$pedido = $phpay
    ->setCustomer(['name' => 'Mário', 'email' => 'fale@phpay.io', 'tax_id' => '12345678901'])
    ->addItem('Assinatura PHPay', 10050)   // R$ 100,50
    ->setQrCode(10050)
    ->setNotificationUrls(['https://exemplo.test/webhook/pagbank'])
    ->create();

$phpay->getPixCode($pedido['id']);   // de qr_codes[0].text

Cartão e boleto, aí sim, vão em charges:

$phpay
    ->setCustomer($cliente)
    ->addItem('Camiseta', 5990, 2)
    ->setCharges([[
        'reference_id'   => 'cobranca-1',
        'amount'         => ['value' => 11980, 'currency' => 'BRL'],
        'payment_method' => ['type' => 'CREDIT_CARD', 'installments' => 1, 'capture' => true],
    ]])
    ->create();

Assinaturas sempre pertencem a um plano, e o assinante pode nascer junto:

$phpay = PHPay::gateway(new PagBankGateway(TOKEN_PAGBANK_SANDBOX))->subscription();

$plano = $phpay->createPlan([
    'name'     => 'Plano PHPay Mensal',
    'amount'   => ['value' => 4990, 'currency' => 'BRL'],   // R$ 49,90
    'interval' => ['unit' => 'MONTHS', 'length' => 1],
]);

$phpay->setPlan($plano['id'])
    ->setCustomer(['name' => 'Mário', 'email' => 'fale@phpay.io', 'tax_id' => '12345678901'])
    ->create();

Pagar.me

Autenticação Basic com a secret key, ambiente pelo prefixo da chave, e Pix como forma de pagamento do pedido.

use PHPay\PagarMe\PagarMeGateway;

$gateway = new PagarMeGateway(SECRET_KEY_PAGARME);

$pedido = PHPay::gateway($gateway)->charge()
    ->setCustomer([
        'name'     => 'Mário Lucas',
        'email'    => 'fale@phpay.io',
        'document' => '12345678901',
    ])
    ->addItem('Assinatura PHPay', 10050)   // R$ 100,50
    ->setPix(1800)                          // expira em 30 minutos
    ->create();

$phpay->getPixCode($pedido['id']);   // de charges[0].last_transaction.qr_code

O cancelamento é DELETE, com valor opcional para estorno parcial:

$phpay->cancel($cobrancaId, 2500);   // estorna R$ 25,00
$phpay->cancel($cobrancaId);         // estorna tudo

Assinaturas aceitam um plano ou a recorrência no próprio payload:

$phpay = PHPay::gateway($gateway)->subscription();

$plano = $phpay->createPlan([
    'name'           => 'Plano PHPay Mensal',
    'interval'       => 'month',
    'interval_count' => 1,
    'items'          => [[
        'name'           => 'Mensalidade',
        'quantity'       => 1,
        'pricing_scheme' => ['price' => 4990],   // R$ 49,90
    ]],
]);

$phpay->setPlan($plano['id'])
    ->setCustomerId($clienteId)
    ->create(['payment_method' => 'pix']);

Consultando entregas de webhook

O Pagar.me deixa ler e reenviar os eventos que já despachou. Isso não é a capacidade SupportsWebhooks — o cadastro dos endpoints é no dashboard — então vive no gateway concreto, não na facade:

$gateway->webhookDeliveries()->setFilter(['size' => 10])->getAll();
$gateway->webhookDeliveries()->resend($hookId);

É assim que o modelo de capacidades abre espaço para o que só um gateway oferece: quem segura PagarMeGateway alcança, quem tipa uma capacidade não.

Cielo

A primeira adquirente da biblioteca, e a forma mostra: não há recurso de cliente — ele é um campo da venda. Daí as duas capacidades.

A particularidade é que a Cielo separa dois hosts por tipo de operação: escritas vão para api.cieloecommerce..., consultas para apiquery.cieloecommerce.... O mesmo recurso usa os dois, e o PHPay roteia sozinho — create() vai num, find() no outro.

use PHPay\Cielo\CieloGateway;

$phpay = PHPay::gateway(new CieloGateway(MERCHANT_ID, MERCHANT_KEY))->charge();

$venda = $phpay
    ->setOrderId('pedido-1')
    ->setCustomer(['Name' => 'Mário Lucas'])
    ->setPix(15700)             // R$ 157,00
    ->setRequestId('pedido-1')  // idempotência
    ->create();

$phpay->getPixCode($venda['Payment']['PaymentId']);

Cartão em duas etapas — autoriza agora, captura depois:

$phpay
    ->setCustomer(['Name' => 'Mário Lucas'])
    ->setCreditCard(15700, $cartao, installments: 3)   // capture: false por padrão
    ->create();

$phpay->capture($paymentId);
$phpay->cancel($paymentId, 2500);   // estorna R$ 25,00

Recorrência

A Cielo não tem endpoint de criar assinatura: a recorrência nasce de uma venda com um bloco RecurrentPayment, e só então ganha um RecurrentPaymentId próprio. Sempre cobra cartão.

use PHPay\Cielo\Enums\RecurrentIntervalEnum;

$phpay = PHPay::gateway(new CieloGateway(MERCHANT_ID, MERCHANT_KEY))->subscription();

$recorrencia = $phpay
    ->setCustomer(['Name' => 'Mário Lucas'])
    ->setCard($cartao)
    ->setInterval(RecurrentIntervalEnum::MONTHLY)
    ->setEndDate('2027-12-31')
    ->create(15700);

$id = $recorrencia['Payment']['RecurrentPayment']['RecurrentPaymentId'];

$phpay->updateAmount($id, 19900);
$phpay->deactivate($id);
$phpay->reactivate($id);
### Rede

Adquirente, e a forma mais estreita da biblioteca: **só
cobranças**. Não há recurso de cliente nem assinatura gerenciável — a
transação tem um campo `subscription`, mas é uma flag para a adquirente, não
algo que você liste ou cancele.

A particularidade é a autenticação: **OAuth2 `client_credentials` num host
separado do de API**, com o caminho do token diferente em cada ambiente. E o
token **expira** — o PHPay renegocia sozinho quando isso acontece.

```php
use PHPay\Rede\Enums\TransactionKindEnum;
use PHPay\Rede\RedeGateway;

/* nenhuma chamada de rede aqui: o token é negociado no primeiro uso */
$gateway = new RedeGateway(REDE_PV, REDE_TOKEN);

$transacao = PHPay::gateway($gateway)->charge()
    ->setReference('pedido-1')
    ->setCard('5448280000000007', 'MARIO LUCAS', '12', '2030', '123')
    ->setPayment(2099, TransactionKindEnum::CREDIT)   // R$ 20,99
    ->setSoftDescriptor('PHPAY')
    ->create();

Fluxo em duas etapas — autoriza agora, captura quando o pedido for separado:

$phpay->setPayment(5000, capture: false)->create();

$phpay->capture($tid);
$phpay->refund($tid, 1000);   // estorna R$ 10,00

O código de retorno "00" significa aprovada:

use PHPay\Rede\Enums\TransactionStatusEnum;

TransactionStatusEnum::approved($phpay->getStatus($tid));

Num processo longo, dá para inspecionar ou descartar o token em mãos:

$gateway->authorization()->hasValidToken();
$gateway->authorization()->forget();

AbacatePay

Pix nativo, e mesmo assim não declara SupportsPixKeys — aquela capacidade é sobre gerenciar chaves e QR Code estático, coisa de PSP. Aqui Pix é o método de pagamento da cobrança, e o único aceito.

Também não declara assinaturas: a API documenta ONE_TIME como a única frequência aceita.

A cobrança é um link de pagamento montado a partir de produtos, não de um valor solto — o total vem calculado em amount. Preço em centavos, com mínimo de 100 (R$ 1,00) por produto.

use PHPay\AbacatePay\AbacatePayGateway;

$gateway = new AbacatePayGateway(ABACATEPAY_TOKEN);

$cobranca = PHPay::gateway($gateway)->charge()
    ->setCustomer([
        'name'      => 'Mário Lucas',
        'email'     => 'fale@phpay.io',
        'cellphone' => '(11) 4002-8922',
        'taxId'     => '12345678901',
    ])
    ->addProduct('prod-1234', 'Assinatura PHPay', 2000)   // R$ 20,00
    ->setUrls(
        completionUrl: 'https://exemplo.test/obrigado',
        returnUrl: 'https://exemplo.test/loja'
    )
    ->create();

$phpay->getPaymentUrl($cobranca);   // o link para onde mandar o cliente
$phpay->isDevMode($cobranca);       // em qual ambiente a cobrança nasceu

O externalId do produto é o id no seu sistema — o AbacatePay cria o produto do lado dele a partir dele, então precisa ser único.

Cupons de desconto

Nenhum outro gateway da biblioteca tem isso, então não é capacidade: vive no gateway concreto, como o webhookDeliveries() do Pagar.me.

$gateway->coupons()->create([
    'code'         => 'PHPAY10',
    'discountKind' => 'PERCENTAGE',
    'discount'     => 10,
]);

Woovi/OpenPix

O segundo gateway com as cinco capacidades, ao lado do Asaas — e o que confirma que o modelo descreve o domínio, não um fornecedor: são duas empresas independentes, com APIs independentes, preenchendo o mesmo contrato.

Sendo PSP Pix-nativo, ele gerencia chaves e QR Code estático de verdade.

use PHPay\Woovi\Enums\PixKeyTypeEnum;
use PHPay\Woovi\WooviGateway;

$phpay = PHPay::gateway(new WooviGateway(WOOVI_APP_ID));

/* chaves Pix da conta */
$phpay->pix()->createKey(PixKeyTypeEnum::RANDOM);
$phpay->pix()->getAll();

/* consulta uma chave antes de pagar */
$phpay->pix()->verifyKey('fale@phpay.io');

/* QR Code estático, com ou sem valor */
$phpay->pix()->staticQrCode('Caixa 1');
$phpay->pix()->staticQrCode('Caixa 2', 2500);

Três particularidades:

O AppID vai cru no Authorization — sem Bearer, sem Basic.

O sandbox tem domínio próprio: api.woovi-sandbox.com contra api.openpix.com.br.

Todo objeto é endereçável pelo correlationID, o id no seu sistema — nenhum outro gateway da biblioteca oferece isso:

$cobranca = $phpay->charge()
    ->setCorrelationId('pedido-1')
    ->setCustomer(['name' => 'Mário Lucas', 'email' => 'fale@phpay.io'])
    ->create(10050);   // R$ 100,50

$phpay->charge()->getPixCode($cobranca);
$phpay->charge()->find('pedido-1');      // pelo SEU id, não pelo do gateway

Webhooks têm CRUD por API — junto com o Asaas, os únicos:

$phpay->webhook(['name' => 'PHPay', 'url' => 'https://exemplo.test/webhook'])->create();
$phpay->webhook()->getAll();

Repare que o webhook fica em api/openpix/v1/, enquanto os demais recursos ficam em api/v1/ — herança da fusão das duas marcas. O PHPay trata isso internamente.

Efí

Duas APIs com as mesmas credenciais, e é isso que dá ao Efí quatro das cinco capacidades:

API Host Autenticação Recursos
Cobranças cobrancas.api.efipay.com.br OAuth2 charge() — boleto
Pix pix.api.efipay.com.br OAuth2 + mTLS pix(), webhook(), subscription(), pixCharge()

Clientes ficam de fora: nenhuma das duas APIs mantém cadastro de cliente.

O gateway não faz chamada de rede no construtor. Cada API tem o seu token, pedido na primeira vez que é necessário e renovado sozinho quando expira — um gateway vivo num worker de fila não passa a tomar 401.

Cobranças (boleto)

use PHPay\Efi\EfiGateway;
use PHPay\Support\{Customer, Money};

$gateway = new EfiGateway(CLIENT_ID, CLIENT_SECRET);

$cobranca = PHPay::gateway($gateway)->charge([
    'description' => 'Assinatura PHPay',
    'expire_at'   => date('Y-m-d', strtotime('+3 days')),
])
    ->setAmount(Money::reais('100,50'))
    ->setCustomer(new Customer('Mário Lucas', '12345678909'))
    ->create();

API Pix

Toda requisição da API Pix é por mTLS, inclusive a do token. Passe o certificado .p12 (ou .pem) da aplicação, que você baixa no painel do Efí:

$gateway = new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: '/caminho/certificado.p12');

Com senha, ou vindo de uma variável de ambiente — o comum em container e serverless:

use PHPay\Http\Certificate;

new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: new Certificate('/caminho/certificado.p12', 'senha'));

new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: Certificate::fromBase64(getenv('EFI_CERTIFICATE_BASE64')));

O fromBase64() grava o certificado num arquivo temporário com permissão 0600, apagado quando o processo termina. A senha nunca aparece num var_dump().

Certificado de homologação só funciona com $sandbox = true, e o de produção, com false. Sem certificado, a API de Cobranças continua funcionando e a API Pix responde com uma ValidationException clara, não com um erro de TLS.

Cobrança Pix — imediata por padrão, com vencimento quando há setDueDate():

$cobranca = $gateway->pixCharge()
    ->setAmount(Money::reais('123,45'))
    ->setKey('sua-chave-pix')                       // chave da conta Efí que recebe
    ->setCustomer(new Customer('Mário Lucas', '12345678909'))
    ->setDescription('Pedido 1234')
    ->setExpiration(3600)                           // segundos
    ->create();

$qr = $gateway->pixCharge()->qrCode($cobranca['loc']['id']);
$qr['qrcode'];         // copia e cola
$qr['imagemQrcode'];   // PNG em base64

/* com vencimento: multa, juros e desconto como num boleto */
$gateway->pixCharge()
    ->setAmount(Money::reais(250))
    ->setKey('sua-chave-pix')
    ->setCustomer(new Customer('Sixtec LTDA', '12345678000199'))
    ->setDueDate('2026-12-31', validityAfterDue: 15)
    ->create();

/* devolução, total ou parcial */
$gateway->pixCharge()->refund($endToEndId, Money::reais(10));

pixCharge() é um extra do gateway concreto, como o webhookDeliveries() do Pagar.me: o charge() da facade já é o boleto, e mudar o retorno dele quebraria quem está na v2.

Chaves Pix — só chaves aleatórias (EVP) são gerenciáveis pela API:

$chave = PHPay::gateway($gateway)->pix()->createKey()['chave'];

PHPay::gateway($gateway)->pix()->getAll();
PHPay::gateway($gateway)->pix()->destroy($chave);

Webhooks — um por chave Pix, endereçado pela própria chave:

PHPay::gateway($gateway)
    ->webhook(['chave' => $chave, 'webhookUrl' => 'https://loja.com/webhook/pix'])
    ->create();

O mTLS vale nos dois sentidos: por padrão o Efí só entrega para um servidor que valide o certificado dele. Se o seu não consegue (hospedagem compartilhada, balanceador que termina o TLS), skipMtlsChecking() desliga a checagem — e aí valide a origem de outro jeito, como um hmac na URL.

Pix Automático — o pagador autoriza uma vez no app do banco, e cada ciclo é debitado sem nova aprovação:

use PHPay\Efi\Enums\{AccountTypeEnum, PeriodicityEnum};

$assinaturas = PHPay::gateway($gateway)->subscription();

/* 1. o location que o QR Code de autorização aponta */
$location = $assinaturas->createLocation();

/* 2. a recorrência: o que o pagador autoriza */
$recorrencia = $assinaturas
    ->setCustomer(new Customer('Mário Lucas', '12345678909'))
    ->setContract('CONTRATO-2026-001')              // até 35 caracteres
    ->setDescription('Plano mensal')
    ->setAmount(Money::reais('49,90'))               // ou setMinimumAmount(), para valor variável
    ->setPeriodicity(PeriodicityEnum::MONTHLY, '2026-10-01')
    ->allowRetries()                                 // até 3 tentativas em 7 dias
    ->setLocation($location['id'])
    ->create();

$assinaturas->find($recorrencia['idRec'])['dadosQR'];   // copia e cola para o pagador autorizar

/* 3. a cobrança de cada ciclo */
$assinaturas
    ->setReceiver('12345-6', AccountTypeEnum::CHECKING, '0001')
    ->createCharge($recorrencia['idRec'], Money::reais('49,90'), '2026-11-05');

$assinaturas->cancel($recorrencia['idRec']);

Exemplos executáveis

O diretório examples/ traz scripts prontos por gateway. Copie o credentials.example.php para credentials.php, preencha, e rode:

php examples/asaas/charges.php

Dois gateways têm também uma checagem de conformidade, que roda contra o sandbox de verdade e relata cada operação. Teste com HTTP mockado prova que a biblioteca monta o payload que decidimos; isto prova que o gateway o aceita:

MP_ACCESS_TOKEN='TEST-...' php examples/mercadopago/sandbox-check.php
PAGBANK_TOKEN='...'        php examples/pagbank/sandbox-check.php

Os dois recusam credenciais de produção e nunca imprimem o token.

Migrando da v1

A v2.0.0 tem breaking changes — a principal é que falhas passaram a ser exceção em vez de array de erro. O de-para completo, quebra por quebra, está em UPGRADE.md.

Dois pontos merecem auditoria de quem vem da v1:

  1. Falhas que antes voltavam como array e passavam despercebidas agora interrompem o fluxo. É o comportamento correto, mas expõe caminhos que nunca foram exercitados.
  2. Integrações que chamavam setCustomer() em laço provavelmente acumularam clientes duplicados no gateway.

Roadmap

Plataforma

Item Status
Definições de arquitetura
Capacidades por gateway
Tratamento de erros por exceção
Testes com HTTP mockado
CI no GitHub Actions
Guia de migração
Documentação ✍️
Site 🕛

Cobertura por gateway

Gateway Cobranças Clientes Assinaturas Webhooks Pix
Asaas
Woovi/OpenPix
Mercado Pago
PagBank
Pagar.me leitura ✅
AbacatePay
Cielo
Rede 🕥
Efí

pronto · ✍️ parcial · 🕥 planejado · não existe na API do gateway

Contribuindo

Leia o manual de contribuição. Ele cobre o ambiente de desenvolvimento, o gate de qualidade e as convenções do projeto.

Contribuições enviadas a partir de 2026-09-20 estão sujeitas ao Contributor License Agreement, aceito por checkbox no pull request.

composer install
composer test     # Pint + Pest + PHPStan nível 9

Nenhum teste pode acessar a rede: os recursos aceitam um GuzzleHttp\Client injetado, e a suíte usa mocks.

Segurança

Encontrou uma vulnerabilidade? Não abra issue pública — siga a política de segurança.

Esta é uma biblioteca de pagamentos: nunca logue, imprima ou versione tokens, access_token, clientSecret ou CPF/CNPJ reais.

Licença

O PHPay é distribuído sob a Business Source License 1.1 a partir da versão 2.0.0. É uma licença source-available: o código é aberto e auditável, com uma única restrição comercial.

O resumo abaixo não substitui a licença — ele existe só para você saber rápido se precisa ler o texto completo.

Pode Usar em produção, inclusive em software fechado e comercial
Pode Processar pagamentos seus ou dos seus clientes
Pode Modificar, forkar, estudar e redistribuir
Não pode Oferecer o PHPay, ou um derivado, como biblioteca, SDK ou serviço de integração de pagamentos que concorra com ele

Ou seja: se você integra pagamentos no seu produto, nada muda para você. A restrição atinge apenas quem quiser revender o próprio PHPay.

Em 2030-09-20 a licença converte automaticamente para MIT, e cada versão converte no máximo quatro anos após ser publicada.

As versões 1.0.0 e 1.0.1 foram publicadas sob MIT e permanecem sob MIT — uma licença nova não é retroativa. O texto está preservado em LICENSE-MIT.md.

Precisa de termos diferentes? Escreva para fale@phpay.io.

Feito por Mário Lucas · fale@phpay.io