xfix / xfix-pay
Unified PHP SDK for XfixPay - M-Pesa, PayPal, Banks, XfixPay
Requires
- php: ^7.4 || ^8.0
- ext-json: *
- ext-openssl: *
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- phpunit/phpunit: ^9.0
- vlucas/phpdotenv: ^5.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A unified PHP SDK for integrating with multiple payment gateways through a consistent, extensible API.
XfixPay currently supports M-Pesa in Kenya and Pesapal payment gateway, with additional payment providers planned, including PayPal, Stripe, bank transfers, and other payment services.
Features
- Unified interface for multiple payment gateways
- M-Pesa STK Push
- STK Push status queries
- C2B URL registration
- B2C payments
- Account balance inquiries
- Transaction reversals
- Automatic M-Pesa OAuth token management and caching
- Automatic encryption of Safaricom security credentials
- Pesapal payment gateway integration (API 3.0)
- Pesapal IPN registration, order submission, status, refunds, and cancellations
- Defensive Pesapal status verification (handles error envelopes and null inner fields)
- PSR-4 autoloading
- PSR-12 compatible codebase
- Extensible gateway architecture
Requirements
- PHP 7.4 or higher
- Composer
- GuzzleHTTP 7.x or higher
- OpenSSL PHP extension
- Safaricom Developer account for M-Pesa integrations
- Safaricom M-Pesa security certificate (
.cer) for B2C, Balance Inquiry, and Reversal APIs - Pesapal merchant account for Pesapal integrations
Installation
Install XfixPay using Composer:
composer require xfix/xfixpay
Composer will automatically install the required dependencies.
Configuration
XfixPay uses a shared client configuration while allowing each gateway to define its own credentials and settings.
Global Configuration
use Xfix\XfixPay\XfixPayClient; $client = new XfixPayClient([ 'environment' => 'sandbox', // sandbox or live 'timeout' => 30, ]);
The global configuration can be shared across gateways.
M-Pesa Configuration
$mpesa = $client->mpesa([ 'consumer_key' => 'your_consumer_key', 'consumer_secret' => 'your_consumer_secret', 'passkey' => 'your_passkey', 'shortcode' => '174379', 'initiator' => 'apitest', 'initiator_password' => 'your_initiator_password', 'environment' => 'sandbox', 'cert_path' => '/path/to/safaricom.cer', ]);
Where:
| Configuration | Description |
|---|---|
consumer_key |
Safaricom Daraja consumer key |
consumer_secret |
Safaricom Daraja consumer secret |
passkey |
Lipa Na M-Pesa Online passkey |
shortcode |
PayBill or Till number |
initiator |
Initiator name for B2C, Balance, and Reversal |
initiator_password |
Initiator password |
environment |
sandbox or live |
cert_path |
Path to the Safaricom.cer certificate |
The cert_path is required only for:
- B2C payments
- Account Balance Inquiry
- Transaction Reversal
The certificate can also be configured after creating the gateway:
$mpesa->setCertificatePath('/path/to/safaricom.cer');
Pesapal Configuration
$pesapal = $client->pesapal([ 'consumer_key' => 'your_pesapal_consumer_key', 'consumer_secret' => 'your_pesapal_consumer_secret', 'environment' => 'sandbox', ]);
Where:
| Configuration | Description |
|---|---|
consumer_key |
Pesapal API 3.0 consumer key |
consumer_secret |
Pesapal API 3.0 consumer secret |
environment |
sandbox or live |
The Pesapal gateway automatically selects the correct API base URL based on the environment:
- Sandbox —
https://cybqa.pesapal.com/pesapalv3/api - Live —
https://pay.pesapal.com/v3/api
M-Pesa Usage
1. STK Push
Initiate a Lipa Na M-Pesa Online payment request.
$response = $mpesa->requestSTKPush( phoneNumber: '254722336262', amount: 1.00, accountReference: 'INV-001', transactionDesc: 'Payment', callbackUrl: 'https://your-server.com/mpesa/callback' );
The response contains the M-Pesa checkout request identifier.
$checkoutRequestId = $response['CheckoutRequestID'];
Store the CheckoutRequestID if you need to query the transaction status later.
Important Parameters
phoneNumber— Customer MSISDN in international formatamount— Payment amountaccountReference— Account or invoice referencetransactionDesc— Transaction descriptioncallbackUrl— URL that receives the STK callback
2. Query STK Push Status
Check the status of an STK Push transaction using its CheckoutRequestID.
$status = $mpesa->querySTKStatus($checkoutRequestId); if ($status['ResultCode'] === '0') { // Payment successful }
The returned response should be evaluated according to the M-Pesa API response codes.
3. Register C2B URLs
Register the validation and confirmation URLs used for Customer-to-Business payments.
$mpesa->registerC2BUrls( confirmationUrl: 'https://your-server.com/mpesa/confirm', validationUrl: 'https://your-server.com/mpesa/validate', responseType: 'Completed' );
Supported response types include:
'Completed'
or:
'Canceled'
The validation endpoint can be used to validate incoming transactions before confirmation.
4. B2C Payment
Send money from a business M-Pesa account to a customer's M-Pesa account.
$response = $mpesa->sendB2C( phoneNumber: '254722336262', amount: 100.00, command: 'SalaryPayment', remarks: 'Salary for July', occasion: 'Monthly salary' );
Supported commands include:
'SalaryPayment'
'BusinessPayment'
'PromotionPayment'
The occasion parameter is optional.
5. Account Balance Inquiry
Retrieve the balance of an M-Pesa shortcode.
$balance = $mpesa->balanceInquiry( partyA: null, identifierType: '4' );
When partyA is null, the configured shortcode is used.
Supported identifier types include:
| Type | Description |
|---|---|
1 |
MSISDN |
2 |
Till Number |
4 |
PayBill |
6. Transaction Reversal
Reverse a completed M-Pesa transaction using its M-Pesa receipt number.
$reversal = $mpesa->reversal( transactionId: 'LKXXXX1234', amount: 100.00, remarks: 'Reversal due to error' );
Use transaction reversals carefully and only when the transaction meets Safaricom's reversal requirements.
Pesapal Usage
1. Register IPN URL
Before submitting any order, you must register an IPN (Instant Payment Notification) URL. The returned ipn_id is required when submitting an order via submitOrderRequest().
$response = $pesapal->registerIPN('https://your-server.com/ipn-handler'); $ipnId = $response['ipn_id'] ?? null; if (!$ipnId) { throw new \RuntimeException('Failed to obtain Pesapal IPN ID.'); }
Important Parameters
ipnUrl— Callback URL that receives payment status notificationsipnId— Returned identifier; store this and pass it asnotificationIdwhen submitting an order
2. Submit Order
Initiate a payment request and get a redirect URL.
Note: The legacy
submitOrder()method is deprecated. It throws aPesapalExceptionand must not be used. UsesubmitOrderRequest()instead, which requires amerchantReferenceand anotificationId.
$response = $pesapal->submitOrderRequest( merchantReference: 'INV-' . time(), // Must be unique per order amount: 100.00, currency: 'KES', description: 'Order #123', callbackUrl: 'https://your-server.com/callback', notificationId: $ipnId, // Required — obtained from registerIPN() billingAddress: [ 'email_address' => 'customer@example.com', 'phone_number' => '254722336262', 'country_code' => 'KE', ] ); $redirectUrl = $response['redirect_url'] ?? null; $orderTrackingId = $response['order_tracking_id'] ?? null;
Store the orderTrackingId if you need to query the transaction status later, and redirect the customer to the redirect_url to complete payment.
Important Parameters
merchantReference— Unique merchant reference for the order (required)amount— Payment amountcurrency— Currency code (e.g., KES)description— Order descriptioncallbackUrl— URL called after paymentnotificationId— IPN ID fromregisterIPN()(required)billingAddress— Optional structured billing address array (email_address,phone_number,country_code)redirectUrl— Payment redirect URL from Pesapal (in response)orderTrackingId— Order tracking ID for status queries (in response)
3. Check Transaction Status
Verify the outcome of a payment using the order_tracking_id returned by submitOrderRequest() (or the transaction_id from your callback/IPN payload).
Pesapal may respond with an error envelope (error.message, error.code, error.error_type) whose inner fields are null, or with a success envelope that still lacks a usable status. Both cases must be treated as failures, so the check should be defensive rather than assuming a top-level status key exists.
use Xfix\XfixPay\XfixPayClient; use Illuminate\Support\Facades\Log; // or your logger of choice $client = new XfixPayClient(); $pesapal = $client->pesapal($configuration); $verification = $pesapal->getTransactionStatus($parsed['transaction_id']); Log::debug('Verification response', $verification); // Distinguish "Pesapal returned an actual error" from // "response has an error envelope with null inner fields". $errorMessage = $verification['error']['message'] ?? null; $errorCode = $verification['error']['code'] ?? null; $errorType = $verification['error']['error_type'] ?? null; $hasError = $errorMessage || $errorCode || $errorType; // Status must be present and non-empty. if ($hasError || empty($verification['payment_status_description'])) { throw new \Exception( 'Pesapal status verification failed: ' . ($errorMessage ?: $verification['message'] ?? 'Unknown error') ); } // At this point the payload is well-formed and carries a usable status. if ($verification['payment_status_description'] === 'Completed') { // Mark the order as paid, fulfil, etc. }
Important Parameters
orderTrackingId— Order tracking ID from thesubmitOrderRequest()response (or thetransaction_idsupplied in the IPN/callback payload)
Response Shape Notes
- Success envelope — contains
payment_status_description(e.g.Completed,Pending,Failed). This is the authoritative status field; do not rely on astatuskey. - Error envelope — contains an
errorobject withmessage,code, anderror_type. Any of these being non-null indicates a failure, even if the inner fields are otherwisenull(e.g."error": {"message": null, "code": null, "error_type": null}should still be treated as an error when the surrounding response indicates a failure). - The defensive check above throws a single, descriptive exception that surfaces whichever error message is actually present, falling back to the top-level
messageand finally to'Unknown error'.
Why This Pattern Matters
The naive check if ($status['status'] === 'completed') is unsafe with Pesapal API 3.0 because:
- The status field is
payment_status_description, notstatus. - A response can arrive with an
errorenvelope whose fields are allnull— the presence of theerrorkey itself is meaningful, and the inner values must not be dereferenced blindly. - When the inner status is absent, the call should fail loudly rather than silently skip fulfilment logic.
4. Request Refund
Request a refund for a completed payment.
$refund = $pesapal->requestRefund($orderTrackingId, 100.00, 'Customer requested refund');
Important Parameters
orderTrackingId— Order tracking IDamount— Refund amount (0 for full refund)remarks— Refund remarks/message
5. Cancel Order
Cancel a pending or failed order.
$cancel = $pesapal->cancelOrder($orderTrackingId, 'Order cancelled by customer');
Important Parameters
orderTrackingId— Order tracking IDremarks— Cancellation reason
Security Certificate
Safaricom requires the initiator password used by the B2C, Balance Inquiry, and Reversal APIs to be encrypted using Safaricom's public certificate.
The certificate is normally provided as a .cer file.
Example:
$mpesa->setCertificatePath('/path/to/safaricom.cer');
XfixPay handles the following automatically:
- Loads the Safaricom certificate.
- Encrypts the initiator password.
- Reuses the encrypted credential for subsequent requests within the gateway instance.
The SDK does not bundle Safaricom certificates.
If the certificate path is missing, invalid, or unreadable, XfixPay throws an MpesaException with a descriptive error message.
Error Handling
M-Pesa operations may throw MpesaException for errors such as:
- Network failures
- Authentication failures
- Invalid configuration
- Missing certificates
- Invalid API responses
- M-Pesa API errors
Pesapal operations may throw PesapalException for errors such as:
- Authentication failures
- Invalid configuration
- Missing or invalid
notificationId - Calls to the deprecated
submitOrder()method - Invalid API responses
- Pesapal API errors
Handle exceptions using try/catch:
use Xfix\XfixPay\Exception\MpesaException; use Xfix\XfixPay\Exception\PesapalException; try { $response = $mpesa->requestSTKPush( phoneNumber: '254722336262', amount: 1.00, accountReference: 'INV-001', transactionDesc: 'Payment', callbackUrl: 'https://your-server.com/mpesa/callback' ); } catch (MpesaException $e) { // Log the exception and handle the failure gracefully. } try { $order = $pesapal->submitOrderRequest( merchantReference: 'INV-' . time(), amount: 100.00, currency: 'KES', description: 'Order #123', callbackUrl: 'https://your-server.com/callback', notificationId: $ipnId ); } catch (PesapalException $e) { // Log the exception and handle the failure gracefully. }
Because getTransactionStatus() may return a well-formed error envelope rather than throwing, status verification should be wrapped in an explicit defensive check after the call — see Pesapal Usage → 3. Check Transaction Status.
Applications should log relevant error information without exposing credentials or other sensitive data.
Testing
Run the PHPUnit test suite with:
vendor/bin/phpunit
The SDK is designed to support HTTP client mocking, allowing unit tests to run without making real requests to payment providers.
Integration tests can be used separately for environments where real API credentials are available.
Gateway Architecture
XfixPay is designed around an extensible gateway architecture.
Each payment provider is implemented as an independent gateway while sharing common infrastructure through the main client.
Example:
$client = new XfixPayClient([ 'environment' => 'sandbox', ]); $mpesa = $client->mpesa($mpesaConfig); $pesapal = $client->pesapal($pesapalConfig);
Future gateways will follow the same pattern:
$paypal = $client->paypal($paypalConfig); $stripe = $client->stripe($stripeConfig); $bank = $client->bank($bankConfig);
This allows applications to integrate multiple payment providers without having to learn a completely different SDK structure for each provider.
Planned Gateways
The following gateways are planned for future releases:
- PayPal — REST API
- Stripe — Stripe API / Connect
- Bank Transfers — Equity, KCB, and other supported banks
- XfixPay — Digital-asset payments
Gateway availability depends on implementation status and the relevant provider's API requirements.
Project Structure
xfixpay/
├── composer.json
├── README.md
├── LICENSE
├── .gitignore
│
├── src/
│ ├── XfixPayClient.php
│ ├── GatewayInterface.php
│ ├── AbstractGateway.php
│ │
│ ├── Exception/
│ │ ├── XfixPayException.php
│ │ ├── MpesaException.php
│ │ ├── PesapalException.php
│ │ ├── PayPalException.php
│ │ ├── BankException.php
│ │ └── StripeException.php
│ │
│ ├── Mpesa/
│ │ ├── MpesaGateway.php
│ │ ├── MpesaConfig.php
│ │ └── MpesaAuth.php
│ │
│ ├── Pesapal/
│ │ ├── PesapalGateway.php
│ │ ├── PesapalConfig.php
│ │ └── PesapalAuth.php
│ │
│ ├── PayPal/
│ │ ├── PayPalGateway.php
│ │ ├── PayPalConfig.php
│ │ └── PayPalAuth.php
│ │
│ ├── Bank/
│ │ ├── BankGateway.php
│ │ └── BankConfig.php
│ │
│ └── Stripe/
│ └── ...
│
├── tests/
│ ├── Unit/
│ │ ├── MpesaGatewayTest.php
│ │ ├── MpesaAuthTest.php
│ │ ├── PesapalGatewayTest.php
│ │ └── XfixPayClientTest.php
│ └── Integration/
│ └── MpesaLiveTest.php
│
├── examples/
│ ├── mpesa_stk_push.php
│ ├── mpesa_b2c.php
│ ├── mpesa_balance.php
│ ├── mpesa_reversal.php
│ └── pesapal_checkout.php
│
└── docs/
├── mpesa.md
├── pesapal.md
└── contributing.md
Key Components
| Component | Purpose |
|---|---|
src/XfixPayClient.php |
Main SDK entry point and gateway factory |
src/GatewayInterface.php |
Common contract for payment gateways |
src/AbstractGateway.php |
Shared gateway functionality |
src/Mpesa/MpesaGateway.php |
M-Pesa API implementation |
src/Mpesa/MpesaAuth.php |
M-Pesa OAuth authentication and token caching |
src/Mpesa/MpesaConfig.php |
M-Pesa configuration container |
src/Pesapal/PesapalGateway.php |
Pesapal API 3.0 implementation |
src/Pesapal/PesapalAuth.php |
Pesapal OAuth authentication and token caching |
src/Pesapal/PesapalConfig.php |
Pesapal configuration container |
src/Exception/MpesaException.php |
M-Pesa-specific exception |
src/Exception/PesapalException.php |
Pesapal-specific exception |
src/Mpesa/certs/ |
Optional certificate directory for local development |
examples/ |
Common integration examples |
tests/ |
Unit and integration tests |
docs/ |
Extended technical documentation |
Gateway Isolation
Each payment provider is kept within its own namespace and directory.
For example:
src/Mpesa/
src/Pesapal/
src/PayPal/
src/Stripe/
src/Bank/
This keeps provider-specific authentication, configuration, exceptions, and API implementations isolated from one another.
Certificate Handling
The SDK does not include payment-provider certificates.
For local development, users may optionally place their own certificates under:
src/Mpesa/certs/
However, production applications should preferably store certificates outside the source repository and reference them through configuration or environment variables.
For example:
'cert_path' => getenv('MPESA_CERT_PATH'),
Never commit private credentials, certificates, API secrets, or production configuration to source control.
Contributing
Contributions are welcome.
Before submitting a pull request:
- Follow PSR-12 coding standards.
- Add or update tests for changed functionality.
- Ensure the PHPUnit test suite passes.
- Keep gateway-specific logic isolated within its gateway namespace.
- Do not commit credentials, certificates, tokens, or other secrets.
Please open an issue before making substantial architectural changes.
License
XfixPay is released under the MIT License.
See the LICENSE file for the full license text.