Looking to hire Laravel developers? Try LaraJobs

laravel-pawapay maintained by pawapay

Description
Laravel package for the PawaPay mobile money payment gateway.
Last update
2026/06/11 16:19 (dev-main)
License
Links
Downloads
0

Comments
comments powered by Disqus

Laravel PawaPay

Latest Version on Packagist PHP Version Laravel Version License

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


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.