payxy / payxy-php-sdk
PHP SDK for the Payxy payment gateway: Hosted Checkout, payment verification and webhook verification.
Requires
- php: ^8.2
- ext-json: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-client-implementation: *
- psr/http-factory: ^1.0
- psr/http-factory-implementation: *
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^2.0 || ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- guzzlehttp/guzzle: ^7.9
- nyholm/psr7: ^1.8
- orchestra/testbench: ^9.0 || ^10.0
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.5
- symfony/config: ^6.4 || ^7.0 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.0 || ^8.0
- symfony/http-client: ^6.4 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
Suggests
- guzzlehttp/guzzle: Default HTTP client; the SDK configures timeouts and TLS verification for it automatically.
- symfony/http-client: Alternative HTTP client; also configured automatically.
Provides
None
Conflicts
- laravel/framework: <11.0
- symfony/http-kernel: <6.4
Replaces
None
This package is auto-updated.
Last update: 2026-10-19 21:48:26 UTC
README
A PHP SDK for the Payxy payment gateway, aligned with the current Payxy Core service. It covers Hosted Checkout (initialize a payment and redirect the customer to Payxy), payment verification, and webhook verification, with Laravel and Symfony integrations.
Card details are collected by Payxy Hosted Checkout. They should not pass through the merchant's PHP application. Direct card, PIN and OTP handling is intentionally not part of this SDK; see docs/direct-api.md.
What's included
| Included | Not included (yet) |
|---|---|
Initialize a payment → paymentUrl + accessCode |
MakePayment / ValidatePayment / direct charging |
| Hosted Checkout redirect, plus an optional Pay-button / checkout-page helper | Collecting PAN, CVV, PIN or OTP |
| VerifyPayment with the full financial breakdown | SignalR realtime client (browser concern) |
| Webhook signature verification and event parsing | |
| Exact money handling (no floats) | |
| Test/live public-key auth; mode-specific webhook secrets | |
| Safe error handling and bounded retries | |
| Laravel service provider + facade, Symfony bundle |
Requirements
- PHP 8.2+ (tested on 8.2 and 8.5),
ext-json - A PSR-18 HTTP client. Guzzle (bundled with Laravel) or
symfony/http-clientis detected and hardened automatically.
composer require payxy/payxy-php-sdk guzzlehttp/guzzle
The package will be installed as payxy/payxy-php-sdk once its public Packagist listing is available. See Status.
Quick start
1. Configure
PAYXY_PUBLIC_KEY=pk_test_xxx # API authentication; pk_live_ for production PAYXY_WEBHOOK_SECRET=sk_test_xxx # webhook signatures only; never sent to Payxy
use Payxy\Payxy; use Payxy\PayxyConfig; $payxy = new Payxy(new PayxyConfig( publicKey: $_ENV['PAYXY_PUBLIC_KEY'], webhookSecret: $_ENV['PAYXY_WEBHOOK_SECRET'] ?? null, ));
| Credential | Purpose |
|---|---|
pk_test_… / pk_live_… |
Authenticate test / production API requests (Authorization: Bearer pk_…) |
sk_test_… / sk_live_… |
Verify signatures on test / production webhooks. Not accepted as API credentials. |
The mode comes from the public-key prefix (pk_test_ = Test, pk_live_ = Production). An sk_ key passed as
the public key is rejected. → Authentication
2. Initialize and redirect
use Payxy\Payments\Request\InitializePaymentRequest; use Payxy\Support\{Amount, Reference}; $transactionId = Reference::generate('ORD'); // ≤ 30 chars, CSPRNG $_SESSION['payxy_transaction_id'] = $transactionId; // persist before redirecting $response = $payxy->payments()->initialize(new InitializePaymentRequest( email: 'customer@example.com', amount: Amount::fromString('75000.00'), // order amount, before Payxy charges transactionId: $transactionId, currency: 'NGN', callbackUrl: 'https://merchant.example.com/payments/payxy/return', )); header('Location: ' . $response->getPaymentUrl()); exit;
→ Payment initialization · Hosted Checkout
3. Verify on the callback
The callback URL is a browser return URL, not proof of payment.
$verification = $payxy->payments()->verify($_SESSION['payxy_transaction_id']); // your id, not the query string if ($verification->isPaid() && $verification->amountMatches(Amount::fromString('75000.00'), 'NGN')) { $order->markAsPaid(); // isPaid() = responseCode "00" && data.isSuccessful === true }
Pending (05) and failed (02) transactions come back as results (isPending(), getStatus()), not exceptions.
→ Payment verification
4. Handle webhooks
use Payxy\Exception\WebhookVerificationException; try { $event = $payxy->webhooks()->constructEventFromGlobals(); // HMAC-SHA512, 300 s window, rotation-aware } catch (WebhookVerificationException) { http_response_code(401); exit; } // Deduplicate on $event->idempotencyKey() ("payment.success:ORD-…"), then: if ($event->isPaymentSuccess()) { /* confirm + fulfil once */ } http_response_code(200);
→ Webhooks
Optional: the checkout helper class
To render a Pay button or a full checkout page, extend one class with your settings:
final class ShopCheckout extends \Payxy\Checkout\PayxyCheckout { protected string $callbackUrl = 'https://merchant.example.com/payments/payxy/return'; protected ?string $buttonColor = '#0066FF'; protected function expectedAmount(string $transactionId): ?\Payxy\Support\Amount { return Order::findByPayxyId($transactionId)?->amount; } } $session = $checkout->start(new CheckoutRequest(email: $email, amount: $amount)); // Initialize $_SESSION['payxy_transaction_id'] = $session->transactionId; echo $checkout->renderPage($session); // or ->renderButton() // callback: $result = $checkout->confirm($_SESSION['payxy_transaction_id']); // Successful / Pending / Failed / AmountMismatch
Redirect display is supported. Popup display (Payxy's browser overlay) is experimental: it requires an HTTPS page, and its framing contract isn't confirmed. → Hosted Checkout
Documentation
| Guide | Covers |
|---|---|
| Authentication | pk_ / sk_ keys, configuration, mode selection, base URLs |
| Hosted Checkout | End-to-end flow, the PayxyCheckout helper, callbacks, SPAs and mobile |
| Payment initialization | Request contract, validation, responses, duplicate IDs, ambiguous failures |
| Payment verification | 00 / 05 / 02 / 04, financial fields, settlement, amount comparison, GetPaymentInfo |
| Webhooks | Events, PascalCase payloads, signatures, idempotent processing |
| IP allowlisting | Mode-specific allowlists, what to allowlist |
| Errors and retries | Exception hierarchy, response codes, retry policy, unknown outcomes |
| Security and PCI | Card data stays on Payxy, log redaction, threat model |
| Laravel · Symfony | Framework setup, callback and webhook routes |
| Direct API | Why direct payment isn't part of the public SDK |
Status: confirmed vs pending
Confirmed from Payxy Core (and exercised live against the sandbox where possible):
- Public keys authenticate Initialize and VerifyPayment; secret keys sign webhooks; the mode comes from the public key.
- Initialize: transaction IDs are 6–30 characters; amounts are decimal strings with ≤ 2 decimals;
modeis deprecated. - VerifyPayment returns the financial breakdown and reports state in
responseCode(00/05/02), with04for an unknown ID, all over HTTP 200. - GetPaymentInfo returns separate checkout and realtime tokens.
- Webhooks:
payment.success/payment.failed, HMAC-SHA512 overtimestamp.rawBody, PascalCase JSON, dual signatures during the 24-hour rotation grace period. - IP allowlisting is mode-specific. MakePayment takes an encrypted
payloadwrapper (hence no direct payment here). - The API domains are
https://api.payxy.ngfor Test andhttps://api.payxy.africafor Production; hosted checkout uses the correspondingcheckoutsubdomains.
Not yet guaranteed (the SDK doesn't depend on these, or marks them as experimental):
- Hosted Checkout redirect query parameters (use the ID from your session).
- iframe/popup support and the hosted page's
postMessageformat (popup display is experimental). - Webhook retry timing as an SLA.
- Packagist availability until the public package submission is completed.
License and security
The Payxy PHP SDK is released under the MIT License. The bundled browser checkout component and its licenses are documented in Third-Party Notices.
Please report suspected vulnerabilities privately as described in the Security Policy. Do not open a public issue containing credentials, payment data, customer information, or vulnerability details.
Development
No local PHP is needed. Docker runs the full gate (code style, PHPStan at max level, PHPUnit on each PHP version):
sh docker-check.sh
PHP_VERSIONS="8.2 8.5" sh docker-check.sh
With PHP installed: composer install && composer check.
Live sandbox smoke test (Initialize → GetPaymentInfo → Verify):
PAYXY_PUBLIC_KEY=pk_test_xxx php examples/live-smoke-test.php
PAYXY_PUBLIC_KEY=pk_test_xxx PAYXY_TRANSACTION_ID=ORD-… php examples/live-smoke-test.php # verify only
Demo shop (product → checkout page → Payxy → callback/re-check page):
echo "PAYXY_PUBLIC_KEY=pk_test_xxx" > examples/demo/.env # gitignored php -S localhost:8080 examples/demo/index.php
Layout
src/
├── Payxy.php, PayxyConfig.php, Environment.php, ResponseCode.php
├── Auth/ PublicKey (pk_), WebhookSecret (sk_): validated, masked, unserializable
├── Http/ Transport (the one request pipeline), Endpoint, RetryPolicy, hardened client factory
├── Payments/ PaymentsService (initialize, verify, getPaymentInfo), requests, responses, SettlementStatus
├── Webhook/ WebhookVerifier (verify / constructEvent), WebhookEvent
├── Checkout/ PayxyCheckout helper, sessions, outcomes, HTML renderer
├── Exception/ Exception hierarchy
├── Support/ Amount, Reference, Assert, ArrayReader, Payload
└── Bridge/ Laravel and Symfony integrations
Adding an endpoint takes a request DTO, a response class, and one $this->request(Endpoint::…, Response::class, $dto)
call. Auth, responseCode handling, retries, error mapping and logging all come from Transport.