katorymnd / pesapal-php-sdk
A PHP SDK for Pesapal API integration, enabling seamless payment handling in PHP applications.
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
Language:Hack
Requires
- php: >=8.0.0
- filp/whoops: ^2.16
- giggsey/libphonenumber-for-php: ^8.13
- guzzlehttp/guzzle: ^7.9
- monolog/monolog: ^3.7
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- mockery/mockery: ^1.6
- phpunit/phpunit: ^11.4
README
The Pesapal PHP SDK provides a simple and efficient way to integrate the Pesapal Payment Gateway into your PHP application. This SDK handles authentication, order submission, transaction status checks, recurring payments, and refunds, making it easier for developers to interact with Pesapal's API 3.0.
Table of Contents
Features
- Authentication: Obtain and manage access tokens using your Pesapal
consumer_key
andconsumer_secret
. - IPN Registration: Register Instant Payment Notification (IPN) URLs to receive real-time transaction status updates.
- Order Submission: Create and submit payment requests to Pesapal.
- Transaction Status: Check the status of transactions using the
OrderTrackingId
. - Recurring Payments: Set up and manage subscription-based payments.
- Refunds: Request refunds for completed transactions.
Requirements
- PHP 8.0 or higher
- Composer
- cURL extension enabled
- An active Pesapal merchant account with
consumer_key
andconsumer_secret
- A sandbox keys file for testing, provided in the project as
pesapal_sandbox_keys.json
. This file contains test keys for different merchant regions (e.g., Kenya, Uganda, Tanzania). You can find the file in the root directory of the project.
Installation
Use Composer to install the Pesapal PHP SDK:
composer require katorymnd/pesapal-php-sdk
Configuration
-
Clone the Repository (if not using Composer):
git clone https://github.com/katorymnd/pesapal-php-sdk.git
-
Install Dependencies:
Navigate to the project directory and install dependencies:
cd pesapal-php-sdk composer install
-
Set Up Environment Variables:
-
Copy the
.env.example
file to.env
:cp .env.example .env
-
Open the
.env
file and set your Pesapalconsumer_key
andconsumer_secret
:CONSUMER_KEY=your_consumer_key_here CONSUMER_SECRET=your_consumer_secret_here
-
-
Ensure
pesapal_dynamic.json
is Writable:The SDK uses
pesapal_dynamic.json
to store theaccess_token
and other dynamic data. Make sure this file is writable by your application.
Usage
Authentication
The SDK automatically handles authentication with Pesapal. Upon initialization, the SDK retrieves the access_token
using your configured consumer_key
and consumer_secret
. The token is valid for 5 minutes and is securely stored in pesapal_dynamic.json
.
You don't need to manually request or manage the access_token
; the SDK ensures it is always available and refreshed as needed for API calls.
How it Works:
- When you make an API call, the SDK checks for a valid
access_token
inpesapal_dynamic.json
. - If the token is expired or unavailable, the SDK automatically re-authenticates with Pesapal and updates
pesapal_dynamic.json
.
Important Notice: IPN Registration
Before initiating any transactions, it is mandatory to register an IPN (Instant Payment Notification) URL. This ensures that you receive real-time updates on transaction status changes, such as successful payments, rejections, or errors.
The Pesapal SDK requires a notification_id
to associate with every transaction, and this is only generated after the IPN URL is registered.
Key Points:
- Mandatory Step: You must register the IPN URL before any transaction to obtain the
notification_id
. - Storage: The
notification_id
is automatically stored inpesapal_dynamic.json
after successful registration. - Publicly Accessible URL: The IPN URL must be publicly accessible and should not be blocked by any server firewall rules.
- Whitelisting Pesapal: If your server has strict rules, ensure to whitelist calls from the Pesapal domain (
pesapal.com
).
Proceed to Register Your IPN URL Below:
Register IPN URL
Registering an IPN (Instant Payment Notification) URL is a crucial step to receive real-time updates on transaction status changes, such as payment completion, rejection, or errors.
To understand how to implement this, refer to the example file provided:
example/register_ipn.php
.
Illustration: IPN Registration Process
- File Location:
example/register_ipn.php
- What It Does: This script demonstrates how to register an IPN URL and retrieve the
notification_id
that is required for all subsequent transactions.
Note: Ensure your IPN URL is publicly accessible and correctly configured to handle incoming notifications from Pesapal.
Submit Order
- File Location:
example/submit_order.php
To create and submit a payment request to Pesapal, you need to prepare order data that includes billing details, transaction description, and callback URLs. The SDK ensures all required fields are formatted correctly, including validating phone numbers and generating the necessary access_token
.
Note: A valid notification_id
must be registered before submitting the payment request. Let's hope you already done that. Refer to the Register IPN URL section.
Example Usage:
The following example demonstrates how to prepare and submit a payment request:
// Prepare order data $orderData = [ "id" => $merchantReference, // Merchant's unique reference for the transaction "currency" => $data['currency'], // Currency for the payment "amount" => (float) $data['amount'], // Transaction amount "description" => $data['description'], // Description of the payment "callback_url" => "https://www.example.com/payment-callback", // Your callback URL "notification_id" => $notificationId, // Notification ID obtained from IPN registration "branch" => "Katorymnd Freelancer", // Optional branch identifier "payment_method" => "card", // Restrict payment option to CARD only "billing_address" => [] ]; // Map billing details $billingDetails = $data['billing_details']; $countryCode = isset($billingDetails['country']) ? strtoupper($billingDetails['country']) : ''; $orderData['billing_address'] = array_merge($orderData['billing_address'], [ "country_code" => $countryCode, "first_name" => $billingDetails['first_name'] ?? '', "middle_name" => '', // Optional "last_name" => $billingDetails['last_name'] ?? '', "line_1" => $billingDetails['address_line1'] ?? '', "line_2" => $billingDetails['address_line2'] ?? '', "city" => $billingDetails['city'] ?? '', "state" => $billingDetails['state'] ?? '', "postal_code" => $billingDetails['postal_code'] ?? '' ]); // Include contact information if (!empty($data['email_address'])) { $orderData['billing_address']['email_address'] = $data['email_address']; } if (!empty($data['phone_number'])) { // Format phone number using libphonenumber $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance(); try { $numberProto = $phoneUtil->parse($data['phone_number'], null); $nationalNumber = $phoneUtil->format($numberProto, \libphonenumber\PhoneNumberFormat::NATIONAL); $orderData['billing_address']['phone_number'] = preg_replace('/[\s()-]/', '', $nationalNumber); } catch (\libphonenumber\NumberParseException $e) { throw new PesapalException('Invalid phone number format: ' . $e->getMessage()); } } // Obtain access token $accessToken = $clientApi->getAccessToken(); if (!$accessToken) { throw new PesapalException('Failed to obtain access token'); } // Submit order request $response = $clientApi->submitOrderRequest($orderData); // Redirect the customer to the payment page header('Location: ' . $response['redirect_url']); exit();
Get Transaction Status
After the transaction is completed, use the OrderTrackingId
to check the transaction status.
$orderTrackingId = $_GET['OrderTrackingId']; // Or retrieve from IPN data // Get transaction status $status = $pesapal->getTransactionStatus($orderTrackingId); echo 'Transaction Status: ' . $status['status'];
Recurring Payments
- File Location:
example/recurring_order.php
Set up subscription-based payments for recurring charges using Pesapal. The SDK supports specifying subscription details such as start and end dates and frequency. It also includes validations for subscription details and formatting for dates and billing information.
Example Usage:
The following example demonstrates how to prepare and submit a recurring payment request:
// Prepare order data $orderData = [ "id" => $merchantReference, // Unique merchant reference "currency" => $currency, // Payment currency "amount" => (float) $amount, // Payment amount "description" => $description, // Description of the subscription "callback_url" => "https://www.example.com/subscription-callback", // Callback URL "notification_id" => $notificationId, // Notification ID from IPN registration "billing_address" => [] ]; // Include billing and contact information if (!empty($emailAddress)) { $orderData['billing_address']['email_address'] = $emailAddress; } if (!empty($phoneNumber)) { $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance(); try { $numberProto = $phoneUtil->parse($phoneNumber, null); $nationalNumber = $phoneUtil->format($numberProto, \libphonenumber\PhoneNumberFormat::NATIONAL); $orderData['billing_address']['phone_number'] = preg_replace('/[\s()-]/', '', $nationalNumber); } catch (\libphonenumber\NumberParseException $e) { throw new PesapalException('Invalid phone number format: ' . $e->getMessage()); } } // Map additional billing details if (isset($data['billing_details'])) { $billingDetails = $data['billing_details']; $orderData['billing_address'] = array_merge($orderData['billing_address'], [ "country_code" => strtoupper($billingDetails['country'] ?? ''), "first_name" => $billingDetails['first_name'] ?? '', "middle_name" => $billingDetails['middle_name'] ?? '', "last_name" => $billingDetails['last_name'] ?? '', "line_1" => $billingDetails['address_line1'] ?? '', "line_2" => $billingDetails['address_line2'] ?? '', "city" => $billingDetails['city'] ?? '', "state" => $billingDetails['state'] ?? '', "postal_code" => $billingDetails['postal_code'] ?? '', "zip_code" => '' ]); } // Handle recurring payment details $isRecurring = isset($subscriptionDetails) && !empty($subscriptionDetails); if ($isRecurring) { if (!$accountNumber) { throw new PesapalException('Account number is required for recurring payments.'); } $requiredSubscriptionFields = ['start_date', 'end_date', 'frequency']; foreach ($requiredSubscriptionFields as $field) { if (empty($subscriptionDetails[$field])) { throw new PesapalException("The field '$field' is required in subscription details."); } } // Validate and format subscription dates $startDate = DateTime::createFromFormat('Y-m-d', $subscriptionDetails['start_date']); $endDate = DateTime::createFromFormat('Y-m-d', $subscriptionDetails['end_date']); if (!$startDate || !$endDate || $endDate <= $startDate) { throw new PesapalException('Invalid or inconsistent subscription date range.'); } $orderData['account_number'] = $accountNumber; $orderData['subscription_type'] = 'AUTO'; $orderData['subscription_details'] = [ 'start_date' => $startDate->format('d-m-Y'), // Format to DD-MM-YYYY 'end_date' => $endDate->format('d-m-Y'), // Format to DD-MM-YYYY 'frequency' => $subscriptionDetails['frequency'] ]; } // Obtain a valid access token $accessToken = $clientApi->getAccessToken(); if (!$accessToken) { throw new PesapalException('Failed to obtain access token'); } // Submit subscription order request $response = $clientApi->submitOrderRequest($orderData);
Refund Request
- File Location:
example/RefundTransactionHandler.php
Requesting a refund for a completed transaction is straightforward with the SDK. The SDK automatically checks the transaction status using the order_tracking_id
before submitting a refund request. If the transaction status is not COMPLETED
, the SDK throws an error to prevent invalid refund attempts.
How It Works:
- The SDK retrieves the transaction status using the provided
order_tracking_id
. - If the transaction is
COMPLETED
, the SDK prepares and submits the refund request automatically. - If the transaction is not
COMPLETED
, the SDK throws an error indicating that the refund request cannot proceed.
Example Usage:
// Provide the order tracking ID from the transaction $orderTrackingId = $data['order_tracking_id']; // Get the transaction status $response = $clientApi->getTransactionStatus($orderTrackingId); $responseData = []; // Initialize response data array if ($response['status'] === 200 && isset($response['response'])) { $transactionStatusData = $response['response']; // Map status_code to status_message $status_code = $transactionStatusData['status_code'] ?? null; $status_messages = [ 0 => 'INVALID', 1 => 'COMPLETED', 2 => 'FAILED', 3 => 'REVERSED', ]; $status_message = isset($status_messages[$status_code]) ? $status_messages[$status_code] : 'UNKNOWN STATUS'; // Log the transaction status $log->info('Transaction status retrieved successfully', [ 'order_tracking_id' => $orderTrackingId, 'status_code' => $status_code, 'status_message' => $status_message, ]); // Collect transaction status data $responseData['success'] = true; $responseData['transaction_status'] = [ 'payment_method' => $transactionStatusData['payment_method'] ?? null, 'amount' => $transactionStatusData['amount'] ?? null, 'created_date' => $transactionStatusData['created_date'] ?? null, 'confirmation_code' => $transactionStatusData['confirmation_code'] ?? null, 'order_tracking_id' => $transactionStatusData['order_tracking_id'] ?? null, 'payment_status_description' => $transactionStatusData['payment_status_description'] ?? null, 'description' => $transactionStatusData['description'] ?? null, 'message' => $transactionStatusData['message'] ?? null, 'payment_account' => $transactionStatusData['payment_account'] ?? null, 'call_back_url' => $transactionStatusData['call_back_url'] ?? null, 'status_code' => $status_code, 'status_message' => $status_message, 'merchant_reference' => $transactionStatusData['merchant_reference'] ?? null, 'account_number' => $transactionStatusData['account_number'] ?? null, 'payment_status_code' => $transactionStatusData['payment_status_code'] ?? null, 'currency' => $transactionStatusData['currency'] ?? null, 'error' => [ 'error_type' => $transactionStatusData['error']['error_type'] ?? null, 'code' => $transactionStatusData['error']['code'] ?? null, 'message' => $transactionStatusData['error']['message'] ?? null ] ]; // Extract confirmation code for refund $confirmationCode = $transactionStatusData['confirmation_code'] ?? null; if ($confirmationCode) { // Prepare refund data with user-provided values $refundData = [ 'confirmation_code' => $confirmationCode, 'amount' => $refundAmount, 'username' => $refundUsername, 'remarks' => $refundRemarks ]; try { // Request refund $refundResponse = $clientApi->requestRefund($refundData); if ($refundResponse['status'] === 200 && isset($refundResponse['response'])) { $refundDataResponse = $refundResponse['response']; // Log the refund response $log->info('Refund requested successfully', [ 'refund_data' => $refundData, 'refund_response' => $refundDataResponse, ]); // Add refund response to the output $responseData['refund_response'] = $refundDataResponse; } else { $errorMessage = $refundResponse['response']['error']['message'] ?? 'Unknown error occurred while requesting refund.'; $log->error('Refund request failed', [ 'error' => $errorMessage, 'refund_data' => $refundData, ]); throw new PesapalException($errorMessage); } } catch (PesapalException $e) { // Log the error $log->error('Error in requesting refund', [ 'error' => $e->getMessage(), 'details' => $e->getErrorDetails(), 'refund_data' => $refundData, ]); // Add the error to the response $responseData['refund_error'] = [ 'error' => $e->getMessage(), 'details' => $e->getErrorDetails(), ]; } } else { // No confirmation code, cannot proceed with refund $log->error('Confirmation code not available, cannot process refund.', [ 'order_tracking_id' => $orderTrackingId, ]); $responseData['refund_error'] = [ 'error' => 'Confirmation code not available, cannot process refund.', ]; } // Output the combined response echo json_encode($responseData); } else { $errorMessage = $response['response']['error']['message'] ?? 'Unknown error occurred while retrieving transaction status.'; $log->error('Transaction status retrieval failed', [ 'error' => $errorMessage, 'order_tracking_id' => $orderTrackingId ]); throw new PesapalException($errorMessage); }
Get Transaction Status
- File Location:
example/transaction_helpers.php
After a transaction is completed, you can check its status using the order_tracking_id
. This is useful to confirm whether a payment was successful, failed, or is still pending.
$orderTrackingId = $data['order_tracking_id']; // Obtain a valid access token $accessToken = $clientApi->getAccessToken(); if (!$accessToken) { throw new PesapalException('Failed to obtain access token'); } // Get the transaction status $response = $clientApi->getTransactionStatus($orderTrackingId);
Tutorials and Guides
Embark on your journey with the Pesapal PHP SDK through our exclusive tutorials and guides:
-
Installing Pesapal Sdk: Kickstart Your Payment Integration - Quickly install and set up the Pesapal PHP SDK to integrate secure payment processing into your PHP app with this essential beginner's guide.
-
Mastering Pesapal Sdk: Unlock Advanced Payment Features - Explore advanced features of the Pesapal PHP SDK: recurring payments, refunds, transaction queries, and more in this essential guide.
Testing Environment Setup
For testing and development purposes, utilize the test cards provided by Pesapal at the following URL:
https://cybqa.pesapal.com/PesapalIframe/PesapalIframe3/TestPayments
License
This project is licensed under the MIT License - see the LICENSE file for details.
Disclaimer: This SDK is not an official Pesapal product. It's an independent project aimed at simplifying Pesapal API integration for PHP developers. For any issues related to the Pesapal API itself, please contact Pesapal Support.