laravel-pawapay maintained by pawapay
Laravel PawaPay
A type-safe Laravel package for the PawaPay mobile money payment gateway — enabling deposits, payouts, refunds, and payment pages across African mobile money providers.
Requirements
- PHP ^8.3
- Laravel 13.x
- A PawaPay API token — get one from the PawaPay Dashboard
Installation
composer require pawapay/laravel-pawapay
The service provider and PawaPay facade alias are registered automatically via Laravel's package auto-discovery.
Publish the configuration file:
php artisan vendor:publish --tag=pawapay-config
Configuration
Add the following to your .env file:
PAWAPAY_TOKEN=your-api-token
PAWAPAY_SANDBOX=true
PAWAPAY_TIMEOUT=30
PAWAPAY_WEBHOOK_PATH=/webhooks/pawapay
PAWAPAY_WEBHOOK_SECRET=your-webhook-secret
| Key | Default | Description |
|---|---|---|
PAWAPAY_TOKEN |
— | Your PawaPay API bearer token (required) |
PAWAPAY_SANDBOX |
true |
Use the sandbox API. Set to false for production |
PAWAPAY_TIMEOUT |
30 |
HTTP request timeout in seconds |
PAWAPAY_WEBHOOK_PATH |
/webhooks/pawapay |
URL path where PawaPay sends webhook callbacks |
PAWAPAY_WEBHOOK_SECRET |
— | Optional secret for webhook signature verification |
Usage
All methods are available via the PawaPay facade or by injecting PawaPay\Contracts\PawaPayClientInterface.
Deposits
Initiate a deposit (customer pays into your account):
use PawaPay\Data\DepositData;
use PawaPay\Facades\PawaPay;
$data = new DepositData(
depositId: (string) Str::uuid(), // unique ID you generate
phoneNumber: '260971234567', // customer's MSISDN
provider: 'MTN_MOMO_ZMB', // mobile money provider code
amount: '100.00',
currency: 'ZMW',
customerMessage: 'Payment for order #1234', // optional
clientReferenceId: 'order-1234', // optional, your internal reference
metadata: [ // optional
['fieldName' => 'orderId', 'fieldValue' => '1234'],
['fieldName' => 'email', 'fieldValue' => 'user@example.com', 'isPII' => true],
],
);
$response = PawaPay::initiateDeposit($data);
if ($response->isAccepted()) {
// Transaction accepted — poll for final status
$status = PawaPay::getDeposit($response->depositId);
if ($status->isCompleted()) {
// Payment confirmed
echo $status->providerTransactionId;
}
}
DepositData fields:
| Field | Type | Required | Description |
|---|---|---|---|
depositId |
string |
Yes | Unique identifier for this deposit (UUID recommended) |
phoneNumber |
string |
Yes | Customer's phone number (MSISDN format) |
provider |
string |
Yes | Provider code, e.g. MTN_MOMO_ZMB |
amount |
string |
Yes | Transaction amount |
currency |
string |
Yes | ISO 4217 currency code, e.g. ZMW |
customerMessage |
?string |
No | Message shown to the customer |
clientReferenceId |
?string |
No | Your internal reference ID |
preAuthorisationCode |
?string |
No | Pre-authorisation code, if applicable |
metadata |
array |
No | Custom metadata fields (see Metadata) |
Available methods:
PawaPay::initiateDeposit(DepositData $data): DepositResponse
PawaPay::getDeposit(string $depositId): DepositResponse
PawaPay::resendDepositCallback(string $depositId): array
Payouts
Send money out to a recipient:
use PawaPay\Data\PayoutData;
use PawaPay\Facades\PawaPay;
$data = new PayoutData(
payoutId: (string) Str::uuid(),
phoneNumber: '260971234567',
provider: 'MTN_MOMO_ZMB',
amount: '50.00',
currency: 'ZMW',
customerMessage: 'Your withdrawal', // optional
clientReferenceId: 'withdrawal-567', // optional
);
$response = PawaPay::initiatePayout($data);
if ($response->isAccepted()) {
$status = PawaPay::getPayout($response->payoutId);
if ($status->isCompleted()) {
echo 'Payout sent: ' . $status->providerTransactionId;
}
}
PayoutData fields:
| Field | Type | Required | Description |
|---|---|---|---|
payoutId |
string |
Yes | Unique identifier for this payout |
phoneNumber |
string |
Yes | Recipient's phone number (MSISDN format) |
provider |
string |
Yes | Provider code |
amount |
string |
Yes | Transaction amount |
currency |
string |
Yes | ISO 4217 currency code |
customerMessage |
?string |
No | Message shown to the recipient |
clientReferenceId |
?string |
No | Your internal reference ID |
metadata |
array |
No | Custom metadata fields |
Available methods:
PawaPay::initiatePayout(PayoutData $data): PayoutResponse
PawaPay::getPayout(string $payoutId): PayoutResponse
PawaPay::resendPayoutCallback(string $payoutId): array
Refunds
Refund a previously completed deposit:
use PawaPay\Data\RefundData;
use PawaPay\Facades\PawaPay;
$data = new RefundData(
refundId: (string) Str::uuid(),
depositId: 'f4401bd2-1568-4140-bf2d-eb77d2b2b639', // original deposit ID
amount: '100.00',
currency: 'ZMW',
clientReferenceId: 'refund-for-order-1234', // optional
);
$response = PawaPay::initiateRefund($data);
if ($response->isAccepted()) {
$status = PawaPay::getRefund($response->refundId);
if ($status->isCompleted()) {
echo 'Refund processed';
} elseif ($status->isFailed()) {
echo 'Refund failed: ' . $status->failureMessage;
}
}
RefundData fields:
| Field | Type | Required | Description |
|---|---|---|---|
refundId |
string |
Yes | Unique identifier for this refund |
depositId |
string |
Yes | The original deposit ID to refund |
amount |
string |
Yes | Amount to refund |
currency |
string |
Yes | ISO 4217 currency code |
clientReferenceId |
?string |
No | Your internal reference ID |
metadata |
array |
No | Custom metadata fields |
Available methods:
PawaPay::initiateRefund(RefundData $data): RefundResponse
PawaPay::getRefund(string $refundId): RefundResponse
Payment Pages
Create a hosted payment page where customers complete the payment in their browser:
use PawaPay\Data\PaymentPageData;
use PawaPay\Facades\PawaPay;
$data = new PaymentPageData(
depositId: (string) Str::uuid(),
returnUrl: 'https://yourapp.com/payment/return',
phoneNumber: '260971234567', // optional, pre-fills the form
amount: '100.00', // optional
currency: 'ZMW', // optional
country: 'ZMB', // optional, ISO 3166-1 alpha-3
reason: 'Order payment', // optional
language: 'en', // optional
customerMessage: 'Thank you', // optional
);
$page = PawaPay::createPaymentPage($data);
// $page contains the payment page URL and details from PawaPay
Available methods:
PawaPay::createPaymentPage(PaymentPageData $data): array
Active Configuration
Retrieve the providers available for your account, optionally filtered by country or operation type:
use PawaPay\Facades\PawaPay;
$config = PawaPay::getActiveConfiguration();
echo $config->companyName;
echo $config->signatureRequired ? 'Signatures required' : 'No signatures needed';
// Filter providers by country (ISO 3166-1 alpha-3)
$zambiaProviders = $config->providersForCountry('ZMB');
// Filter by operation type
$depositProviders = $config->providersForOperationType('DEPOSIT');
// Or pass filters directly to the API call
$config = PawaPay::getActiveConfiguration(country: 'ZMB', operationType: 'DEPOSIT');
Response Helper Methods
All transaction responses (DepositResponse, PayoutResponse, RefundResponse) share these helper methods:
$response->isAccepted() // status === ACCEPTED (transaction is being processed)
$response->isCompleted() // status === COMPLETED (transaction succeeded)
$response->isFailed() // status === FAILED or REJECTED
$response->isDuplicate() // status === DUPLICATE_IGNORED (same ID submitted twice)
When a transaction fails, additional details are available:
$response->failureCode; // FailureCode enum, e.g. FailureCode::InvalidPhoneNumber
$response->failureMessage; // Human-readable failure description
Metadata
All transaction data classes accept an optional metadata array for passing custom fields alongside a transaction. Fields marked isPII are treated as personally identifiable information by PawaPay.
$metadata = [
['fieldName' => 'orderId', 'fieldValue' => '1234'],
['fieldName' => 'customerEmail', 'fieldValue' => 'user@example.com', 'isPII' => true],
];
Webhooks
PawaPay sends real-time status updates to your application via HTTP callbacks. This package registers the webhook route automatically — no manual route registration is needed.
Default endpoint: POST /webhooks/pawapay
You can change the path via PAWAPAY_WEBHOOK_PATH in your .env.
Signature Verification
When PAWAPAY_WEBHOOK_SECRET is set, the package verifies the Content-Digest header of every incoming webhook request using SHA-256 or SHA-512. Requests that fail verification are rejected.
If no secret is configured, signature verification is skipped and all incoming requests to the webhook endpoint are accepted.
Events
When a webhook is received, the package dispatches a Laravel event based on the transaction type. Listen to these events to react to transaction status changes.
| Event | Properties | Triggered by |
|---|---|---|
PawaPay\Events\DepositStatusUpdated |
$depositId, $status, $payload |
Deposit callbacks |
PawaPay\Events\PayoutStatusUpdated |
$payoutId, $status, $payload |
Payout callbacks |
PawaPay\Events\RefundStatusUpdated |
$refundId, $status, $payload |
Refund callbacks |
The $status property is a TransactionStatus enum. The $payload array contains the full raw webhook body.
Register your listeners in AppServiceProvider or a dedicated EventServiceProvider:
use Illuminate\Support\Facades\Event;
use PawaPay\Events\DepositStatusUpdated;
use PawaPay\Enums\TransactionStatus;
Event::listen(DepositStatusUpdated::class, function (DepositStatusUpdated $event) {
if ($event->status === TransactionStatus::Completed) {
// Mark the order as paid
Order::where('deposit_id', $event->depositId)->update(['paid' => true]);
}
if ($event->status->isFinal() && $event->status !== TransactionStatus::Completed) {
// Handle failure — notify the customer, release reserved stock, etc.
}
});
Error Handling
| Exception | When thrown |
|---|---|
PawaPay\Exceptions\AuthenticationException |
HTTP 401 — invalid or missing API token |
PawaPay\Exceptions\ApiException |
Any other non-2xx API response |
PawaPay\Exceptions\PawaPayException |
Base class; also thrown on webhook signature failure |
All exceptions expose a $response property with the raw Illuminate\Http\Client\Response object for inspection.
use PawaPay\Exceptions\AuthenticationException;
use PawaPay\Exceptions\ApiException;
use PawaPay\Exceptions\PawaPayException;
use PawaPay\Facades\PawaPay;
try {
$response = PawaPay::initiateDeposit($data);
} catch (AuthenticationException $e) {
// Invalid token — check PAWAPAY_TOKEN in your .env
logger()->error('PawaPay auth failed', ['body' => $e->response->body()]);
} catch (ApiException $e) {
// Non-2xx response from PawaPay API
logger()->error('PawaPay API error', [
'status' => $e->response->status(),
'body' => $e->response->body(),
]);
} catch (PawaPayException $e) {
// Catch-all for any other PawaPay error
logger()->error('PawaPay error: ' . $e->getMessage());
}
Enums Reference
TransactionStatus
| Case | Value | Description |
|---|---|---|
Accepted |
ACCEPTED |
Transaction received and accepted |
Enqueued |
ENQUEUED |
Queued for processing |
Processing |
PROCESSING |
Currently being processed |
InReconciliation |
IN_RECONCILIATION |
Under reconciliation |
Completed |
COMPLETED |
Successfully completed |
Failed |
FAILED |
Transaction failed |
Rejected |
REJECTED |
Rejected by the provider |
DuplicateIgnored |
DUPLICATE_IGNORED |
Duplicate transaction ID |
$status->isFinal(); // true for Completed, Failed, Rejected
$status->isSuccessful(); // true only for Completed
OperationType
| Case | Value |
|---|---|
Deposit |
DEPOSIT |
Payout |
PAYOUT |
Refund |
REFUND |
Remittance |
REMITTANCE |
PushDeposit |
PUSH_DEPOSIT |
NameLookup |
NAME_LOOKUP |
FailureCode
Authentication & Authorization
| Code | Description |
|---|---|
NO_AUTHENTICATION |
No authentication token provided |
AUTHENTICATION_ERROR |
Token is invalid or expired |
AUTHORISATION_ERROR |
Insufficient permissions |
HTTP_SIGNATURE_ERROR |
HTTP signature verification failed |
Feature Restrictions
| Code | Description |
|---|---|
DEPOSITS_NOT_ALLOWED |
Deposits not enabled for this account |
PAYOUTS_NOT_ALLOWED |
Payouts not enabled for this account |
REFUNDS_NOT_ALLOWED |
Refunds not enabled for this account |
Input Validation
| Code | Description |
|---|---|
INVALID_INPUT |
General invalid input |
MISSING_PARAMETER |
Required parameter is missing |
UNSUPPORTED_PARAMETER |
Parameter is not supported |
INVALID_PARAMETER |
Parameter value is invalid |
DUPLICATE_METADATA_FIELD |
Duplicate field names in metadata |
Business Logic
| Code | Description |
|---|---|
INVALID_PHONE_NUMBER |
Phone number is not valid for this provider |
INVALID_AMOUNT |
Amount is invalid |
AMOUNT_OUT_OF_BOUNDS |
Amount exceeds provider limits |
INVALID_CURRENCY |
Currency is not supported |
INVALID_PROVIDER |
Provider code is unrecognised |
PROVIDER_TEMPORARILY_UNAVAILABLE |
Provider is down or unreachable |
PAWAPAY_WALLET_OUT_OF_FUNDS |
Insufficient funds in your PawaPay wallet |
NOT_FOUND |
Transaction ID not found |
INVALID_STATE |
Transaction is not in a state that allows this operation |
System
| Code | Description |
|---|---|
UNKNOWN_ERROR |
Unexpected error on PawaPay's side |
Testing
The package uses Laravel's Http::fake() for testing — no real API calls are made. See tests/Feature/ for complete examples covering deposits, payouts, refunds, active configuration, and webhook handling.
License
The MIT License (MIT).
MIT License
Copyright (c) 2026 PawaPay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.