Repository navigation
Releases
v2.8.0 — Inter, o primeiro banco
Décimo gateway, e o primeiro de um banco.
Inter
A API Pix do Inter é a especificação do BACEN quase literal: mesmos
caminhos, mesmos schemas, mesma taxonomia de erro. Isso encolheu o trabalho — é
o mesmo terreno da API Pix da Efí.
use PHPay\Http\Certificate;
use PHPay\Inter\InterGateway;
use PHPay\Support\{Customer, Money};
$gateway = new InterGateway(
CLIENT_ID,
CLIENT_SECRET,
certificate: Certificate::pair('/caminho/certificado.crt', '/caminho/chave.key'),
);
$cobranca = $gateway->charge()
->setKey(PIX_KEY)
->setAmount(Money::reais('110,00'))
->setCustomer(Customer::make('Fulano de Teste', '12345678901'))
->create();| Capacidade | |
|---|---|
SupportsCharges |
cobrança Pix, imediata (cob) e com vencimento (cobv) |
SupportsWebhooks |
um por chave Pix, como a Efí |
SupportsWebhookEvents |
leitura da notificação |
charge() é a cobrança Pix — na Efí charge() é o boleto —, porque o
boleto do Inter é outra API (/cobranca/v3) que esta versão não fala.
A notificação diverge do padrão em dois pontos
E os dois falham em silêncio, por isso vale saber antes de integrar.
O corpo é um array na raiz, não o {"pix": [...]} que o BACEN manda e que a
Efí entrega:
[{ "endToEndId": "E876...", "txid": "9711...", "valor": "110.00", "horario": "..." }]Um parser escrito pela especificação não acharia a chave pix e devolveria
zero eventos para todo pagamento de verdade, sem erro nenhum. O
webhookEvents() lê as duas formas.
devolucoes é um objeto, onde o padrão manda array. Pode ser erro de
documentação deles, então as duas leituras passam: lista continua lista, objeto
vira lista de um.
Correção, publicada depois: estas notas diziam que o Inter acrescenta
/pixà URL do webhook. Não acrescenta — a notificação chega na URL que
você cadastrou. A afirmação veio de uma leitura errada da expressão
{$request.body#/webhookUrl/pix}que o Redoc mostra na referência: o callback
se chamapixna especificação e o renderizador juntou o nome à expressão.
O sufixo/pixé quirk documentado do Itaú, não do Inter. O código nunca
esteve errado — a biblioteca manda a URL como você passou.
O certificado vem em dois arquivos
O Inter entrega um .crt e um .key, e a documentação dele manda juntar com
openssl pkcs12 -export antes de usar. Não precisa: o cURL aceita as
metades como vêm.
Certificate::pair($crt, $key, $senha); // de arquivo
Certificate::pairFromBase64($crtB64, $keyB64, $senha); // de variável de ambienteA opção "sem mudança no core" custava mais do que parecia: o .pfx que aquele
comando gera era recusado pelo próprio Certificate, porque o Guzzle não
reconhece essa extensão — então o usuário convertia e ainda precisava renomear.
Num par a senha vai na chave, nunca no certificado. O Guzzle transforma o
segundo elemento de cert em CURLOPT_SSLCERTPASSWD, e o .crt é público:
não tem o que decifrar. Verificado contra um servidor que exige certificado de
cliente, nos três caminhos do par e no .p12 de controle.
O .gitignore passou a cobrir *.crt, *.cer e *.key. Chave privada
commitada não se desfaz com um revert.
O token é emitido por escopo
Primeiro gateway assim. scope é obrigatório e não existe token que carregue
todas as permissões, então o gateway pede a união do que as capacidades usam —
pedir escopo que a aplicação não tem reprova a requisição inteira.
O cache não é otimização: gerar token é limitado a 5 chamadas por minuto,
e a documentação manda reaproveitar pela hora de validade. Pedir por requisição
levaria 429 na sexta.
O que o Inter não declara, e por quê
- estorno existe na API, mas o schema exige
valorenquanto a capacidade
promete que valor ausente estorna integral, e a chamada é endereçada pelo
endToEndIddo pagamento — mesma razão da Efí, então fica em
charge()->refund(); - assinatura de webhook: a notificação vem por mTLS, que o certificado já
cobre. Não existe esquema para verificar; - chaves Pix: a API não tem endpoint de DICT. A chave que endereça o webhook
é uma que você já tem; - clientes e assinaturas não existem na API.
Três coisas que a referência descreve e o gateway não serve, conferidas na
lista de operações: gestão de chave Pix, /loc/{id}/qrcode — o copia-e-cola
volta na própria cobrança — e a listagem de webhooks. A taxonomia de erros
menciona um GET /webhook, mas aquela seção é o texto do BACEN copiado inteiro.
Compatibilidade
Só entraram métodos novos; nenhuma assinatura existente mudou. A tabela de
capacidades do README ganhou as colunas de estorno e leitura de webhook, que
faltavam desde que as duas entraram.
Nada do Inter foi exercitado contra o sandbox dele: a forma dos payloads veio da
referência, e a suíte não sai para a rede.
Documentação: Inter ·
Certificado e mTLS
v2.7.0 — quem usa a biblioteca passa a conseguir testar
Uma mudança só, e ela é para quem usa a biblioteca, não para quem a mantém.
Testar sem sair para a rede
A suíte do PHPay injeta Client em cada Resource, e os construtores aceitam ?Client $client justamente para isso. Mas quem consome a biblioteca não constrói Resource: recebe o gateway montado, sobretudo resolvendo por nome. E o registry não aceita Client.
Resultado até aqui: uma aplicação que usasse o PHPay não escrevia teste de controller, de listener de webhook nem de job sem falar com o gateway de verdade.
use PHPay\Http\ClientFactory;
ClientFactory::fake([
new Response(200, [], json_encode(['id' => 'pay_001', 'status' => 'PENDING'])),
]);
/* o código da aplicação roda igual, sem saber que está em teste */
$pedido->pagar();
ClientFactory::recorded(); // o que ele mandou
ClientFactory::stopFaking();| Membro | |
|---|---|
fake($respostas) |
daqui em diante, todo client montado responde da fila |
faking() |
se está em modo falso |
recorded() |
as requisições que a aplicação fez |
stopFaking() |
volta ao normal e limpa o histórico |
Por que na fábrica
Aceitar Client no registry era o caminho óbvio, e o levantamento o descartou: quatro gateways têm mais de um client.
| Gateway | Clients |
|---|---|
| Efí | Cobranças + API Pix |
| Cielo | escritas + consultas |
| Rede | transações + host de OAuth |
Um client só falsearia metade. Na Rede seria pior: ela pede token antes de cada chamada, num host separado, então a outra metade iria para a rede na primeira linha do teste.
Como toda a biblioteca monta client em ClientFactory::make() — 14 pontos de chamada, sem nenhum new Client( fora dela, e com teste varrendo src/ que reprova quem furar —, falsear ali alcança todos de uma vez. E nenhuma assinatura mudou: nem a do registry, nem a dos construtores.
Três coisas para não se machucar
O retry continua instalado. Falsear a rede não desliga a política que impede cobrança dupla — teste que queira exercitá-la põe duas respostas na fila.
Só request e options são gravados. Em modo falso as respostas são as que o seu teste enfileirou, então repeti-las não diz nada. O que o teste não sabe de antemão é a requisição.
É estado estático, com o mesmo contrato do Mail::fake() do Laravel: opt-in, e stopFaking() restaura. Desligue no teardown:
afterEach(fn () => ClientFactory::stopFaking());Client injetado no construtor não é afetado, porque não passa pela fábrica — é o que mantém a suíte da própria biblioteca funcionando.
A página Testando cobre o resto, e o de-para completo está no CHANGELOG.
v2.6.0 — contrato comum para os verificadores de assinatura
Uma mudança só, e ela é para quem não sabe o gateway em tempo de escrita.
Nenhuma quebra. O caminho até aqui passou por um desenho que quebrava, e a seção final explica.
Os verificadores de assinatura ganharam contrato
Os cinco gateways que assinam concordam em exatamente quatro métodos — um segredo, o corpo cru, os headers e uma resposta — e discordam em todo o resto: token no header, HMAC do corpo, RSA contra chave publicada, segredo na query.
Esse pedaço comum é o que código genérico precisa, e até agora ele não conseguia alcançá-lo: webhookSignature() devolvia object, sem nada para tipar.
use PHPay\Webhooks\VerifiesSignature;
$phpay = PHPay::make('asaas', $config); // não sei o gateway em tempo de escrita
$ok = $phpay->webhookSignature() // VerifiesSignature, não object
->setSecret($segredo)
->setPayload($corpo)
->setHeaders($headers)
->verify();Quem segura o gateway concreto nunca teve o problema — AsaasGatewayInterface::webhookSignature() já devolve o verificador concreto. Isto é para quem não pode segurá-lo, que é o caso que a v2.5.0 passou a atender com PHPay::make().
Os setters de esquema específico ficam fora do contrato, porque pertencem a um esquema cada: setDataId() no Mercado Pago, setQuery() e setSignatureKey() na AbacatePay, setPublicKeys() na Woovi. Código genérico pergunta por eles com method_exists().
Por que a capacidade continua devolvendo object
O desenho óbvio seria estreitar o retorno da capacidade SupportsWebhookSignature. Isso é compatível para quem consome e fatal para quem implementa — tipo de retorno em PHP é covariante:
Fatal error: Declaration of GatewayPrivadoDeTerceiro::webhookSignature(): object
must be compatible with SupportsWebhookSignature::webhookSignature(): VerifiesSignature
Gateway próprio implementando uma capacidade é cenário suportado pela biblioteca, então aquele caminho exigiria um major. Em vez disso, só a facade estreita, e como a capacidade não garante mais o contrato, a facade confere em runtime e recusa um verificador que não o cumpra:
Privado: O verificador de assinatura deste gateway não cumpre
PHPay\Webhooks\VerifiesSignature.
Nos nove gateways da biblioteca isso nunca dispara, e há teste varrendo todos para continuar assim.
O que fica aberto
Quem tipa a capacidade para injeção de dependência — o que a documentação de capacidades recomenda — ainda recebe object. Fechar essa ponta exige major, e está registrado no CLAUDE.md para não ser redescoberto.
O de-para completo está no CHANGELOG.
v2.5.0 — estorno, status e webhook com forma única, e o gateway podendo vir da config
Cinco frentes entraram desde a v2.4.0, e quatro delas são a mesma ideia aplicada em lugares diferentes: o que divergia entre os nove gateways passa a ter forma única.
Nenhuma assinatura existente mudou de comportamento. Duas quebram em caso de borda, e estão descritas no fim.
Estorno virou capacidade
Seis gateways já estornavam, com três nomes para a mesma operação — refund, cancel, void —, três unidades e três sentidos para valor ausente. E nenhum era alcançável pela facade: quem segurava PHPay tinha que descer para o gateway concreto e saber de antemão como o método se chamava ali.
$phpay->refund($id); // devolve tudo
$phpay->refund($id, 2500); // parte, na unidade daquele gateway
$phpay->refund($id, Money::reais(25.50)); // o mesmo valor nos seteAsaas e Woovi ganharam o método — os dois gateways completos não o tinham, embora a API dos dois exponha o endpoint. Cielo e Pagar.me ganharam refund() sobre o endpoint que já tinham; cancel() continua nas duas, porque ali um endpoint só anula e estorna.
Efí e AbacatePay ficam de fora, cada uma pelo seu motivo, e os motivos estão na documentação.
Status de pagamento numa forma só
A mesma cobrança paga voltava como RECEIVED, COMPLETED, approved, PAID, paid, CONCLUIDA ou '00', dependendo de quem respondeu.
use PHPay\Asaas\Enums\InvoiceStatusEnum;
use PHPay\Support\PaymentStatus;
if (InvoiceStatusEnum::of($cobranca['status']) === PaymentStatus::PAID) {
// libera o pedido
}Os enums concretos continuam existindo e acessíveis. Cinco gateways não tinham enum de status e ganharam o seu — a Efí ganhou dois, porque as APIs de Cobranças e Pix respondem em vocabulários diferentes com as mesmas credenciais.
Três estados merecem nota, porque conflatá-los erraria sobre dinheiro: AUTHORIZED não é PAID (Cielo, PagBank e Mercado Pago separam autorização de captura), PARTIALLY_PAID existe pelo underpaid do Pagar.me, e PAID não é estado final — estorno e chargeback ainda vêm depois.
O webhook, do outro lado
A v2.3.0 provou que a notificação veio do gateway. Esta lê o que ela diz:
foreach ($gateway->webhookEvents($corpo) as $evento) {
if ($evento->type === EventType::PAID) {
liberarPedido($evento->resourceId);
}
}Devolve lista, nunca um evento. A API Pix da Efí manda pix como array — vários pagamentos numa requisição — e o PagBank manda o pedido com todas as cobranças. Tratar como objeto único perderia pagamento em silêncio.
E quando a notificação não carrega estado — Mercado Pago manda só data.id, a Cielo manda três campos —, status é null e requiresLookup() diz que é preciso consultar:
if ($evento->requiresLookup()) {
$cobranca = $phpay->charge()->find($evento->resourceId);
}O gateway pode vir da configuração
Antes só era possível começar sabendo a classe e a assinatura do construtor.
$phpay = PHPay::make('asaas', ['token' => $token, 'sandbox' => true]);PHPay::gateway() continua sendo o preferido quando você sabe o gateway em tempo de escrita: segurando o concreto, capacidade ausente é erro de análise e não de runtime.
Config errada falha na construção, nomeando a chave — inclusive chave desconhecida, porque um skandbox engolido é typo que só aparece em produção.
Guzzle 8, em comportamento e em tipos
A v2.5.0 aceita ^7.3 || ^8.0. A RetryPolicy continua distinguindo "não saiu da máquina" de "deu timeout depois de enviar" nas duas majors — a decisão que impede cobrança duplicada —, e agora o PHPStan nível 9 passa nas duas, com job próprio no CI.
Nada de nível afrouxado nem ignoreErrors: o que se passa ao Guzzle virou forma de array fechada, e a pilha de handlers é montada como variável local, porque declarar HandlerStack como tipo falha numa das duas majors com ou sem argumento genérico.
De passagem
Cobertura de testes de 88,8% para 91,2%, fechando os oito arquivos piores — entre eles o Asaas/Charge, que era o recurso mais usado da biblioteca e estava em 56,5%.
Duas quebras de borda
-
O parâmetro do
refund()do Mercado Pago foi renomeado de$valuepara$amount. O tipo alargou (?float→Money|int|float|null), então chamada posicional continua valendo e número cru continua lido em reais. Quebra só quem chamava por argumento nomeado:refund($id, value: 10.5). -
Três métodos
protectedsaíram doHasRedeClient:clientRedeOauthBoot(),oauthUri()eoauthTokenPath(). Não tinham chamador em lugar nenhum — oRedeGatewaysempre montou aquele client inline. É trait interno, mas quem o tivesse usado em código próprio sentiria.
O de-para completo está no CHANGELOG.
v2.4.0 — QR Code estático de Pix, CI no PHP 8.1 e a documentação em docs/
Quatro frentes de melhoria entraram desde a v2.3.0 — e a maior delas não é código, é a documentação ficar legível.
Nenhuma assinatura de método mudou. O que mudou de lugar foi a documentação.
QR Code estático de Pix, composto aqui
Pelo padrão do BACEN um QR Code estático é montado localmente, a partir da chave: não há o que pedir a ninguém. Asaas e Woovi expõem endpoint por conveniência, a Efí não expõe — e até agora a biblioteca só podia repassar essa assimetria para quem integrava.
use PHPay\Support\{Money, StaticPixCode};
StaticPixCode::make('fale@phpay.io', 'Sixtec LTDA', 'BRASILIA')
->withAmount(Money::reais('100,50')) // opcional: sem valor, o pagador digita
->withTxid('PEDIDO1234') // opcional: *** quando ausente
->payload();É suporte, como o Money: não é Resource, não é capacidade, não entra na enum Capability — não há chamada HTTP. Devolve só o copia-e-cola; renderizar a imagem exigiria um codificador de QR Code, e dependência nova numa biblioteca de pagamentos custa mais que a conveniência.
O CRC é a parte que erra. Ele cobre o literal 6304 que o anuncia, não para antes dele — e parar antes gera um código que simplesmente não lê, erro que aparece na frente do pagador e não no log. Por isso o vetor de teste do Manual de Padrões para Iniciação do Pix está na suíte exigindo o payload byte a byte, e também no smoke test do PHP 8.1.
Dois detalhes que valem a leitura da documentação: o nome do recebedor é exigido pelo padrão e ignorado pelo app do pagador, que mostra o nome vindo do DICT; e o txid aceita só letras e números, então PEDIDO-1234 não passa.
O PHP 8.1 passa a ser verificado por execução
O composer.json promete ^8.1, mas o CI rodava a matriz a partir do 8.2 — a compatibilidade com a versão mínima era garantida só pela análise estática. Agora existe um job próprio no 8.1: valida o composer.json, instala apenas as dependências de runtime, confere a sintaxe de src/ e roda um smoke test que carrega o autoload, instancia os nove gateways, percorre as capacidades e exercita os value objects. A suíte do Pest continua fora dessa versão, porque o toolchain de dev exige 8.2+.
Junto: cobertura de testes medida no CI, num job só, com o número no log e no resumo.
A documentação saiu do README
O README tinha 1.338 linhas e 46 KB; quem chegava pelo Packagist rolava 500 linhas de conceito antes de achar o gateway que usa. Agora tem 308 linhas, com o essencial — o que é, a matriz de capacidades, requisitos, instalação, início rápido — e um índice.
O resto virou docs/: sete páginas de conceito e nove de gateway.
Atenção: link externo que apontava para uma âncora do README antigo deixa de funcionar. O índice de
docs/cobre todos os destinos.
Nesse trabalho apareceu um defeito que estava escondido: o bloco de código do exemplo de recorrência da Cielo nunca era fechado, e cercas de markdown alternam — o título da Rede e a prosa embaixo dele renderizavam como código no GitHub, e isso seguia por AbacatePay, Woovi e Efí. Dez links do sumário apontavam para âncoras que, por causa disso, não existiam. Corrigido, e a matriz de capacidades ganhou a coluna de assinatura de webhook, que entrou na v2.3.0 e tinha ficado fora da tabela.
Para não repetir: o gate agora tem composer test:links, que confere todo link relativo e toda âncora dos markdowns do repositório — e não conta título dentro de bloco de código, que foi justamente como o erro passou.
Também
- O Dependabot deixa de propor major do Guzzle: a 8 removeu o acessor de que a
RetryPolicydepende para não repetir umPOSTjá processado. O motivo e o que falta estão registrados. - 506 testes (de 483), PHPStan nível 9, e CI obrigatório para mergear.
O detalhamento item por item está no CHANGELOG.
Instalação: composer require phpay-io/phpay
v2.3.0 — verificação de assinatura de webhook, prazos e credencial fora de dump
Cadastrar o webhook era metade do caminho. Esta versão entrega a outra: provar que a notificação veio do gateway. Junto vem a auditoria de segurança do bloco 1 — prazo em toda requisição, credencial fora de dump e unidade monetária validada.
Tudo aditivo: nenhuma assinatura de método mudou.
Antes de atualizar
ext-opensslpassou a ser requisito de runtime, por causa da verificação RSA da Woovi. Habilite a extensão antes de atualizar.- Valor decimal na Efí, no PagBank e no Pagar.me agora lança
ValidationExceptionem vez de emitir uma cobrança com o valor errado. Se você mandava10.50onde o gateway espera1050, a cobrança saía de onze centavos sem erro nenhum — agora para na validação, com a conversão na mensagem.
Verificação de assinatura de webhook
Capacidade nova, Capability::WEBHOOK_SIGNATURE, pelo mesmo caminho das outras:
$ok = $gateway->webhookSignature()
->setSecret($segredoDoWebhook)
->setPayload(file_get_contents('php://input'))
->setHeaders(getallheaders())
->verify();| Gateway | O que é verificado | O que isso prova |
|---|---|---|
| Asaas | token no header asaas-access-token |
que o remetente conhece o token — o corpo não é assinado |
| PagBank | x-authenticity-token: SHA-256 de {token}-{corpo cru} |
que o corpo chegou inteiro |
| Mercado Pago | x-signature: HMAC-SHA256 de id:…;request-id:…;ts:…; |
que é da sua aplicação e aponta para aquele recurso |
| Woovi/OpenPix | x-webhook-signature: RSA-SHA256 do corpo, contra as chaves publicadas |
que o corpo veio da Woovi |
| AbacatePay | webhookSecret na query, mais o HMAC do corpo quando a chave é dada |
que a URL é a sua e, com a chave, que o corpo não mudou |
O corpo verificado é sempre o cru (php://input, ou $request->getContent() no Laravel): reserializar o JSON muda o hash. verify() devolve bool, porque assinatura que não bate é resposta — o que levanta ValidationException é uso errado (segredo ausente, corpo vazio, header que não chegou).
Efí, Cielo, Rede e Pagar.me não declaram a capacidade, e isso é de propósito: a Efí notifica por mTLS, Cielo e Rede não mandam assinatura nenhuma e a v5 da Pagar.me não documenta esquema. Nos quatro, webhookSignature() levanta NotImplementedException. Um verificador sem nada para checar teria que responder true, o que é pior que não ter verificação, porque parece ter.
Segurança e robustez
- Prazo em toda requisição: 60s no total, 10s para conectar, pela
PHPay\Http\ClientFactory, que passa a ser o único lugar onde um client nasce. O padrão do Guzzle é esperar para sempre, e um gateway travado prendia o worker até o processo morrer. - Credencial fora de dump:
PHPay\Http\MasksCredentialsdá__debugInfo()a todo gateway e, peloHasHttpClient, a todo recurso — a credencial vira********e oClientinteiro fica oculto, porque ele carrega o token nos headers. Odd()do Symfony VarDumper lê propriedade privada por conta própria e não é coberto. #[\SensitiveParameter]em todo parâmetro que recebe credencial, para não aparecer em stack trace.MoneyrecusaNAN,INFe valor que estouraria os centavos num inteiro de 64 bits. Os três convertiam para0ou davam a volta e viravam negativo, passando por todas as checagens.
Também
CHANGELOG.mdretroativo, cobrindo as cinco releases anteriores.- 440 testes (de 393), nenhum tocando a rede. PHPStan nível 9.
O detalhamento item por item está no CHANGELOG.
Instalação: composer require phpay-io/phpay
v2.2.0 — Efí Pix com mTLS e assinaturas completas do Asaas
Duas lacunas fechadas. O Efí, que era o gateway mais fraco da biblioteca com 1 de 5 capacidades, passa a ter 4 de 5 com a API Pix. As assinaturas do Asaas, que só sabiam criar, passam a cobrir os 14 endpoints da API.
Para quem chama a biblioteca, tudo é aditivo. Veja a nota de compatibilidade abaixo.
🔐 Efí: API Pix com mTLS
O Efí tem duas APIs com as mesmas credenciais. A de Cobranças (boleto) já estava integrada, e a v2.2.0 integra a API Pix:
| Gateway | Clientes | Cobranças | Assinaturas | Webhooks | Chaves Pix |
|---|---|---|---|---|---|
| Efí v2.1.0 | — | ✅ | — | — | — |
| Efí v2.2.0 | — | ✅ | ✅ | ✅ | ✅ |
Toda requisição da API Pix é por mTLS, inclusive a do token. Basta passar o certificado da aplicação:
$efi = new EfiGateway($clientId, $clientSecret, certificate: '/caminho/certificado.p12');
$cobranca = $efi->pixCharge()
->setAmount(Money::reais('123,45'))
->setKey('sua-chave-pix')
->setCustomer(new Customer('Mário Lucas', '12345678909'))
->create();
$efi->pixCharge()->qrCode($cobranca['loc']['id'])['qrcode']; // copia e cola- Cobrança Pix (
pixCharge()): imediata ou com vencimento, QR Code e devolução - Chaves Pix (
pix()): chaves aleatórias (EVP) - Webhooks (
webhook()): um por chave Pix, comskipMtlsChecking()para servidores que não validam o certificado do Efí - Pix Automático (
subscription()): o pagador autoriza uma vez no app do banco, e cada ciclo é debitado sem nova aprovação. Cobre a recorrência, o QR Code de autorização e a cobrança de cada ciclo
Na API Pix, o valor só entra como Money. Ela quer reais ("123.45"), enquanto a API de Cobranças do mesmo gateway quer centavos. Aceitar número cru ali recriaria exatamente o erro que o Money existe para evitar.
🪪 Certificate: mTLS genérico
O padrão do BACEN para API Pix exige mTLS, e Inter, BB, Itaú, Sicoob e Sicredi seguem o mesmo esquema. Por isso o certificado é PHPay\Http\Certificate, e não algo exclusivo do Efí:
new Certificate('/caminho/certificado.p12', 'senha');
Certificate::fromBase64(getenv('EFI_CERTIFICATE_BASE64')); // para container e serverlessfromBase64() grava o certificado num temporário com permissão 0600, apagado ao fim do processo. A senha nunca aparece num var_dump().
🔁 Asaas: assinaturas completas
Antes só existia create(). Agora o recurso cobre toda a API de assinaturas do Asaas:
$assinaturas = $phpay->subscription();
$assinatura = $assinaturas
->setCustomer($cliente)
->setAmount(Money::reais('49,90'))
->setCycle(SubscriptionCycleEnum::MONTHLY)
->setSubscription(['billingType' => 'BOLETO', 'nextDueDate' => '2026-10-10'])
->create();
$id = $assinatura['id'];
$assinaturas->deactivate($id); // pausa sem apagar cobranças
$assinaturas->reactivate($id, '2026-11-10'); // o Asaas exige novo vencimento
file_put_contents('carne.pdf', $assinaturas->paymentBook($id));getAll(),find(),update(),destroy(),deactivate()ereactivate()getPayments(): as cobranças já geradas pela assinaturapaymentBook(): o carnê, como os bytes do PDFupdateCreditCard(): troca o cartão sem cobrar- Nota fiscal emitida automaticamente a cada cobrança:
create/get/update/destroyInvoiceSettings()egetInvoices()
🐛 Correções
- Efí: o token da API de Cobranças ficava em cache para sempre. Um gateway que fica vivo num worker de fila passava a tomar 401 quando o Efí expirava o token. Agora o token é renovado sozinho, e o da API Pix também.
- Asaas:
cyclepassa a ser obrigatório na criação de assinatura, como na especificação do Asaas. Antes o payload sem ele passava pela validação e só falhava no gateway.
⚠️ Nota de compatibilidade
Para quem chama a biblioteca, nada muda. Os parâmetros novos do EfiGateway (certificate e pixClient) são opcionais e ficam no fim do construtor, e create([...]) da assinatura do Asaas funciona como antes.
Três pontos para conferir:
- O Guzzle mínimo sobe de
^7.0para^7.3, a primeira versão que sabe usar certificado.p12. A 7.3 é de março de 2021. - Criar assinatura no Asaas sem
cycleagora lançaValidationExceptionantes da chamada, em vez deApiExceptiondo Asaas. A requisição já falhava; só falha mais cedo e com mensagem melhor. - Quem implementa a
SubscriptionInterfacedo Asaas fora da biblioteca precisa acompanhar as assinaturas novas:create(array $subscription = [])ganhou default, e a interface ganhou métodos. Quem só usa o recurso não é afetado.
🧪 Qualidade
275 → 354 testes (968 asserções), todos com HTTP simulado e sem rede.
- Os testes do Efí conferem que o client montado apresenta o certificado e o token em toda requisição, no host certo de cada ambiente.
- As rotas foram conferidas no SDK oficial do Efí, no padrão Pix do BACEN e na especificação oficial do Asaas.
Ainda não foram exercitadas contra os sandboxes reais, o que exige o certificado de homologação do Efí e o token de sandbox do Asaas. Os scripts examples/efi/pix.php e examples/asaas/subscriptions.php estão prontos para isso.
Full Changelog: v2.1.0...v2.2.0
v2.1.0 — Money, Customer e Woovi/OpenPix
Um gateway novo e dois value objects que fecham uma lacuna da v2.0.0: a biblioteca normalizava os nomes dos métodos entre gateways, mas deixava os payloads crus. Quem integrava ainda precisava saber que um gateway quer reais e outro quer centavos, e que o CPF se chama cpfCnpj num e document noutro.
Tudo aditivo — veja a nota de compatibilidade abaixo.
💸 Money: o erro de centavos vs reais, eliminado no tipo
Dois gateways usam reais decimais; os outros sete usam centavos inteiros. Mandar 100.50 num gateway de centavos não quebra a integração — cobra R$ 1,00, e você só descobre na conciliação.
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 10050O mesmo objeto, a unidade certa em cada gateway.
- Aceita string em notação brasileira:
'100,50','1.234,56' multiply(),plus(),equals(),format()→'R$ 100,50'- Recusa mais de duas casas decimais em vez de arredondar calado:
Money::reais(10 / 3)lança exceção. Arredondamento silencioso é como nascem erros de um centavo na conciliação. - Novo
setAmount()no Asaas, Mercado Pago e Efí, onde o valor vivia dentro do array de payload
👤 Customer: uma forma só para o cliente
O mesmo campo tinha seis grafias: cpfCnpj, tax_id, document, taxId, taxID, cpf_cnpj.
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 em todosE carrega o que cada gateway deriva do cliente:
$cliente->isIndividual(); // o Pagar.me precisa como type
$cliente->documentType(); // 'CPF' | 'CNPJ', a Cielo quer em IdentityType
$cliente->firstName(); // o Mercado Pago quer o nome separado
$cliente->phoneParts(); // o PagBank quer {country, area, number}O value object cobre todos os campos obrigatórios de cliente dos oito gateways que têm cliente. withExtra() fica para opcionais específicos, como endereço.
🎉 Gateway novo: Woovi/OpenPix
O segundo gateway com as cinco capacidades, ao lado do Asaas.
| Gateway | Clientes | Cobranças | Assinaturas | Webhooks | Chaves Pix |
|---|---|---|---|---|---|
| Asaas | ✅ | ✅ | ✅ | ✅ | ✅ |
| Woovi/OpenPix | ✅ | ✅ | ✅ | ✅ | ✅ |
Duas empresas independentes preenchendo o mesmo contrato é evidência de que o modelo de capacidades da v2.0.0 descreve o domínio, e não um fornecedor.
$phpay->pix()->createKey(PixKeyTypeEnum::RANDOM);
$phpay->pix()->verifyKey('fale@phpay.io');
$phpay->charge()->setCorrelationId('pedido-1')->create(Money::reais(100.50));
$phpay->charge()->find('pedido-1'); // pelo SEU id, não pelo do gateway⚠️ Nota de compatibilidade
Para quem chama a biblioteca, nada muda. Todo método que recebe valor aceita Money|int (ou Money|int|float nos gateways de reais), e todo setCustomer() aceita Customer|array. Número cru e array continuam funcionando, lidos como sempre foram.
Para quem implementou SupportsCustomers num gateway próprio, há uma mudança de assinatura:
// v2.0.0
public function customer(array $customer = []): object;
// v2.1.0
public function customer(Customer|array $customer = []): object;Uma implementação com customer(array ...) passa a dar erro fatal no autoload, porque não pode aceitar menos que a interface. Ajuste a assinatura da sua implementação para Customer|array.
Pela regra estrita do SemVer isso seria major. Optamos por minor porque o contrato foi publicado na v2.0.0 horas antes, e a falha é alta — erro fatal imediato — e não silenciosa.
🧪 Qualidade
219 → 275 testes (759 asserções). Os mais importantes são os cruzados: o mesmo Money e o mesmo Customer produzindo o formato certo em cada gateway, numa expectativa só.
v2.0.0 — oito gateways, exceções tipadas e capacidades declaradas
Primeira versão com quebras de compatibilidade desde o lançamento, e a maior da biblioteca até aqui: de 2 para 8 gateways, falhas passam a ser exceção, e o projeto ganha CI, guia de migração e licença nova.
O de-para completo, quebra por quebra, está em UPGRADE.md.
🎉 Seis gateways novos
| Gateway | Clientes | Cobranças | Assinaturas | Webhooks | Chaves Pix |
|---|---|---|---|---|---|
| Asaas | ✅ | ✅ | ✅ | ✅ | ✅ |
| Mercado Pago | ✅ | ✅ | ✅ | — | — |
| PagBank | ✅ | ✅ | ✅ | — | — |
| Pagar.me | ✅ | ✅ | ✅ | — | — |
| AbacatePay | ✅ | ✅ | — | — | — |
| Cielo | — | ✅ | ✅ | — | — |
| Rede | — | ✅ | — | — | — |
| Efí | — | ✅ | — | — | — |
Cobre o topo do mercado brasileiro: Cielo e Rede lideram entre adquirentes (28% por usuários e 18% por volume), Mercado Pago e PagBank dominam e-commerce e PME, Pagar.me é o favorito técnico, e o AbacatePay traz o perfil Pix-nativo.
🧩 Gateways declaram o que suportam
GatewayInterface era a união de tudo que o Asaas sabe fazer — todo gateway precisava declarar cinco recursos mesmo sem suportá-los. Agora cada recurso é uma interface que o gateway implementa se, e só se, oferecer:
use PHPay\Contracts\Capability;
$phpay->supports(Capability::SUBSCRIPTIONS);
$phpay->capabilities();
$phpay->name();Chamar um recurso indisponível diz o que o gateway oferece:
NotImplementedException: Rede não suporta chaves Pix.
Capacidades disponíveis: cobranças.
E quem segura o gateway concreto ganha a checagem em tempo de análise — (new RedeGateway(...))->pix() vira erro de PHPStan, não de produção.
SupportsPixKeyssignifica gerenciar chaves e QR Code estático, o que só um PSP que emite chave própria oferece. Todos os oito aceitam Pix — a coluna vazia não quer dizer o contrário.
🚨 Erros viram exceção
Um array retornado agora é sempre sucesso. Todas as exceções implementam PHPayException:
| Exceção | Quando |
|---|---|
ValidationException |
payload inválido, antes de qualquer HTTP |
ApiException |
gateway recusou ou está inacessível |
NotImplementedException |
recurso não suportado pelo gateway |
ApiException carrega getStatusCode(), getResponse(), getGateway() e isConnectionError().
💸 Unidade monetária documentada
Não estava em lugar nenhum, e errar não quebra a integração — ela cobra o valor errado:
| Gateway | Unidade | R$ 100,50 |
|---|---|---|
| Asaas, Mercado Pago | Reais (decimal) | 100.50 |
| PagBank, Pagar.me, Cielo, Rede, AbacatePay, Efí | Centavos (inteiro) | 10050 |
Nos que usam centavos, o PHPay recusa valor decimal na validação, antes da chamada.
🐛 Correções
- A validação de tipo não funcionava.
AsaasChargeRequestusava&&onde deveria usar||: um campo presente com o tipo errado passava batido e seguia para a API. setCustomer()criava cliente duplicado a cada chamada. Agora reaproveita quando o array trazid, e existesetCustomerId().EfiGatewayfazia chamada de rede no construtor. Autoriza sob demanda, uma vez só.Efi\Charge::getStatus()apontava para o endpoint do Asaas.WebhookEventsEnumusava namespace fora do autoload e não carregava.EfiGateway::customer()/webhook()devolviamstdClassvazio, causando "undefined method" adiante.examples/efi/charges.phppassava100.00num gateway que usa centavos — cobraria R$ 1,00.
🧪 Qualidade
- 3 → 219 testes (589 asserções). Todo recurso aceita um client HTTP injetado; nenhum teste toca a rede.
- CI no GitHub Actions, matriz 8.2/8.3/8.4 com dependências
locked,lowestehighest. - PHPStan nível 9 como fonte única, com
phpVersionmínimo 8.1 garantindo a compatibilidade declarada. - Checagem de conformidade contra sandbox para Mercado Pago e PagBank, que roda de verdade e recusa credenciais de produção.
⚖️ Licença: MIT → Business Source License 1.1
A partir desta versão, source-available com uma única restrição comercial: ninguém pode oferecer o PHPay, ou um derivado, como biblioteca/SDK/serviço de integração de pagamentos concorrente.
Usar em produção, inclusive em software fechado e comercial, continua liberado — quem integra pagamentos no próprio produto não é afetado.
Em 2030-09-20 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 permanecem sob MIT — licença nova não é retroativa, e quem já baixou mantém aqueles direitos para sempre.
Contribuições passam a estar sujeitas ao CLA.
⚠️ Breaking changes
- Métodos lançam exceção em vez de retornar
['error' => ...] - Gateways declaram capacidades —
GatewayInterfacetem sóname() PHPay\Gateways\Asaas\*eEfi\*deixaram de existirpix()não aceita mais argumentomessages()de cobrança e assinatura ficou planoAsaasExceptionseAsaasPixRequestremovidasAsaasGatewayperdeu as propriedades públicas$cliente$customer
Requisitos
Consumir a lib: PHP ^8.1 (inalterado). Desenvolver: PHP ^8.2.
v1.0.1
What's Changed
- wip: implemented pest tests by @mariolucasdev in #63
- PHPAY-64: wip(subscription): subscription with asaas by @mariolucasdev in #66
- PHPAY-01: fix(examples): namespace by @evandrosystems in #67
- PHPAY-69: fix(asaas): customer filters by @evandrosystems in #69
Full Changelog: v1.0.0...v1.0.1