PHPackages                             paynexus/laravel-paynexus - 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. paynexus/laravel-paynexus

ActiveLibrary[Payment Processing](/categories/payments)

paynexus/laravel-paynexus
=========================

Laravel SDK for the PayNexus payment orchestration platform. Accept M-Pesa STK Push payments, track payment status in real time, and keep local payment records synchronized with PayNexus.

v1.1.7(1mo ago)041MITPHP ^8.2

Since May 21Compare

[ Source](https://github.com/MCBANKSKE/paynexus-laravel-plugin)[ Packagist](https://packagist.org/packages/paynexus/laravel-paynexus)[ Docs](https://www.paynexus.co.ke)[ RSS](/packages/paynexus-laravel-paynexus/feed)WikiDiscussions Synced 3w ago

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

PayNexus Laravel Plugin
=======================

[](#paynexus-laravel-plugin)

[![Latest Version](https://camo.githubusercontent.com/bedb1c440911049e2a73f55c02c409c7b6e4c6fc8274e205e237b06d999d0eac/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7061796e657875732f6c61726176656c2d7061796e657875733f7374796c653d666c61742d737175617265)](https://packagist.org/packages/paynexus/laravel-paynexus)[![License](https://camo.githubusercontent.com/a87d5d25a19d7a72d08c518c6c8a49a7087a19a635752015ae6944f58213fcb4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7061796e657875732f6c61726176656c2d7061796e657875733f7374796c653d666c61742d737175617265)](https://github.com/MCBANKSKE/paynexus-laravel-plugin/blob/main/LICENSE)[![Laravel](https://camo.githubusercontent.com/ce052d5774b89fb37f4da1a278cbbc2a10947bcd251a707324dd1b4d3d14c641/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d382532422d4646324432303f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c)](https://laravel.com)[![PHP](https://camo.githubusercontent.com/119ab04a0e7c293f3d9a4648c84e373315e69d1f6d6aa8f2065519dade71686b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e302532422d3737374242343f7374796c653d666c61742d737175617265266c6f676f3d706870)](https://php.net)

**Accept M-Pesa payments through PayNexus in any Laravel application**

A powerful client SDK that connects your Laravel application to the PayNexus payment platform, handling M-Pesa STK Push, real-time payment status tracking, webhook processing, and automatic local record-keeping.

[Documentation](#-) • [Installation](#installation) • [Quick Start](#quick-start) • [API Reference](#api-reference)

---

✨ Features
----------

[](#-features)

- 🚀 **Easy Integration** - Simple facade-based API for quick setup
- 💳 **M-Pesa STK Push** - Seamless mobile payment initiation
- 🔄 **Real-time Tracking** - Poll for payment completion or use webhooks
- 📊 **Local Records** - Automatic database synchronization
- 🔔 **Webhook Events** - Laravel events for payment state changes
- 🔒 **Secure** - HMAC signature verification for webhooks
- 🎯 **Polymorphic Relations** - Link payments to any model (Order, Invoice, etc.)
- ✅ **Phone Validation** - Built-in phone number normalization

⚠️ Plugin vs Platform
---------------------

[](#️-plugin-vs-platform)

> **This plugin is for external Laravel applications** (e.g., ecommerce stores, SaaS apps) that want to accept payments through PayNexus.

ComponentDescription**PayNexus Platform**The payment gateway platform at [paynexus.co.ke](https://paynexus.co.ke) that processes payments**This Plugin**A Laravel package that YOUR application installs to connect to the PayNexus platform> **Note:** If you're looking for the PayNexus platform source code, that's a separate repository. This plugin is the client SDK for merchants integrating with PayNexus.

---

📑 Table of Contents
-------------------

[](#-table-of-contents)

- [How It Works](#how-it-works)
- [Installation](#installation)
- [Configuration](#configuration)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
    - [Merchant](#merchant)
    - [Businesses](#businesses)
    - [Payment Accounts](#payment-accounts)
    - [Initiate Payment](#initiate-payment)
    - [Payment Status](#payment-status)
    - [Poll for Completion](#poll-for-completion)
    - [List Payments](#list-payments)
    - [Phone Validation](#phone-validation)
    - [Webhooks](#webhooks)
- [Webhook Integration](#webhook-integration)
- [Events](#events)
- [Local Payment Records](#local-payment-records)
- [Ecommerce Integration Guide](#ecommerce-integration-guide)
    - [Order Checkout Flow](#order-checkout-flow)
    - [Cart Checkout Controller](#cart-checkout-controller)
    - [Payment Status Page (Livewire)](#payment-status-page-livewire)
    - [Handling Webhooks in Your App](#handling-webhooks-in-your-app)
    - [Subscription Integration](#subscription-integration)
    - [Manual Payment Verification](#manual-payment-verification)
- [Error Handling](#error-handling)
- [Testing](#testing)
- [JavaScript Polling](#javascript-polling)
- [Custom Webhook Handler](#custom-webhook-handler)
- [Configuration Reference](#configuration-reference)
- [Getting API Keys](#getting-api-keys)
- [Support](#support)
- [Troubleshooting](#troubleshooting)
- [License](#license)

---

🔄 How It Works
--------------

[](#-how-it-works)

 ```
sequenceDiagram
    participant App as Your Laravel App
    participant API as PayNexus API
    participant MPesa as M-Pesa (Daraja)
    participant Customer as Customer

    App->>API: initiatePayment()
    API->>API: Create payment record
    App->>App: Create local record
    API->>MPesa: STK Push
    MPesa->>Customer: Payment prompt
    Customer->>MPesa: Enter PIN
    MPesa->>API: Callback
    API->>API: Update payment status
    API->>App: Webhook
    App->>App: Update local record
    App->>App: Dispatch event
```

      Loading **Payment Flow:**

1. **Initiate** — Your app calls `PayNexus::initiatePayment(...)`. A payment record is created both on PayNexus **and** in your local `paynexus_payments` table.
2. **STK Push** — PayNexus sends an M-Pesa STK Push to the customer's phone.
3. **Customer pays** — The customer enters their PIN on their phone.
4. **Callback** — M-Pesa sends a callback to PayNexus. PayNexus updates its record and fires a webhook to your app.
5. **Webhook received** — This plugin receives the webhook, updates the local `paynexus_payments` record, and dispatches a Laravel event (`PaymentCompleted` or `PaymentFailed`).
6. **Your listener** — Your `EventServiceProvider` listener marks the order as paid, sends a receipt, etc.

> **Both records always match.** If the webhook is missed, you can call `PayNexus::pollStatus(...)` or `PayNexus::getPaymentByCheckoutId(...)` to sync manually.

---

📦 Installation
--------------

[](#-installation)

Install the package via Composer:

```
composer require paynexus/laravel-paynexus
```

The service provider and facade are auto-discovered by Laravel.

### Publish Config and Migration

[](#publish-config-and-migration)

```
php artisan vendor:publish --tag=paynexus-config
php artisan vendor:publish --tag=paynexus-migrations
php artisan migrate
```

---

⚙️ Configuration
----------------

[](#️-configuration)

Add these to your `.env` file:

```
# Required — your secret API key from the PayNexus merchant dashboard
PAYNEXUS_SECRET_KEY=sk_your_secret_key_here

# Optional — your public API key for read operations (client-side safe)
PAYNEXUS_PUBLIC_KEY=pk_your_public_key_here

# For backwards compatibility, you can also use PAYNEXUS_API_KEY (maps to secret key)
# PAYNEXUS_API_KEY=sk_your_secret_key_here

# Required — PayNexus API base URL
PAYNEXUS_BASE_URL=https://paynexus.co.ke

# Optional — webhook signature verification secret
PAYNEXUS_WEBHOOK_SECRET=whsec_your_webhook_secret

# Optional — default currency (default: KES)
PAYNEXUS_CURRENCY=KES
```

### 🔑 Where to Get Credentials

[](#-where-to-get-credentials)

CredentialLocationUsage**Secret Key** (`sk_…`)[PayNexus Dashboard → API Keys](https://paynexus.co.ke/merchant/merchant-api-keys)Write operations (initiate payments, manage webhooks). Keep server-side only.**Public Key** (`pk_…`)[PayNexus Dashboard → API Keys](https://paynexus.co.ke/merchant/merchant-api-keys)Read operations only (merchant info, payment status). Safe for client-side code.**Webhook Secret**[PayNexus Dashboard → Webhooks](https://paynexus.co.ke/merchant)Generate a webhook pointing to `https://yourapp.com/paynexus/webhook`### 🔓 Public Key Allowed Operations

[](#-public-key-allowed-operations)

Public keys (`pk_`) can perform the following read-only operations:

**Merchant Information**

- `GET /api/merchant` - Get merchant information
- `GET /api/merchant/businesses` - List merchant businesses
- `GET /api/merchant/payment-accounts` - List payment accounts

**Payment Read Operations**

- `GET /api/payments/{reference}` - Get payment status by reference
- `GET /api/payments/{id}/status-by-id` - Get payment status by ID
- `POST /api/payments/status-by-checkout-id` - Get payment status by checkout ID
- `GET /api/payments` - List all payments

**M-Pesa Operations**

- `GET /api/mpesa/health` - M-Pesa service health check
- `POST /api/mpesa/validate-phone` - Validate phone number
- `POST /api/mpesa/payment/status` - Check M-Pesa transaction status

**Webhook Read Operations**

- `GET /api/webhooks` - List registered webhooks

**API Key Read Operations**

- `GET /api/api-keys` - List API keys

**Invoice Read Operations**

- `GET /api/invoices` - List invoices
- `GET /api/invoices/{invoice}` - View invoice details

**Receipt Read Operations**

- `GET /api/receipts` - List receipts
- `GET /api/receipts/{receipt}` - View receipt details

### 🔒 Public Key Blocked Operations

[](#-public-key-blocked-operations)

Public keys (`pk_`) are blocked from these write operations at the middleware level:

**Payment Write Operations**

- `POST /api/payments/initiate` - Initiate payments
- `POST /api/mpesa/payment/initiate` - Initiate STK push payment

**Webhook Management**

- `POST /api/webhooks/register` - Register webhooks
- `PUT /api/webhooks/{id}` - Update webhooks
- `DELETE /api/webhooks/{id}` - Delete webhooks

**API Key Management**

- `POST /api/api-keys` - Create API keys
- `PUT /api/api-keys/{id}` - Update API keys
- `DELETE /api/api-keys/{id}` - Delete API keys

**Invoice Management**

- `POST /api/invoices` - Create invoices
- `PUT /api/invoices/{invoice}` - Update invoices
- `DELETE /api/invoices/{invoice}` - Delete invoices
- `POST /api/invoices/{invoice}/send` - Send invoices

**Receipt Management**

- `POST /api/receipts/{receipt}/resend` - Resend receipts

---

🚀 Quick Start
-------------

[](#-quick-start)

```
use PayNexus\Facades\PayNexus;

// 1. Initiate an M-Pesa STK Push payment
$result = PayNexus::initiatePayment([
    'amount'      => 1500,
    'phone'       => '254712345678',
    'description' => 'Order #1001 - Wireless Headphones',
]);

if ($result['success']) {
    $checkoutRequestId = $result['data']['checkout_request_id'];
    $reference         = $result['data']['reference'];
    echo "STK Push sent! Reference: {$reference}";
}

// 2. Check status immediately
$status = PayNexus::getPaymentByCheckoutId($checkoutRequestId);
echo $status['data']['status']; // 'pending', 'completed', 'failed'

// 3. Or poll until completion (blocks up to 120s by default)
$final = PayNexus::pollStatus($checkoutRequestId);
if ($final['data']['status'] === 'completed') {
    echo 'Payment completed!';
}
```

---

📚 API Reference
---------------

[](#-api-reference)

### 👤 Merchant

[](#-merchant)

```
// Get merchant details
$merchant = PayNexus::getMerchant();
// Returns: { success: true, data: { id, business_name, status, subscription_status, … } }
```

### 🏢 Businesses

[](#-businesses)

```
// List merchant businesses
$businesses = PayNexus::getBusinesses();
// Returns: { success: true, data: [{ id, business_name, business_email, status, … }] }
```

### 💳 Payment Accounts

[](#-payment-accounts)

```
// List all payment accounts
$accounts = PayNexus::getPaymentAccounts();
// Returns: { success: true, data: [{ id, provider, type, account_name, till_number, … }] }
```

### 💰 Initiate Payment

[](#-initiate-payment)

Two endpoints are available:

```
// Option A: Generic payment initiation (recommended)
// payment_account_id is auto-resolved from the API if not provided
$result = PayNexus::initiatePayment([
    'amount'      => 500,
    'phone'       => '254712345678',
    'description' => 'Invoice #42',
]);

// Option B: M-Pesa-specific endpoint (includes server-side phone validation)
$result = PayNexus::initiateMpesaPayment([
    'amount'      => 500,
    'phone'       => '0712345678',   // accepts local format too
    'description' => 'Invoice #42',
    'remark'      => 'Website Payment',
]);

// You can also pass a specific payment_account_id if you have multiple accounts:
// $result = PayNexus::initiatePayment(['payment_account_id' => 2, ...]);
```

**Idempotency Protection:**

To prevent duplicate payments, you can provide an idempotency key:

```
$result = PayNexus::initiatePayment([
    'amount'      => 500,
    'phone'       => '254712345678',
    'description' => 'Invoice #42',
    'idempotency_key' => 'inv-42-unique-key',  // Optional but recommended
]);
```

If a payment with the same `idempotency_key` already exists, the existing payment details will be returned instead of creating a new one. This is especially useful for handling network retries and preventing duplicate charges.

**Benefits of idempotency:**

- Prevents duplicate payments on network retries
- Safe to retry payment initiation without side effects
- Atomic database transactions ensure consistency
- Existing payment returned if key matches

**Response:**

```
{
    "success": true,
    "data": {
        "payment_id": 42,
        "reference": "PNXABCD1234",
        "checkout_request_id": "ws_CO_...",
        "merchant_request_id": "...",
        "amount": 500,
        "currency": "KES",
        "phone": "254712345678",
        "status": "pending",
        "response_code": "0",
        "response_description": "Success. Request accepted for processing",
        "customer_message": "Success. Request accepted for processing"
    },
    "message": "Payment initiated successfully"
}
```

Both methods also create a **local `paynexus_payments` record** automatically.

### 📊 Payment Status

[](#-payment-status)

```
// By PayNexus reference
$status = PayNexus::getPaymentByReference('PNXABCD1234');

// By PayNexus payment ID
$status = PayNexus::getPaymentById(42);

// By M-Pesa checkout request ID (also syncs local record)
$status = PayNexus::getPaymentByCheckoutId('ws_CO_...');

// Real-time M-Pesa query via Daraja (most accurate, also syncs local record)
$status = PayNexus::checkMpesaStatus('ws_CO_...');
```

### ⏱️ Poll for Completion

[](#️-poll-for-completion)

Blocks until the payment reaches a terminal state or times out:

```
$result = PayNexus::pollStatus(
    checkoutRequestId: 'ws_CO_...',
    intervalSeconds: 3,    // check every 3 seconds (default)
    timeoutSeconds: 120,   // give up after 2 minutes (default)
);

if ($result['data']['status'] === 'completed') {
    // Payment is done
}
```

### 📋 List Payments

[](#-list-payments)

```
$payments = PayNexus::listPayments([
    'status'         => 'completed',
    'payment_method' => 'mpesa',
    'from_date'      => '2026-01-01',
    'to_date'        => '2026-12-31',
    'per_page'       => 50,
]);
```

### ✅ Phone Validation

[](#-phone-validation)

```
$validation = PayNexus::validatePhone('0712345678');
// { success: true, data: { valid: true, normalized: '254712345678' } }
```

### 🔗 Webhooks

[](#-webhooks)

```
// Register your webhook endpoint with PayNexus
PayNexus::registerWebhook(
    name: 'My App',
    url: 'https://myapp.com/paynexus/webhook',
    events: ['payment.completed', 'payment.failed']
);

// List webhooks
$webhooks = PayNexus::listWebhooks();

// Update a webhook
PayNexus::updateWebhook(1, ['active' => false]);

// Delete a webhook
PayNexus::deleteWebhook(1);
```

---

🔔 Webhook Integration
---------------------

[](#-webhook-integration)

The plugin automatically registers a POST route at `/paynexus/webhook` (configurable via `PAYNEXUS_WEBHOOK_PATH`).

### Queue Support for Webhooks

[](#queue-support-for-webhooks)

For production environments, you can enable queue processing for webhooks to improve reliability:

```
PAYNEXUS_QUEUE_WEBHOOKS=true
PAYNEXUS_QUEUE_CONNECTION=redis
PAYNEXUS_QUEUE_NAME=webhooks
```

When enabled, webhooks are dispatched to a queue job (`ProcessPayNexusWebhook`) with configurable retry behavior:

```
PAYNEXUS_WEBHOOK_MAX_ATTEMPTS=5
PAYNEXUS_WEBHOOK_BACKOFF=exponential  # linear, exponential, or constant
PAYNEXUS_WEBHOOK_BASE_DELAY=2000     # milliseconds
```

**Benefits of queue processing:**

- Asynchronous processing prevents webhook timeouts
- Automatic retries with exponential backoff
- Better handling of high-volume webhook traffic
- Dead-letter queue support for failed webhooks

**Handling Webhook Failures:**

When a webhook job exhausts its retry attempts, a `WebhookProcessingFailed` event is dispatched. You can listen for this event:

```
use PayNexus\Events\WebhookProcessingFailed;

protected $listen = [
    WebhookProcessingFailed::class => [
        \App\Listeners\HandleWebhookFailure::class,
    ],
];
```

**PayNexus sends this payload when a payment completes:**

```
{
    "event": "payment.completed",
    "timestamp": "2026-05-22T10:00:00.000000Z",
    "data": {
        "payment_id": 42,
        "merchant_id": 1,
        "reference": "PNXABCD1234",
        "amount": "1500.00",
        "currency": "KES",
        "phone": "254712345678",
        "status": "completed",
        "account_reference": "ORD-1001",
        "checkout_request_id": "ws_CO_20260522100000123456",
        "transaction_id": "SH1234ABCDE",
        "provider_transaction_id": "SH1234ABCDE",
        "provider_reference": "SH1234ABCDE",
        "payer_name": "JOHN DOE",
        "created_at": "2026-05-22T09:59:00.000000Z",
        "updated_at": "2026-05-22T10:00:00.000000Z"
    }
}
```

**Headers sent with each webhook:**

HeaderDescription`X-PayNexus-Signature`HMAC-SHA256 of the JSON body using your webhook secret`X-PayNexus-Timestamp`Unix timestamp — used for replay protectionThe webhook controller:

1. Verifies the `X-PayNexus-Signature` header (HMAC-SHA256 of the JSON body using your webhook secret).
2. Checks `X-PayNexus-Timestamp` for replay protection (rejects payloads older than 5 minutes by default).
3. Finds or creates the matching `paynexus_payments` record.
4. Updates the local record's status.
5. Dispatches a Laravel event (`PaymentCompleted` or `PaymentFailed`).

**Setup in PayNexus dashboard:**

1. Go to **Webhooks** in your PayNexus merchant panel.
2. Add a webhook pointing to `https://yourapp.com/paynexus/webhook`.
3. Copy the generated secret and set `PAYNEXUS_WEBHOOK_SECRET` in your `.env`.

---

🎯 Events
--------

[](#-events)

EventWhenProperties`PayNexus\Events\PaymentCompleted`Webhook reports `payment.completed``$payment` (PaynexusPayment), `$payload` (array)`PayNexus\Events\PaymentFailed`Webhook reports `payment.failed``$payment` (PaynexusPayment), `$payload` (array), `$reason` (string)`PayNexus\Events\PaymentInitiated`After `initiatePayment()` succeeds`$payment` (PaynexusPayment)**Register listeners in your `EventServiceProvider`:**

```
use PayNexus\Events\PaymentCompleted;
use PayNexus\Events\PaymentFailed;

protected $listen = [
    PaymentCompleted::class => [
        \App\Listeners\HandlePaymentSuccess::class,
    ],
    PaymentFailed::class => [
        \App\Listeners\HandlePaymentFailure::class,
    ],
];
```

---

📦 Local Payment Records
-----------------------

[](#-local-payment-records)

Every payment initiated through the plugin creates a row in the `paynexus_payments` table.

```
use PayNexus\Models\PaynexusPayment;

// Find by reference
$payment = PaynexusPayment::where('reference', 'PNXABCD1234')->first();

// Find by checkout request ID
$payment = PaynexusPayment::where('checkout_request_id', 'ws_CO_...')->first();

// Query scopes
$pending   = PaynexusPayment::pending()->get();
$completed = PaynexusPayment::completed()->get();
$failed    = PaynexusPayment::failed()->get();

// Check state
$payment->isPending();   // true/false
$payment->isCompleted(); // true/false
$payment->isTerminal();  // completed, failed, or timeout
$payment->isVerified();  // true/false - if payment was verified with provider
$payment->isManuallyConfirmed(); // true/false - if manually confirmed by admin
$payment->canRetry();    // true/false - if failed payment can be retried

// Helper methods
$payment->markCompleted($transactionId, $providerReference);
$payment->markFailed($reason);
$payment->markVerified($verifiedAmount, $verifiedPhone, $verificationMethod);
$payment->markManuallyConfirmed('admin@example.com');

// Link to your models via polymorphic relation
$payment->payable; // → App\Models\Order, App\Models\Invoice, etc.
```

### 🔍 Payment Verification

[](#-payment-verification)

The plugin tracks verification data from the payment provider:

```
$payment = PaynexusPayment::where('reference', 'PNXABCD1234')->first();

// Check if payment has been verified with provider
if ($payment->isVerified()) {
    echo "Verified on: " . $payment->verified_date->format('Y-m-d H:i:s');
    echo "Verified amount: " . $payment->verified_amount;
    echo "Verification method: " . $payment->verification_method;
}

// Manually mark as verified (e.g., after checking with bank)
$payment->markVerified(
    verifiedAmount: 1500.00,
    verifiedPhone: '254712345678',
    verificationMethod: 'bank_statement'
);
```

### 👤 Manual Confirmation

[](#-manual-confirmation)

For payments that need manual review:

```
// Mark payment as manually confirmed by an admin
$payment->markManuallyConfirmed('admin@example.com');

// Check if manually confirmed
if ($payment->isManuallyConfirmed()) {
    echo "Confirmed by: " . $payment->confirmed_by;
    echo "Confirmed at: " . $payment->confirmed_at->format('Y-m-d H:i:s');
}
```

### 🔗 Polymorphic Relationship

[](#-polymorphic-relationship)

Link payments to any of your models:

```
// In your Order model
class Order extends Model
{
    public function payments()
    {
        return $this->morphMany(\PayNexus\Models\PaynexusPayment::class, 'payable');
    }
}

// When initiating payment, set payable after creation:
$result = PayNexus::initiatePayment([...]);

if ($result['success']) {
    $localPayment = PaynexusPayment::where('checkout_request_id', $result['data']['checkout_request_id'])->first();
    $localPayment->update([
        'payable_type' => Order::class,
        'payable_id'   => $order->id,
    ]);
}
```

---

🛒 Ecommerce Integration Guide
-----------------------------

[](#-ecommerce-integration-guide)

### 📦 Order Checkout Flow

[](#-order-checkout-flow)

Here is a complete end-to-end checkout flow for an ecommerce store.

**Step 1: Order Model Setup**

```
// app/Models/Order.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use PayNexus\Models\PaynexusPayment;

class Order extends Model
{
    protected $fillable = [
        'user_id', 'order_number', 'total', 'currency', 'status',
        'customer_name', 'customer_email', 'customer_phone',
    ];

    public function payments()
    {
        return $this->morphMany(PaynexusPayment::class, 'payable');
    }

    public function latestPayment()
    {
        return $this->morphOne(PaynexusPayment::class, 'payable')->latestOfMany();
    }

    public function isPaid(): bool
    {
        return $this->payments()->completed()->exists();
    }

    public function markAsPaid(): void
    {
        $this->update(['status' => 'paid']);
    }
}
```

**Step 2: Orders Migration**

```
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('order_number')->unique();
    $table->decimal('total', 14, 2);
    $table->string('currency', 10)->default('KES');
    $table->string('status')->default('pending'); // pending, paid, shipped, cancelled
    $table->string('customer_name');
    $table->string('customer_email')->nullable();
    $table->string('customer_phone');
    $table->timestamps();
});
```

### 🛍️ Cart Checkout Controller

[](#️-cart-checkout-controller)

```
// app/Http/Controllers/CheckoutController.php
namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\Request;
use PayNexus\Facades\PayNexus;
use PayNexus\Models\PaynexusPayment;

class CheckoutController extends Controller
{
    /**
     * Step 1: Show checkout page with cart summary.
     */
    public function show(Request $request)
    {
        $cart = $request->user()->cart; // your cart logic
        return view('checkout', compact('cart'));
    }

    /**
     * Step 2: Create order and initiate M-Pesa payment.
     */
    public function pay(Request $request)
    {
        $request->validate([
            'phone' => 'required|string',
        ]);

        // Create the order
        $order = Order::create([
            'user_id'        => $request->user()->id,
            'order_number'   => 'ORD-' . strtoupper(uniqid()),
            'total'          => $request->user()->cart->total(),
            'customer_name'  => $request->user()->name,
            'customer_email' => $request->user()->email,
            'customer_phone' => $request->phone,
        ]);

        // Initiate M-Pesa STK Push
        $result = PayNexus::initiatePayment([
            'amount'            => $order->total,
            'phone'             => $request->phone,
            'account_reference' => $order->order_number,
            'description'       => "Payment for {$order->order_number}",
            'metadata'          => ['order_id' => $order->id],
        ]);

        if (!$result['success']) {
            return back()->withErrors(['payment' => $result['message'] ?? 'Payment initiation failed.']);
        }

        // Link the local PaynexusPayment to the order
        $checkoutRequestId = $result['data']['checkout_request_id'];
        $localPayment = PaynexusPayment::where('checkout_request_id', $checkoutRequestId)->first();

        if ($localPayment) {
            $localPayment->update([
                'payable_type' => Order::class,
                'payable_id'   => $order->id,
            ]);
        }

        // Redirect to a page that polls for payment status
        return redirect()->route('checkout.status', [
            'order'               => $order->id,
            'checkout_request_id' => $checkoutRequestId,
        ]);
    }

    /**
     * Step 3: Show payment status page (polls via AJAX or Livewire).
     */
    public function status(Request $request, Order $order)
    {
        return view('checkout.status', [
            'order'              => $order,
            'checkoutRequestId'  => $request->query('checkout_request_id'),
        ]);
    }

    /**
     * AJAX endpoint: check payment status from the browser.
     */
    public function checkStatus(Request $request)
    {
        $request->validate(['checkout_request_id' => 'required|string']);

        $result = PayNexus::getPaymentByCheckoutId($request->checkout_request_id);

        $status = $result['data']['status'] ?? 'pending';

        // If completed, mark the order as paid
        if ($status === 'completed') {
            $local = PaynexusPayment::where('checkout_request_id', $request->checkout_request_id)->first();
            if ($local && $local->payable) {
                $local->payable->markAsPaid();
            }
        }

        return response()->json([
            'status'  => $status,
            'data'    => $result['data'] ?? [],
        ]);
    }
}
```

**Routes:**

```
// routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/checkout', [CheckoutController::class, 'show'])->name('checkout');
    Route::post('/checkout/pay', [CheckoutController::class, 'pay'])->name('checkout.pay');
    Route::get('/checkout/{order}/status', [CheckoutController::class, 'status'])->name('checkout.status');
    Route::post('/checkout/check-status', [CheckoutController::class, 'checkStatus'])->name('checkout.check-status');
});
```

### ⚡ Payment Status Page (Livewire)

[](#-payment-status-page-livewire)

If you use Livewire, here is a component that polls automatically:

```
// app/Livewire/PaymentStatus.php
namespace App\Livewire;

use Livewire\Component;
use PayNexus\Facades\PayNexus;
use PayNexus\Models\PaynexusPayment;

class PaymentStatus extends Component
{
    public string $checkoutRequestId;
    public string $status = 'pending';
    public ?string $transactionId = null;
    public ?string $reference = null;

    public function mount(string $checkoutRequestId)
    {
        $this->checkoutRequestId = $checkoutRequestId;
        $this->checkPaymentStatus();
    }

    public function checkPaymentStatus(): void
    {
        $result = PayNexus::getPaymentByCheckoutId($this->checkoutRequestId);

        $this->status = $result['data']['status'] ?? 'pending';
        $this->transactionId = $result['data']['provider_transaction_id'] ?? null;
        $this->reference = $result['data']['reference'] ?? null;

        if ($this->status === 'completed') {
            $local = PaynexusPayment::where('checkout_request_id', $this->checkoutRequestId)->first();
            if ($local && $local->payable && method_exists($local->payable, 'markAsPaid')) {
                $local->payable->markAsPaid();
            }
        }
    }

    public function render()
    {
        return view('livewire.payment-status');
    }
}
```

```
{{-- resources/views/livewire/payment-status.blade.php --}}

    @if ($status === 'pending')

            Waiting for payment...
            Check your phone and enter your M-Pesa PIN to complete the payment.

    @elseif ($status === 'completed')

            Payment Successful!
            @if ($transactionId)
                M-Pesa Receipt: {{ $transactionId }}
            @endif
            @if ($reference)
                Reference: {{ $reference }}
            @endif

    @elseif ($status === 'failed')

            Payment Failed
            The payment was not completed. Please try again.

                Try Again

    @endif

```

### 🎧 Handling Webhooks in Your App

[](#-handling-webhooks-in-your-app)

```
// app/Listeners/HandlePaymentSuccess.php
namespace App\Listeners;

use PayNexus\Events\PaymentCompleted;

class HandlePaymentSuccess
{
    public function handle(PaymentCompleted $event): void
    {
        $payment = $event->payment;

        // Update the linked order
        if ($payment->payable && method_exists($payment->payable, 'markAsPaid')) {
            $payment->payable->markAsPaid();
        }

        // Send confirmation email, SMS, etc.
        // Mail::to($payment->payable->customer_email)->send(new OrderConfirmation($payment->payable));
    }
}
```

```
// app/Listeners/HandlePaymentFailure.php
namespace App\Listeners;

use PayNexus\Events\PaymentFailed;

class HandlePaymentFailure
{
    public function handle(PaymentFailed $event): void
    {
        $payment = $event->payment;

        // Update order status
        if ($payment->payable) {
            $payment->payable->update(['status' => 'payment_failed']);
        }

        // Notify the customer
        // Notification::send($payment->payable->user, new PaymentFailedNotification($event->reason));
    }
}
```

### 🔄 Subscription Integration

[](#-subscription-integration)

For subscription-based applications:

**Subscription Model:**

```
// app/Models/Subscription.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use PayNexus\Models\PaynexusPayment;

class Subscription extends Model
{
    protected $fillable = [
        'user_id',
        'plan_id',
        'status',
        'expires_at',
    ];

    public function payments()
    {
        return $this->morphMany(PaynexusPayment::class, 'payable');
    }

    public function activate()
    {
        $this->update([
            'status' => 'active',
            'expires_at' => now()->addMonth(),
        ]);
    }
}
```

**Subscription Payment Controller:**

```
// app/Http/Controllers/SubscriptionController.php
public function renew(Request $request)
{
    $subscription = $request->user()->subscription;

    $result = PayNexus::initiatePayment([
        'amount' => 1000,
        'phone' => $request->user()->phone,
        'account_reference' => "SUB-{$subscription->id}",
        'description' => 'Subscription renewal',
    ]);

    if ($result['success']) {
        $payment = PaynexusPayment::where(
            'checkout_request_id',
            $result['data']['checkout_request_id']
        )->first();

        $payment->update([
            'payable_type' => Subscription::class,
            'payable_id' => $subscription->id,
        ]);
    }

    return back();
}
```

### � Manual Payment Verification

[](#-manual-payment-verification)

For payments that require manual admin review:

```
// app/Http/Controllers/Admin/PaymentController.php
public function verifyPayment(Request $request, $id)
{
    $payment = PaynexusPayment::findOrFail($id);

    $payment->markVerified(
        verifiedAmount: $request->verified_amount,
        verifiedPhone: $request->verified_phone,
        verificationMethod: 'manual_admin'
    );

    return back()->with('success', 'Payment verified successfully');
}

public function confirmPayment(Request $request, $id)
{
    $payment = PaynexusPayment::findOrFail($id);

    $payment->markManuallyConfirmed(auth()->user()->email);

    // Update linked order
    if ($payment->payable && method_exists($payment->payable, 'markAsPaid')) {
        $payment->payable->markAsPaid();
    }

    return back()->with('success', 'Payment confirmed successfully');
}
```

---

⚠️ Error Handling
-----------------

[](#️-error-handling)

The plugin provides specific exceptions for different error scenarios:

```
use PayNexus\Exceptions\PayNexusAuthException;
use PayNexus\Exceptions\PayNexusConnectionException;
use PayNexus\Exceptions\PayNexusApiException;

try {
    $result = PayNexus::initiatePayment([
        'amount' => 1000,
        'phone' => '254712345678',
        'account_reference' => 'TEST',
    ]);

    if (!$result['success']) {
        Log::error('Payment initiation failed', [
            'message' => $result['message'],
            'error' => $result['error'] ?? null,
        ]);
        return back()->with('error', 'Payment failed. Please try again.');
    }

} catch (PayNexusAuthException $e) {
    Log::error('Authentication error', ['message' => $e->getMessage()]);
    return back()->with('error', 'API authentication failed. Check your API keys.');

} catch (PayNexusConnectionException $e) {
    Log::error('Connection error', ['message' => $e->getMessage()]);
    return back()->with('error', 'Could not connect to payment gateway. Please try again.');

} catch (PayNexusApiException $e) {
    Log::error('API error', ['message' => $e->getMessage()]);
    return back()->with('error', 'Payment gateway error. Please try again.');
}
```

---

🧪 Testing
---------

[](#-testing)

### Mocking HTTP Requests

[](#mocking-http-requests)

```
// tests/Feature/PaymentTest.php
use PayNexus\Facades\PayNexus;
use Illuminate\Support\Facades\Http;

test('payment can be initiated', function () {
    // Mock the HTTP client
    Http::fake([
        'paynexus.co.ke/*' => Http::response([
            'success' => true,
            'data' => [
                'payment_id' => 123,
                'reference' => 'PNXTEST',
                'checkout_request_id' => 'ws_CO_test',
            ],
        ]),
    ]);

    $result = PayNexus::initiatePayment([
        'amount' => 1000,
        'phone' => '254712345678',
        'account_reference' => 'TEST',
    ]);

    expect($result['success'])->toBeTrue();
    expect($result['data']['checkout_request_id'])->toBe('ws_CO_test');
});
```

---

🌐 JavaScript Polling
--------------------

[](#-javascript-polling)

For client-side payment status polling:

```
// resources/js/payment-status.js
function pollPaymentStatus(checkoutRequestId) {
    const interval = setInterval(async () => {
        try {
            const response = await fetch(`/api/payment/status/${checkoutRequestId}`);
            const data = await response.json();

            if (data.status === 'completed') {
                clearInterval(interval);
                window.location.href = '/payment/success';
            } else if (data.status === 'failed') {
                clearInterval(interval);
                window.location.href = '/payment/failed';
            }
        } catch (error) {
            console.error('Error checking payment status:', error);
        }
    }, 3000); // Check every 3 seconds
}

// Usage
pollPaymentStatus('ws_CO_123456789');
```

---

� Custom Webhook Handler
------------------------

[](#-custom-webhook-handler)

If you need custom webhook handling beyond the built-in controller:

```
// app/Http/Controllers/CustomWebhookController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use PayNexus\Facades\PayNexus;
use PayNexus\Models\PaynexusPayment;

class CustomWebhookController extends Controller
{
    public function handle(Request $request)
    {
        // Verify signature
        $signature = $request->header('X-PayNexus-Signature');
        $payload = $request->getContent();
        $secret = config('paynexus.webhook.secret');

        $expected = hash_hmac('sha256', $payload, $secret);

        if (!hash_equals($expected, $signature)) {
            return response()->json(['error' => 'Invalid signature'], 403);
        }

        $data = $request->all();
        $event = $data['event'] ?? 'unknown';

        // Find or create payment
        $payment = PaynexusPayment::where(
            'reference',
            $data['data']['reference'] ?? null
        )->first();

        if (!$payment) {
            $payment = PaynexusPayment::create([
                'reference' => $data['data']['reference'],
                'amount' => $data['data']['amount'],
                'status' => $event === 'payment.completed' ? 'completed' : 'failed',
            ]);
        }

        // Handle event
        if ($event === 'payment.completed') {
            $payment->markCompleted(
                $data['data']['transaction_id'] ?? null,
                $data['data']['provider_reference'] ?? null
            );
        }

        return response()->json(['received' => true]);
    }
}
```

Register your custom webhook route:

```
// routes/web.php
Route::post('/webhooks/paynexus', [CustomWebhookController::class, 'handle'])
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
```

---

�� Configuration Reference
--------------------------

[](#-configuration-reference)

KeyEnv VariableDefaultDescription`secret_key``PAYNEXUS_SECRET_KEY`—Your PayNexus secret key (`sk_…`) - Required for write operations`public_key``PAYNEXUS_PUBLIC_KEY`—Your PayNexus public key (`pk_…`) - Optional, for read operations`base_url``PAYNEXUS_BASE_URL``https://paynexus.co.ke`PayNexus API base URL`currency``PAYNEXUS_CURRENCY``KES`Default currency`webhook.secret``PAYNEXUS_WEBHOOK_SECRET`—HMAC secret for webhook signature verification`webhook.path``PAYNEXUS_WEBHOOK_PATH``/paynexus/webhook`Webhook receiver route`webhook.tolerance`—`300`Max age (seconds) for webhook timestamp`polling.interval``PAYNEXUS_POLL_INTERVAL``3`Seconds between `pollStatus` queries`polling.timeout``PAYNEXUS_POLL_TIMEOUT``120`Max seconds `pollStatus` will block`http.timeout``PAYNEXUS_HTTP_TIMEOUT``30`HTTP request timeout (seconds)`http.retry_times``PAYNEXUS_HTTP_RETRIES``2`Number of retries on connection failure`http.retry_sleep``PAYNEXUS_HTTP_RETRY_SLEEP``500`Milliseconds between retries`log_channel``PAYNEXUS_LOG_CHANNEL``null`Custom log channel`queue.webhooks``PAYNEXUS_QUEUE_WEBHOOKS``false`Enable queue processing for webhooks`queue.connection``PAYNEXUS_QUEUE_CONNECTION``default`Queue connection for webhooks`queue.queue``PAYNEXUS_QUEUE_NAME``default`Queue name for webhooks`retry.webhook_max_attempts``PAYNEXUS_WEBHOOK_MAX_ATTEMPTS``3`Max retry attempts for webhook jobs`retry.webhook_backoff``PAYNEXUS_WEBHOOK_BACKOFF``exponential`Retry backoff strategy (linear, exponential, constant)`retry.webhook_base_delay``PAYNEXUS_WEBHOOK_BASE_DELAY``1000`Base delay in milliseconds for retries### Advanced Configuration Options

[](#advanced-configuration-options)

**Queue Webhooks:**

```
PAYNEXUS_QUEUE_WEBHOOKS=true
PAYNEXUS_QUEUE_CONNECTION=redis
PAYNEXUS_QUEUE_NAME=webhooks
```

**Retry Configuration:**

```
PAYNEXUS_WEBHOOK_MAX_ATTEMPTS=5
PAYNEXUS_WEBHOOK_BACKOFF=exponential  # linear, exponential, or constant
PAYNEXUS_WEBHOOK_BASE_DELAY=2000     # milliseconds
```

**Custom Webhook Path:**

```
PAYNEXUS_WEBHOOK_PATH=/custom/webhook/path
```

**Custom Polling Settings:**

```
PAYNEXUS_POLL_INTERVAL=5  # Check every 5 seconds
PAYNEXUS_POLL_TIMEOUT=180 # Timeout after 3 minutes
```

**HTTP Client Settings:**

```
PAYNEXUS_HTTP_TIMEOUT=60      # 60 second timeout
PAYNEXUS_HTTP_RETRIES=3        # Retry 3 times
PAYNEXUS_HTTP_RETRY_SLEEP=1000 # Wait 1 second between retries
```

**Custom Log Channel:**

```
PAYNEXUS_LOG_CHANNEL=paynexus
```

Then in `config/logging.php`:

```
'channels' => [
    'paynexus' => [
        'driver' => 'daily',
        'path' => storage_path('logs/paynexus.log'),
        'level' => 'info',
    ],
],
```

🔑 Getting API Keys
------------------

[](#-getting-api-keys)

To use this plugin, you need API keys from the PayNexus platform:

1. **Register** at [paynexus.co.ke](https://paynexus.co.ke) to create an account
2. **Choose a subscription plan** that fits your needs
3. **Navigate to the merchant panel** after registration
4. **Create a business** in your merchant dashboard
5. **Add a payment account** (e.g., M-Pesa till number) to your business
6. **Navigate to API Keys** and select the appropriate API key for the specific payment account you created
    - **Secret Key (`sk_`)**: For initiating payments and write operations. Keep this server-side only.
    - **Public Key (`pk_`)**: For read operations like checking payment status. Safe to use in client-side code.
7. **Configure webhook** in PayNexus dashboard pointing to `https://yourapp.com/paynexus/webhook`
8. **Copy the webhook secret** and set `PAYNEXUS_WEBHOOK_SECRET` in your `.env`

💬 Support
---------

[](#-support)

- **Documentation**: [README.md](https://github.com/MCBANKSKE/paynexus-laravel-plugin/blob/main/README.md)
- **Issues**: [GitHub Issues](https://github.com/MCBANKSKE/paynexus-laravel-plugin/issues)
- **Email**:
- **Platform**: [paynexus.co.ke](https://paynexus.co.ke)

---

🔧 Troubleshooting
-----------------

[](#-troubleshooting)

### "PayNexus secret key is required"

[](#paynexus-secret-key-is-required)

- Set `PAYNEXUS_SECRET_KEY` (or `PAYNEXUS_API_KEY` for backwards compatibility) in your `.env`.
- Run `php artisan config:clear` after changing environment variables.

### "Unable to reach PayNexus API"

[](#unable-to-reach-paynexus-api)

- Check your internet connection.
- Verify `PAYNEXUS_BASE_URL` is correct (`https://paynexus.co.ke`, no trailing `/api`).
- Test connectivity: `curl -H "X-API-Key: sk_..." https://paynexus.co.ke/api/merchant`

### "Invalid PayNexus webhook signature"

[](#invalid-paynexus-webhook-signature)

- Ensure `PAYNEXUS_WEBHOOK_SECRET` matches the secret shown in PayNexus when you created the webhook.
- The webhook URL must be publicly accessible (PayNexus needs to reach it).

### Payment stays "pending"

[](#payment-stays-pending)

- The customer may not have confirmed the STK push on their phone.
- Call `PayNexus::checkMpesaStatus($checkoutRequestId)` to query M-Pesa directly.
- Check that your PayNexus callback URL is correctly configured in the M-Pesa Daraja portal.

### Local record not updating

[](#local-record-not-updating)

- Ensure your webhook endpoint is correctly registered with PayNexus.
- Check `storage/logs/laravel.log` for `[PayNexus Webhook]` entries.
- Verify your `.env` has `PAYNEXUS_WEBHOOK_SECRET` set correctly.
- You can manually sync by calling `PayNexus::getPaymentByCheckoutId(...)`.

---

📄 License
---------

[](#-license)

MIT — see [LICENSE](LICENSE).

---

**Built with ❤️ for the Laravel community**

[PayNexus](https://paynexus.co.ke) • [GitHub](https://github.com/MCBANKSKE/paynexus-laravel-plugin)

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance88

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

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

18

Last Release

59d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/154880732?v=4)[MCBANKS](/maintainers/mcbankske)[@MCBANKSKE](https://github.com/MCBANKSKE)

---

Tags

laravelpaymentecommercepayment gatewaympesamobile-moneykenyastk-pushpaynexus

### Embed Badge

![Health badge](/badges/paynexus-laravel-paynexus/health.svg)

```
[![Health](https://phpackages.com/badges/paynexus-laravel-paynexus/health.svg)](https://phpackages.com/packages/paynexus-laravel-paynexus)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)

PHPackages © 2026

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