PHPackages                             nexuspay/payment-made-easy - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. nexuspay/payment-made-easy

ActiveLibrary[Payment Processing](/categories/payments)

nexuspay/payment-made-easy
==========================

A Laravel package for handling payments with multiple gateways (Paystack, Flutterwave, Stripe, Seerbit, etc.)

v1.0.2(1mo ago)055MITPHPPHP ^8.1CI failing

Since May 10Pushed 1mo agoCompare

[ Source](https://github.com/Alphabalex/payment-made-easy)[ Packagist](https://packagist.org/packages/nexuspay/payment-made-easy)[ RSS](/packages/nexuspay-payment-made-easy/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (6)Versions (4)Used By (0)

Payment Made Easy
=================

[](#payment-made-easy)

A comprehensive Laravel package for integrating multiple payment gateways with support for one-time payments, subscriptions/plans, disbursements/transfers, virtual accounts, and payment links — all behind a consistent interface.

Supported Gateways
------------------

[](#supported-gateways)

GatewayOne-TimeSubscriptionsDisbursementsVirtual AccountsPayment Links**Paystack**✅✅✅✅✅**Flutterwave**✅✅✅✅✅**Stripe**✅✅——✅**Seerbit**✅✅—✅—**Monnify**✅⚠️ n/a✅✅—**Squad**✅—✅✅—**Remita**✅—✅——**Budpay**✅—✅✅—**Interswitch**✅————**PayPal**✅✅✅—✅**M-Pesa**✅————**MTN MoMo**✅—✅——**Razorpay**✅✅✅—✅**Paddle**✅✅——✅> ⚠️ Monnify implements `SubscriptionDriverInterface` for interface compatibility, but all subscription methods throw `SubscriptionException` — Monnify has no subscription API.

> Capability detection is done via `instanceof` — drivers only implement the interfaces they support. No breaking changes when new capabilities are added.

For **runtime checks** without resolving the driver from the container, use `NexusPay\PaymentMadeEasy\GatewayCapabilities`:

```
use NexusPay\PaymentMadeEasy\GatewayCapabilities;
use NexusPay\PaymentMadeEasy\Contracts\SubscriptionDriverInterface;

if (GatewayCapabilities::driverImplements('paystack', SubscriptionDriverInterface::class)) {
    // safe to call subscription APIs
}

$slugs = GatewayCapabilities::gatewaysImplementing(SubscriptionDriverInterface::class);
```

---

Installation
------------

[](#installation)

### 1. Install via Composer

[](#1-install-via-composer)

```
composer require nexuspay/payment-made-easy
```

**PHP:** enable the **`bcmath`** extension (`ext-bcmath`); it is required by Composer for consistent amount conversion in drivers.

**Stripe:** the SDK is not installed with the package by default. If you use **`Payment::driver('stripe')`** or Stripe webhooks, add:

```
composer require stripe/stripe-php:^17.4
```

Without it, resolving the Stripe driver or handling Stripe webhooks throws a clear error pointing to this command.

**Outbound gateway HTTP:** drivers use Laravel’s **`Http`** facade (`illuminate/http`). Per-gateway **`http_timeout`** and **`http_verify`** in config apply to each request. For app-wide defaults (proxy, extra headers, timeouts), see the [Laravel HTTP client](https://laravel.com/docs/http-client) documentation — **`Http::globalOptions()`** is available from **Laravel 10.42** onward; on **Laravel 8–10.41** rely on those config keys or extend drivers as needed.

**Framework versions:** this package supports **Laravel 8.83+** through **Laravel 13** on **PHP 8.1+** (Laravel 13 requires **PHP 8.3+**). Match `illuminate/*` versions to your `laravel/framework` release.

### 2. Publish Configuration

[](#2-publish-configuration)

```
php artisan vendor:publish --provider="NexusPay\PaymentMadeEasy\PaymentServiceProvider"
```

### 3. (Optional) Publish &amp; Run Migrations

[](#3-optional-publish--run-migrations)

The package ships with migrations for recording payment activity to your database. This is opt-in:

```
# Publish migrations
php artisan vendor:publish --tag=payment-gateways-migrations

# Run migrations
php artisan migrate
```

Then enable recording in your `.env`:

```
PAYMENT_RECORDING_ENABLED=true
PAYMENT_RECORD_WEBHOOKS=true
# Optional: persist transactions / transfers / subscriptions from first-class webhook events
PAYMENT_RECORDING_AUTO_WEBHOOK_EVENTS=false
```

### 4. Verify Your Setup

[](#4-verify-your-setup)

After adding credentials to your `.env`, run the following to confirm all gateways are configured correctly:

```
php artisan payment:gateways --configured
```

Gateways with missing credentials will be highlighted in yellow.

```
# Default gateway & currency
PAYMENT_GATEWAY=paystack
PAYMENT_CURRENCY=NGN

# Paystack
PAYSTACK_PUBLIC_KEY=pk_live_xxxxx
PAYSTACK_SECRET_KEY=sk_live_xxxxx
PAYSTACK_CALLBACK_URL=https://yoursite.com/payment/callback
PAYSTACK_WEBHOOK_SECRET=your_paystack_webhook_secret

# Flutterwave
FLUTTERWAVE_PUBLIC_KEY=FLWPUBK-xxxxx
FLUTTERWAVE_SECRET_KEY=FLWSECK-xxxxx
FLUTTERWAVE_ENCRYPTION_KEY=your_flutterwave_encryption_key
FLUTTERWAVE_CALLBACK_URL=https://yoursite.com/payment/callback
FLUTTERWAVE_WEBHOOK_SECRET=your_flutterwave_webhook_secret

# Stripe
STRIPE_PUBLIC_KEY=pk_live_xxxxx
STRIPE_SECRET_KEY=sk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
STRIPE_CALLBACK_URL=https://yoursite.com/payment/callback

# Seerbit
SEERBIT_PUBLIC_KEY=your_seerbit_public_key
SEERBIT_SECRET_KEY=your_seerbit_secret_key
SEERBIT_BASE_URL=https://seerbitapi.com/api/v2
SEERBIT_CALLBACK_URL=https://yoursite.com/payment/callback
SEERBIT_WEBHOOK_SECRET=your_seerbit_webhook_secret

# Monnify
MONNIFY_API_KEY=your_monnify_api_key
MONNIFY_SECRET_KEY=your_monnify_secret_key
MONNIFY_CONTRACT_CODE=your_monnify_contract_code
MONNIFY_WALLET_ACCOUNT_NUMBER=your_monnify_wallet_account
MONNIFY_CALLBACK_URL=https://yoursite.com/payment/callback
MONNIFY_WEBHOOK_SECRET=your_monnify_webhook_secret

# Squad (GTCo)
SQUAD_SECRET_KEY=your_squad_secret_key
SQUAD_PUBLIC_KEY=your_squad_public_key
SQUAD_BENEFICIARY_ACCOUNT=your_squad_beneficiary_account
SQUAD_CALLBACK_URL=https://yoursite.com/payment/callback
SQUAD_WEBHOOK_SECRET=your_squad_webhook_secret

# Remita
REMITA_API_KEY=your_remita_api_key
REMITA_MERCHANT_ID=your_remita_merchant_id
REMITA_SERVICE_TYPE_ID=your_remita_service_type_id
REMITA_SOURCE_ACCOUNT=your_remita_source_account
REMITA_CALLBACK_URL=https://yoursite.com/payment/callback

# Budpay
BUDPAY_SECRET_KEY=your_budpay_secret_key
BUDPAY_PUBLIC_KEY=your_budpay_public_key
BUDPAY_CALLBACK_URL=https://yoursite.com/payment/callback
BUDPAY_WEBHOOK_SECRET=your_budpay_webhook_secret

# Interswitch
INTERSWITCH_CLIENT_ID=your_interswitch_client_id
INTERSWITCH_CLIENT_SECRET=your_interswitch_client_secret
INTERSWITCH_MERCHANT_CODE=your_interswitch_merchant_code
INTERSWITCH_PAYABLE_CODE=your_interswitch_payable_code
INTERSWITCH_TERMINAL_ID=your_interswitch_terminal_id
INTERSWITCH_CALLBACK_URL=https://yoursite.com/payment/callback
INTERSWITCH_WEBHOOK_SECRET=your_interswitch_webhook_secret

# PayPal
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
PAYPAL_CURRENCY=USD
PAYPAL_BASE_URL=https://api.paypal.com
PAYPAL_CALLBACK_URL=https://yoursite.com/payment/callback
PAYPAL_CANCEL_URL=https://yoursite.com/payment/cancel

# M-Pesa (Safaricom)
MPESA_CONSUMER_KEY=your_mpesa_consumer_key
MPESA_CONSUMER_SECRET=your_mpesa_consumer_secret
MPESA_SHORTCODE=your_mpesa_shortcode
MPESA_PASSKEY=your_mpesa_passkey
MPESA_INITIATOR_NAME=your_mpesa_initiator
MPESA_SECURITY_CREDENTIAL=your_mpesa_security_credential
MPESA_CALLBACK_URL=https://yoursite.com/webhooks/payment-gateways/mpesa
MPESA_RESULT_URL=https://yoursite.com/webhooks/payment-gateways/mpesa
MPESA_TIMEOUT_URL=https://yoursite.com/webhooks/payment-gateways/mpesa

# MTN MoMo
MTNMOMO_COLLECTION_USER_ID=your_mtnmomo_collection_user_id
MTNMOMO_COLLECTION_API_KEY=your_mtnmomo_collection_api_key
MTNMOMO_COLLECTION_SUBSCRIPTION_KEY=your_mtnmomo_collection_subscription_key
MTNMOMO_DISBURSEMENT_USER_ID=your_mtnmomo_disbursement_user_id
MTNMOMO_DISBURSEMENT_API_KEY=your_mtnmomo_disbursement_api_key
MTNMOMO_DISBURSEMENT_SUBSCRIPTION_KEY=your_mtnmomo_disbursement_subscription_key
MTNMOMO_ENVIRONMENT=sandbox
MTNMOMO_CURRENCY=EUR
MTNMOMO_CALLBACK_URL=https://yoursite.com/webhooks/payment-gateways/mtnmomo

# Razorpay
RAZORPAY_KEY_ID=rzp_live_xxxxx
RAZORPAY_KEY_SECRET=your_razorpay_key_secret
RAZORPAY_ACCOUNT_NUMBER=your_razorpay_x_account_number
RAZORPAY_CURRENCY=INR
RAZORPAY_CALLBACK_URL=https://yoursite.com/payment/callback
RAZORPAY_WEBHOOK_SECRET=your_razorpay_webhook_secret

# Paddle
PADDLE_API_KEY=your_paddle_api_key
PADDLE_CLIENT_TOKEN=your_paddle_client_token
PADDLE_WEBHOOK_SECRET=your_paddle_webhook_secret
PADDLE_CURRENCY=USD
PADDLE_BASE_URL=https://api.paddle.com
PADDLE_CALLBACK_URL=https://yoursite.com/payment/callback

# Webhook settings
PAYMENT_WEBHOOKS_ENABLED=true
PAYMENT_WEBHOOK_VERIFY_SIGNATURE=true
# When true with verify on, HMAC gateways must have webhook_secret (or Monnify secret_key / Interswitch client_secret) set
PAYMENT_WEBHOOK_REQUIRE_SIGNING_SECRET=true
PAYMENT_WEBHOOK_LOG_EVENTS=true
PAYMENT_WEBHOOK_QUEUE_EVENTS=false
# full = log payload data; minimal = gateway + event type only (less PII in logs)
PAYMENT_WEBHOOK_LOG_DETAIL=full
# When true with log_detail=full, sensitive keys are redacted before logging (see WebhookLogSanitizerInterface)
PAYMENT_WEBHOOK_LOG_SANITIZE=true
# Optional: FQCN of a custom WebhookLogSanitizerInterface implementation
# PAYMENT_WEBHOOK_LOG_SANITIZER=
# Extra webhook keys to redact (merged with package defaults; suffix rules apply — see config)
# PAYMENT_WEBHOOK_LOG_REDACT_KEYS=
PAYMENT_WEBHOOK_IDEMPOTENCY_ENABLED=false
PAYMENT_WEBHOOK_IDEMPOTENCY_TTL=86400
# Optional abuse protection (see Webhooks section for middleware)
PAYMENT_WEBHOOK_THROTTLE_ENABLED=false
PAYMENT_WEBHOOK_THROTTLE_MAX_ATTEMPTS=60
PAYMENT_WEBHOOK_THROTTLE_DECAY_SECONDS=60
# Optional: force stack traces on unexpected webhook errors (HTTP 500). Omit to tie to APP_DEBUG (traces off in production).
# PAYMENT_WEBHOOK_LOG_UNEXPECTED_TRACE=
```

---

One-Time Payments
-----------------

[](#one-time-payments)

Use the **default** gateway (`PAYMENT_GATEWAY` / config) or call `driver(...)` explicitly. Amounts are in **major** currency units unless a gateway note says otherwise.

```
use NexusPay\PaymentMadeEasy\Facades\Payment;

// Default driver (same payload shape as your configured gateway)
$response = Payment::initializePayment([
    'email'        => 'customer@example.com',
    'amount'       => 5000.00,
    'reference'    => 'ORDER_123',
    'callback_url' => 'https://yoursite.com/payment/callback',
    'metadata'     => ['order_id' => '123', 'customer_id' => '456'],
]);

// Other drivers — same general flow; see Gateway-Specific Notes for redirects and quirks
Payment::driver('flutterwave')->initializePayment([
    'email'    => 'customer@example.com',
    'amount'   => 5000.00,
    'name'     => 'John Doe',
    'phone'    => '+2348123456789',
    'currency' => 'NGN',
]);

Payment::driver('stripe')->initializePayment([
    'email'    => 'customer@example.com',
    'amount'   => 50.00,
    'currency' => 'usd',
]);

Payment::driver('seerbit')->initializePayment([
    'email'    => 'customer@example.com',
    'amount'   => 5000.00,
    'currency' => 'NGN',
    'name'     => 'John Doe',
    'phone'    => '+2348123456789',
]);

// Paystack — hosted checkout
$response = Payment::driver('paystack')->initializePayment([
    'email'        => 'customer@example.com',
    'amount'       => 5000.00,
    'reference'    => 'ORDER_123',
    'callback_url' => 'https://yoursite.com/payment/callback',
    'metadata'     => ['order_id' => '123'],
]);

// Redirect customer to $response['data']['authorization_url']

// Verify after callback
$verification = Payment::driver('paystack')->verifyPayment('ORDER_123');

// Payment details by reference
$payment = Payment::driver('paystack')->getPayment('ORDER_123');

// Refund
$refund = Payment::driver('paystack')->refundPayment('ORDER_123', 2500.00); // partial
$full   = Payment::driver('paystack')->refundPayment('ORDER_123');           // full

// List transactions
$txns = Payment::driver('paystack')->getTransactions(['per_page' => 50, 'page' => 1]);
```

---

Subscriptions &amp; Plans
-------------------------

[](#subscriptions--plans)

> Available on: **Paystack**, **Flutterwave**, **Stripe**, **Seerbit**

```
use NexusPay\PaymentMadeEasy\Contracts\SubscriptionDriverInterface;

$driver = Payment::driver('paystack');

// --- Plans ---
$plan = $driver->createPlan([
    'name'     => 'Pro Monthly',
    'amount'   => 5000.00,
    'interval' => 'monthly',   // monthly | weekly | annually | quarterly
]);
$planCode = $plan['data']['plan_code'];

$driver->updatePlan($planCode, ['name' => 'Pro Monthly (Updated)']);
$driver->getPlan($planCode);
$driver->listPlans(['per_page' => 20]);
$driver->deletePlan($planCode);

// --- Subscriptions ---
$sub = $driver->createSubscription([
    'email'      => 'customer@example.com',
    'plan_code'  => $planCode,
    'start_date' => '2026-06-01T00:00:00.000Z', // optional
]);
$subCode = $sub['data']['subscription_code'];

$driver->getSubscription($subCode);
$driver->listSubscriptions(['plan' => $planCode]);
$driver->listCustomerSubscriptions('customer@example.com');
$driver->pauseSubscription($subCode);
$driver->resumeSubscription($subCode);
$driver->cancelSubscription($subCode);
```

### Stripe plans and subscriptions

[](#stripe-plans-and-subscriptions)

Stripe maps plans to **Product + Price**. `createPlan()` returns a **Price ID** in `plan_code` — pass that to `createSubscription()`. Supported interval values: `day`, `week`, `month`, `year`.

```
$stripePlan = Payment::driver('stripe')->createPlan([
    'name'     => 'Pro Monthly',
    'amount'   => 29.99,
    'currency' => 'usd',
    'interval' => 'month',
]);
$priceId = $stripePlan['data']['plan_code'];

Payment::driver('stripe')->createSubscription([
    'email'     => 'customer@example.com',
    'plan_code' => $priceId,
]);
```

---

Disbursements &amp; Transfers
-----------------------------

[](#disbursements--transfers)

> Available on: **Paystack**, **Flutterwave**, **Monnify**, **Squad**, **Remita**, **Budpay**, **PayPal**, **Razorpay**, **MTN MoMo**. Paystack and Flutterwave use **transfer recipients**; Monnify, Squad, Remita, and Budpay typically use **bank account** fields. PayPal, Razorpay X, and MTN use different payloads — see **Gateway-Specific Notes**.

```
// Resolve an account number before transferring
$account = Payment::driver('paystack')->resolveAccountNumber([
    'account_number' => '0123456789',
    'bank_code'      => '044',
]);

// Create recipient
$recipient = Payment::driver('paystack')->createTransferRecipient([
    'name'           => 'Jane Doe',
    'account_number' => '0123456789',
    'bank_code'      => '044',
    'currency'       => 'NGN',
]);
$recipientCode = $recipient['data']['recipient_code'];

// Single transfer
$transfer = Payment::driver('paystack')->transfer([
    'amount'    => 10000.00,
    'recipient' => $recipientCode,
    'reason'    => 'Salary payout',
    'reference' => 'PAYOUT_001',
]);

// Bulk transfer
Payment::driver('paystack')->bulkTransfer([
    'transfers' => [
        ['amount' => 5000.00, 'recipient' => 'RCP_abc', 'reference' => 'B1'],
        ['amount' => 3000.00, 'recipient' => 'RCP_def', 'reference' => 'B2'],
    ],
]);

// Verify & list
Payment::driver('paystack')->verifyTransfer('PAYOUT_001');
Payment::driver('paystack')->listTransfers(['per_page' => 50]);

// List banks
$banks = Payment::driver('paystack')->listBanks(['country' => 'nigeria']);
```

### Bank-account style transfers (Monnify, Squad, Remita, Budpay)

[](#bank-account-style-transfers-monnify-squad-remita-budpay)

```
Payment::driver('monnify')->resolveAccountNumber([
    'account_number' => '0123456789',
    'bank_code'      => '044',
]);

Payment::driver('monnify')->transfer([
    'amount'                => 10000.00,
    'account_number'        => '0123456789',
    'bank_code'             => '044',
    'narration'             => 'Salary payout',
    'reference'             => 'MNFY_PAYOUT_001',
    'wallet_account_number' => config('payment-gateways.gateways.monnify.wallet_account_number'),
]);

Payment::driver('squad')->transfer([
    'account_number' => '0123456789',
    'bank_code'      => '044',
    'amount'         => 5000.00,
    'currency'       => 'NGN',
    'reference'      => 'SQ_PAYOUT_001',
    'narration'      => 'Salary',
]);

Payment::driver('remita')->bulkTransfer([
    'batchRef'  => 'BATCH_001',
    'narration' => 'Payroll',
    'transfers' => [
        ['amount' => 5000.00, 'account_number' => '0123456789', 'bank_code' => '044', 'name' => 'Jane Doe'],
    ],
]);

Payment::driver('budpay')->transfer([
    'amount'         => 5000.00,
    'currency'       => 'NGN',
    'account_number' => '0123456789',
    'bank_code'      => '044',
    'narration'      => 'Payout',
    'reference'      => 'BDP_PAYOUT_001',
]);
```

---

Virtual Accounts
----------------

[](#virtual-accounts)

> Available on: **Paystack**, **Flutterwave**, **Seerbit**, **Monnify**, **Squad**, **Budpay**

```
// Create a dedicated virtual account (bank transfer → auto-credited)
$va = Payment::driver('paystack')->createVirtualAccount([
    'email'          => 'customer@example.com',
    'name'           => 'Jane Doe',
    'bvn'            => '12345678901',
    'preferred_bank' => 'wema-bank',  // or 'titan-paystack'
]);

$accountNumber = $va['data']['account_number'];
$bankName      = $va['data']['bank']['name'];

Payment::driver('paystack')->getVirtualAccount($va['data']['id']);
Payment::driver('paystack')->listVirtualAccounts(['active' => true]);
Payment::driver('paystack')->deactivateVirtualAccount($va['data']['id']);
```

---

Payment Links
-------------

[](#payment-links)

> Available on: **Paystack**, **Flutterwave**, **Stripe**, **Seerbit**

```
$link = Payment::driver('paystack')->createPaymentLink([
    'name'        => 'Product Launch Special',
    'amount'      => 2500.00,
    'description' => 'Early-bird ticket',
    'currency'    => 'NGN',
]);

$url = $link['data']['link'];

Payment::driver('paystack')->updatePaymentLink($link['data']['id'], ['amount' => 3000.00]);
Payment::driver('paystack')->getPaymentLink($link['data']['id']);
Payment::driver('paystack')->listPaymentLinks();
Payment::driver('paystack')->disablePaymentLink($link['data']['id']);
```

---

Webhooks
--------

[](#webhooks)

Webhook routes are registered automatically:

```
POST /webhooks/payment-gateways/{gateway}

```

**Queueing:** Set `PAYMENT_WEBHOOK_QUEUE_EVENTS=true` to return `202` immediately and process the payload on the queue. The raw body is preserved so signature verification still works in the worker.

**Idempotency:** Set `PAYMENT_WEBHOOK_IDEMPOTENCY_ENABLED=true` to ignore duplicate deliveries with the same body (cache key per gateway + payload hash). Failed processing releases the lock so the provider can retry.

**Signing secrets:** With `PAYMENT_WEBHOOK_VERIFY_SIGNATURE` and `PAYMENT_WEBHOOK_REQUIRE_SIGNING_SECRET` both true (defaults), gateways that use HMAC (or Stripe’s signing secret) return **400** if the secret is missing, so you never verify with an empty key. **M-Pesa**, **MTN MoMo**, **Remita**, and **PayPal** skip this check (no shared HMAC secret in the same sense).

**Unexpected errors:** On HTTP **500** from the webhook controller, only the error message is logged by default; stack traces are included when **`APP_DEBUG`** is true. Set `PAYMENT_WEBHOOK_LOG_UNEXPECTED_TRACE=true` only if you explicitly want traces in logs (e.g. staging).

**Rate limiting:** Publish config and add the package middleware to `payment-gateways.webhooks.middleware`:

```
// config/payment-gateways.php
'webhooks' => [
    // ...
    'throttle' => [
        'enabled' => env('PAYMENT_WEBHOOK_THROTTLE_ENABLED', false),
        // ...
    ],
    'middleware' => [
        \NexusPay\PaymentMadeEasy\Http\Middleware\ThrottlePaymentWebhooks::class,
    ],
],
```

`payment:webhook-replay` bypasses signature verification by design; keep it **console-only** in production.

Configure each gateway's dashboard to point to:

```
https://yoursite.com/webhooks/payment-gateways/paystack
https://yoursite.com/webhooks/payment-gateways/flutterwave
https://yoursite.com/webhooks/payment-gateways/stripe
https://yoursite.com/webhooks/payment-gateways/seerbit
https://yoursite.com/webhooks/payment-gateways/monnify
https://yoursite.com/webhooks/payment-gateways/squad
https://yoursite.com/webhooks/payment-gateways/remita
https://yoursite.com/webhooks/payment-gateways/budpay
https://yoursite.com/webhooks/payment-gateways/interswitch
https://yoursite.com/webhooks/payment-gateways/paypal
https://yoursite.com/webhooks/payment-gateways/mpesa
https://yoursite.com/webhooks/payment-gateways/mtnmomo
https://yoursite.com/webhooks/payment-gateways/razorpay
https://yoursite.com/webhooks/payment-gateways/paddle

```

### Available Events

[](#available-events)

Event ClassFired When`PaymentSuccessful`A payment completes successfully`PaymentFailed`A payment fails`PaymentPending`A payment is pending`RefundProcessed`A refund is processed`SubscriptionCreated`A subscription is activated`SubscriptionCancelled`A subscription is cancelled`SubscriptionRenewed`A subscription renews / invoice paid`TransferSuccessful`A transfer/payout succeeds`TransferFailed`A transfer/payout fails`DisputeCreated`A dispute is raised`ChargebackCreated`A chargeback is raised### Listening to Events

[](#listening-to-events)

Register listeners in `app/Providers/EventServiceProvider.php` (or Laravel 11+ `AppServiceProvider` / `bootstrap/app.php` event discovery as you prefer):

```
use NexusPay\PaymentMadeEasy\Events\PaymentSuccessful;
use NexusPay\PaymentMadeEasy\Events\PaymentFailed;
use NexusPay\PaymentMadeEasy\Events\PaymentPending;
use NexusPay\PaymentMadeEasy\Events\RefundProcessed;
use NexusPay\PaymentMadeEasy\Events\SubscriptionCreated;
use NexusPay\PaymentMadeEasy\Events\SubscriptionCancelled;
use NexusPay\PaymentMadeEasy\Events\SubscriptionRenewed;
use NexusPay\PaymentMadeEasy\Events\TransferSuccessful;
use NexusPay\PaymentMadeEasy\Events\TransferFailed;
use NexusPay\PaymentMadeEasy\Events\DisputeCreated;
use NexusPay\PaymentMadeEasy\Events\ChargebackCreated;

protected $listen = [
    PaymentSuccessful::class   => [App\Listeners\HandleSuccessfulPayment::class],
    PaymentFailed::class       => [App\Listeners\HandleFailedPayment::class],
    PaymentPending::class      => [App\Listeners\HandlePendingPayment::class],
    RefundProcessed::class     => [App\Listeners\HandleRefundProcessed::class],
    SubscriptionCreated::class   => [App\Listeners\HandleSubscriptionCreated::class],
    SubscriptionCancelled::class => [App\Listeners\HandleSubscriptionCancelled::class],
    SubscriptionRenewed::class   => [App\Listeners\HandleSubscriptionRenewed::class],
    TransferSuccessful::class    => [App\Listeners\HandleTransferSuccessful::class],
    TransferFailed::class        => [App\Listeners\HandleTransferFailed::class],
    DisputeCreated::class        => [App\Listeners\HandleDisputeCreated::class],
    ChargebackCreated::class     => [App\Listeners\HandleChargebackCreated::class],
];
```

```
// app/Listeners/HandleSuccessfulPayment.php
class HandleSuccessfulPayment
{
    public function handle(PaymentSuccessful $event): void
    {
        $gateway   = $event->webhookEvent->getGateway();   // e.g. 'paystack'
        $eventType = $event->webhookEvent->getEventType(); // e.g. 'charge.success'
        $data      = $event->paymentData;

        // Normalized keys commonly include: reference, amount, currency, status,
        // customer_email, transaction_date, metadata

        // update order, send receipt, etc.
    }
}
```

Subscription and transfer listeners receive typed payloads: **`$event->subscriptionData`**, **`$event->invoiceData`** (renewals), **`$event->transferData`**, **`$event->reason`** (cancellations / failed transfers), **`$event->disputeData`**, **`$event->chargebackData`**.

### Manual processing with `WebhookManager`

[](#manual-processing-with-webhookmanager)

For a custom route (e.g. non-standard path), resolve **`WebhookManager`** and pass the gateway slug plus the request. Catch **`WebhookException`** for invalid signatures or bad payloads.

```
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use NexusPay\PaymentMadeEasy\WebhookManager;
use NexusPay\PaymentMadeEasy\Exceptions\WebhookException;

class CustomWebhookController extends Controller
{
    public function handle(Request $request, string $gateway, WebhookManager $webhookManager)
    {
        try {
            $webhookManager->handle($gateway, $request);

            return response()->json(['status' => 'success']);
        } catch (WebhookException $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }
    }
}
```

Optional `.env` toggles (see the Installation env block for the full list):

```
PAYMENT_WEBHOOKS_ENABLED=true
PAYMENT_WEBHOOK_VERIFY_SIGNATURE=true
PAYMENT_WEBHOOK_LOG_EVENTS=true
PAYMENT_WEBHOOK_QUEUE_EVENTS=false
PAYSTACK_WEBHOOK_SECRET=your_paystack_webhook_secret
FLUTTERWAVE_WEBHOOK_SECRET=your_flutterwave_webhook_secret
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
SEERBIT_WEBHOOK_SECRET=your_seerbit_webhook_secret
```

---

Capability Detection
--------------------

[](#capability-detection)

Check what a driver supports at runtime:

```
use NexusPay\PaymentMadeEasy\Contracts\SubscriptionDriverInterface;
use NexusPay\PaymentMadeEasy\Contracts\DisbursementDriverInterface;
use NexusPay\PaymentMadeEasy\Contracts\VirtualAccountDriverInterface;
use NexusPay\PaymentMadeEasy\Contracts\PaymentLinkDriverInterface;

$driver = Payment::driver('paystack');

if ($driver instanceof SubscriptionDriverInterface) {
    $driver->createPlan([...]);
}

if ($driver instanceof DisbursementDriverInterface) {
    $driver->transfer([...]);
}

if ($driver instanceof VirtualAccountDriverInterface) {
    $driver->createVirtualAccount([...]);
}

if ($driver instanceof PaymentLinkDriverInterface) {
    $driver->createPaymentLink([...]);
}
```

> **Note:** Monnify implements `SubscriptionDriverInterface` but all methods throw `SubscriptionException`. Always wrap in try/catch or check before calling.

### Injecting `PaymentManager`

[](#injecting-paymentmanager)

Use constructor injection when you need explicit gateway resolution or cleaner tests:

```
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use NexusPay\PaymentMadeEasy\PaymentManager;
use NexusPay\PaymentMadeEasy\Contracts\SubscriptionDriverInterface;

class PaymentController extends Controller
{
    public function __construct(private PaymentManager $paymentManager) {}

    public function charge(Request $request)
    {
        $driver = $this->paymentManager->driver($request->gateway ?? 'paystack');

        return $driver->initializePayment([
            'email'     => $request->email,
            'amount'    => $request->amount,
            'reference' => $request->reference,
        ]);
    }

    public function createPlan(Request $request)
    {
        $driver = $this->paymentManager->driver($request->gateway ?? 'paystack');

        if (!$driver instanceof SubscriptionDriverInterface) {
            abort(422, 'Selected gateway does not support subscriptions.');
        }

        return $driver->createPlan($request->validated());
    }
}
```

---

Gateway-Specific Notes
----------------------

[](#gateway-specific-notes)

### Monnify

[](#monnify)

**Hosted checkout** — redirect the customer to **`checkoutUrl`** in the response:

```
Payment::driver('monnify')->initializePayment([
    'email'       => 'customer@example.com',
    'amount'      => 5000.00,
    'currency'    => 'NGN',
    'name'        => 'John Doe',
    'description' => 'Order #123',
    'reference'   => 'ORDER_123',
]);
```

Monnify's primary use case is **virtual accounts** (reserved bank accounts). The package supports:

- Reserved account creation (`createVirtualAccount`) via POST `/api/v2/bank-transfer/reserved-accounts`
- Single and bulk disbursements
- Authentication is automatic — the driver fetches and caches a Bearer token via Basic Auth

```
$va = Payment::driver('monnify')->createVirtualAccount([
    'email'             => 'customer@example.com',
    'name'              => 'Jane Doe',
    'bvn'               => '12345678901',
    'currency_code'     => 'NGN',
    'contract_code'     => config('payment-gateways.gateways.monnify.contract_code'),
    'reference'         => 'VA_' . uniqid(),
    'split_percentages' => [],
]);
```

### Squad (GTCo)

[](#squad-gtco)

```
Payment::driver('squad')->initializePayment([
    'email'     => 'customer@example.com',
    'amount'    => 5000.00,
    'currency'  => 'NGN',
    'reference' => 'TXN_' . uniqid(),
]);

// Virtual account
Payment::driver('squad')->createVirtualAccount([
    'customer_identifier' => 'customer_001',
    'email'               => 'customer@example.com',
    'name'                => 'Jane Doe',
]);
```

### Remita

[](#remita)

Remita's payment flow uses a **Remita Retrieval Reference (RRR)**. `initializePayment()` returns an RRR that you use to construct the checkout URL. Pass the RRR as `$reference` to `verifyPayment()`.

```
$result = Payment::driver('remita')->initializePayment([
    'email'       => 'customer@example.com',
    'amount'      => 5000.00,
    'description' => 'Invoice #1001',
    'reference'   => 'ORDER_1001',
]);

// Redirect to checkout: $result['data']['authorization_url']
// After callback, verify with RRR:
Payment::driver('remita')->verifyPayment($result['data']['rrr']);
```

### Budpay

[](#budpay)

Budpay uses a dual-header auth: Bearer token + `Encryption` header (HMAC-SHA512). This is handled internally; no extra configuration required.

```
Payment::driver('budpay')->initializePayment([
    'email'     => 'customer@example.com',
    'amount'    => 5000.00,
    'reference' => 'BDP_' . uniqid(),
    'currency'  => 'NGN',
]);
```

### Interswitch (Webpay)

[](#interswitch-webpay)

Interswitch uses **OAuth2 client\_credentials** — the token is fetched and cached automatically. Amount is stored in kobo internally. Currency uses ISO 4217 numeric codes (e.g. `566` for NGN).

```
Payment::driver('interswitch')->initializePayment([
    'email'     => 'customer@example.com',
    'amount'    => 5000.00,
    'reference' => 'ISW_' . uniqid(),
    'currency'  => 'NGN',
]);
```

### PayPal

[](#paypal)

PayPal uses **OAuth2 client\_credentials** — the access token is fetched and cached automatically. Amounts are in major currency units (USD, EUR, etc.) as decimal strings. The driver covers payments, billing subscriptions, and Payouts (bulk disbursements).

```
$order = Payment::driver('paypal')->initializePayment([
    'amount'   => 50.00,
    'currency' => 'USD',
    'email'    => 'customer@example.com',
]);
// Redirect to $order['links'][n]['href'] where rel == 'approve'
// After approval, capture using the order ID:
Payment::driver('paypal')->verifyPayment($order['id']);
```

PayPal webhooks use RSA-SHA256 certificate-based signing (not HMAC). The handler verifies that all required PayPal signature headers are present as a guard.

### M-Pesa (Safaricom Kenya)

[](#m-pesa-safaricom-kenya)

M-Pesa uses **STK Push** — a prompt is sent to the customer's phone. Payment is **asynchronous**; the result is delivered to your `MPESA_CALLBACK_URL`.

```
$response = Payment::driver('mpesa')->initializePayment([
    'phone'  => '254712345678',   // international format, no +
    'amount' => 500,              // KES whole number
]);
$checkoutRequestId = $response['data']['checkout_request_id'];

// Poll or wait for callback, then verify:
Payment::driver('mpesa')->verifyPayment($checkoutRequestId);
```

M-Pesa does not send HMAC signatures — secure your callback URLs via HTTPS.

### MTN MoMo

[](#mtn-momo)

MTN MoMo uses two separate products (Collections and Disbursements), each with their own API User + Key + Subscription Key. Payments are also **asynchronous** — a push is sent to the customer's wallet, and the result is delivered to your `MTNMOMO_CALLBACK_URL`.

```
$response = Payment::driver('mtnmomo')->initializePayment([
    'phone'    => '256771234567',   // MSISDN, no +
    'amount'   => 1000,
    'currency' => 'UGX',
]);
$referenceId = $response['data']['reference'];

// Poll for status:
Payment::driver('mtnmomo')->verifyPayment($referenceId);

// Disbursement:
Payment::driver('mtnmomo')->transfer([
    'phone'    => '256771234567',
    'amount'   => 1000,
    'currency' => 'UGX',
    'narration' => 'Salary payout',
]);
```

### Razorpay

[](#razorpay)

Razorpay uses **Basic Auth** (key\_id:key\_secret) on every request. Amounts are in paise (1 INR = 100 paise), handled internally by `convertAmount()`. The driver supports payments, subscriptions, payouts (Razorpay X account required), and payment links.

```
$order = Payment::driver('razorpay')->initializePayment([
    'email'    => 'customer@example.com',
    'amount'   => 499.00,
    'currency' => 'INR',
    'name'     => 'Jane Doe',
    'phone'    => '+919876543210',
]);
// Pass $order['id'], $order['data']['key_id'], etc. to Razorpay checkout.js
```

### Paddle

[](#paddle)

Paddle is a **Merchant of Record** for SaaS / digital goods. The driver targets the Paddle Billing API. Plans map to Prices, payments to Transactions, and payment links to Paddle's native Payment Links.

```
$transaction = Payment::driver('paddle')->initializePayment([
    'amount'      => 29.99,
    'currency'    => 'USD',
    'description' => 'Pro plan',
    'email'       => 'customer@example.com',
]);
// Redirect to $transaction['data']['authorization_url']
```

Paddle webhooks use HMAC-SHA256 with the `Paddle-Signature: ts=...;h1=...` format, verified automatically.

---

Security
--------

[](#security)

Run **`composer audit`** regularly on consuming applications and keep gateway credentials in environment variables only (never commit secrets). Webhook replay (`payment:webhook-replay`) skips signature checks — restrict production use to trusted operators.

**Vulnerability disclosure:** do not use the public issue tracker for undisclosed security bugs. Follow **[SECURITY.md](SECURITY.md)** (private report and scope).

Testing
-------

[](#testing)

Run the package's own test suite:

```
composer test          # all tests
composer test:unit     # unit tests only
composer test:feature  # feature tests only
```

The test suite covers:

Test fileWhat it tests`tests/Unit/PaymentManagerTest.php`driver resolution, interface detection, alias binding`tests/Unit/WebhookManagerTest.php`handler resolution, unknown gateway exception`tests/Unit/Drivers/PaystackDriverTest.php`HTTP mock — initialize, verify, refund, error handling`tests/Unit/Drivers/RazorpayDriverTest.php`HTTP mock — order creation, verify, refund, errors`tests/Feature/WebhookHandlingTest.php`full HTTP webhook flow — signature check, event dispatchIn your own application, you can mock the `Payment` facade or bind a fake driver:

```
// In your test
Payment::shouldReceive('driver->initializePayment')
    ->once()
    ->andReturn(['status' => true, 'data' => ['authorization_url' => 'https://...']]);
```

Database Recording
------------------

[](#database-recording)

When `PAYMENT_RECORDING_ENABLED=true`, use `PaymentRecorder` to persist activity:

```
use NexusPay\PaymentMadeEasy\Services\PaymentRecorder;

$recorder = app(PaymentRecorder::class);

// After initializing a payment
$transaction = $recorder->recordTransaction('paystack', $requestData, $gatewayResponse);

// After verifying / receiving a webhook
$recorder->updateTransactionStatus('ORDER_001', 'successful', 'PS_GATEWAY_REF');

// Record a transfer
$transfer = $recorder->recordTransfer('paystack', $transferData, $transferResponse);

// Log a webhook event for auditing
$recorder->logWebhookEvent('paystack', $webhookEvent);

// Refunds (e.g. after a refund webhook) — appends `payment_refunds` rows and updates parent status
// (partially_refunded vs refunded from summed amounts). Optional 6th arg: provider refund id for dedupe.
$recorder->handleRefundWebhook('paystack', 'ORDER_001', 50.0, 'NGN', $rawPayload, 're_abc123');
```

With **`PAYMENT_RECORDING_AUTO_WEBHOOK_EVENTS=true`**, listeners also persist **`RefundProcessed`** (status `refunded` / `partially_refunded` + `payment_refunds` rows), **`DisputeCreated`** / **`ChargebackCreated`** (status `disputed` + **`payment_disputes`** rows), in addition to payment, transfer, and subscription outcomes.

### Available Models

[](#available-models)

ModelTablePurpose`PaymentTransaction``payment_transactions`One-time payment records`PaymentRefund``payment_refunds`One row per refund (multi-refund history)`PaymentDispute``payment_disputes`Dispute / chargeback events (linked to transaction when present)`PaymentSubscription``payment_subscriptions`Subscription / billing records`PaymentTransfer``payment_transfers`Disbursement / transfer records`PaymentWebhookLog``payment_webhook_logs`Audit log for all incoming webhooksUse **`$transaction->refunds`** and **`$transaction->totalRefundedAmount()`** for reporting. Status **`partially_refunded`** is set when refunded sum is below the original amount; **`refunded`** when the sum covers the original (with a small float tolerance).

All models support polymorphic `payable` / `subscriber` / `initiator` relationships so you can link them to your own models (e.g. `User`, `Order`).

```
// Attach a transaction to a user
$transaction->payable()->associate($user)->save();

// Scope helpers
PaymentTransaction::successful()->gateway('paystack')->get();
PaymentSubscription::active()->forEmail('user@example.com')->get();
PaymentTransfer::successful()->gateway('mtnmomo')->get();
```

---

Artisan Commands
----------------

[](#artisan-commands)

The package ships built-in Artisan commands for local development and debugging.

### `payment:gateways` — List all configured gateways

[](#paymentgateways--list-all-configured-gateways)

```
# List all 14 supported gateways with their capabilities
php artisan payment:gateways

# Show only gateways that have credentials configured
php artisan payment:gateways --configured
```

Output shows a capability matrix (Payments, Subscriptions, Disbursements, Virtual Accounts, Payment Links), marks the default gateway with `*`, and colour-codes each gateway as **ready** (green) or **missing env** (yellow) based on whether credentials are present in your `.env`.

---

### `payment:verify` — Verify a payment by reference

[](#paymentverify--verify-a-payment-by-reference)

```
# Pretty-print the verification result
php artisan payment:verify paystack ORDER_123

# Dump the full raw API response as JSON
php artisan payment:verify paystack ORDER_123 --json
```

Useful for quickly checking a payment status from the command line without writing a temporary controller.

---

### `payment:webhook-replay` — Replay a logged webhook event

[](#paymentwebhook-replay--replay-a-logged-webhook-event)

> Requires `PAYMENT_RECORDING_ENABLED=true` and migrations to have been run.

```
# Replay webhook log #42 — re-fires the stored event through the handler
php artisan payment:webhook-replay 42

# Dry-run — prints the stored payload without dispatching any events
php artisan payment:webhook-replay 42 --dry-run
```

Looks up the `PaymentWebhookLog` row by its primary key, reconstructs the original request, and routes it back through the appropriate webhook handler. The log's status is updated to `processed` on success or `failed` on error.

---

### `payment:transactions` — List recorded transactions

[](#paymenttransactions--list-recorded-transactions)

Requires the **`payment_transactions`** table (publish and run package migrations).

```
php artisan payment:transactions
php artisan payment:transactions --gateway=paystack --status=successful --limit=50
```

---

Contributing
------------

[](#contributing)

Please read [CONTRIBUTING.md](CONTRIBUTING.md) for how to report issues, run tests, and submit pull requests.

License
-------

[](#license)

This package is released under the [MIT License](LICENSE).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance90

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

52d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/46940605?v=4)[Balogun Abdulquddus](/maintainers/alphabalex)[@Alphabalex](https://github.com/Alphabalex)

---

Top Contributors

[![Alphabalex](https://avatars.githubusercontent.com/u/46940605?v=4)](https://github.com/Alphabalex "Alphabalex (18 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/nexuspay-payment-made-easy/health.svg)

```
[![Health](https://phpackages.com/badges/nexuspay-payment-made-easy/health.svg)](https://phpackages.com/packages/nexuspay-payment-made-easy)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77022.3M149](/packages/laravel-mcp)[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k90.5k1](/packages/mike-bronner-laravel-model-caching)[illuminate/auth

The Illuminate Auth package.

10528.2M1.2k](/packages/illuminate-auth)[illuminate/routing

The Illuminate Routing package.

1419.2M3.0k](/packages/illuminate-routing)[spatie/laravel-export

Create a static site bundle from a Laravel app

674146.0k6](/packages/spatie-laravel-export)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
