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

ActiveLibrary[Payment Processing](/categories/payments)

shadrackjm/laravel-snippe
=========================

A Laravel package for integrating Snippe Tanzania payment gateway — Mobile Money, Cards, and QR payments.

1.0.3(2mo ago)115↓91.7%MITPHPPHP ^8.2

Since Mar 24Pushed 2mo agoCompare

[ Source](https://github.com/shadrackjm/laravel-snippe)[ Packagist](https://packagist.org/packages/shadrackjm/laravel-snippe)[ Docs](https://github.com/shadrackjm/laravel-snippe)[ RSS](/packages/shadrackjm-laravel-snippe/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (12)Versions (5)Used By (0)

Laravel Snippe
==============

[](#laravel-snippe)

[![Latest Version on Packagist](https://camo.githubusercontent.com/6b0c80fa1e25938e78d8eabffaab95c169513e02677fff0cfcd7dbd15fe41e38/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f736861647261636b6a6d2f6c61726176656c2d736e697070652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/shadrackjm/laravel-snippe)[![PHP Version](https://camo.githubusercontent.com/c9f64f714c636ba27a3bba6dfd52f98426832db1262747efa54b212d16943651/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d626c7565)](https://php.net)[![Laravel](https://camo.githubusercontent.com/5b8e585c1631d63a029f7f50b754c8f9a8303cf45ce980700f570cbe5836adf9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c61726176656c2d3131253230253743253230313225323025374325323031332d726564)](https://laravel.com)[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](LICENSE)

A clean, expressive Laravel package for integrating the **Snippe Tanzania** payment gateway. Supports **Mobile Money** (Airtel, M-Pesa, Halotel, Mixx), **Bank Transfer**, **Card payments**, **Dynamic QR** codes, and **Payment Sessions** (hosted checkout) — with webhook signature verification and a fluent builder API.

---

Requirements
------------

[](#requirements)

DependencyVersionPHP^8.2Laravel^11.0 | ^12.0 | ^13.0---

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

[](#installation)

```
composer require shadrackjm/laravel-snippe
```

> Package discovery is automatic — no need to manually register the service provider.

### Publish the config file

[](#publish-the-config-file)

```
php artisan vendor:publish --tag=snippe-config
```

This creates `config/snippe.php` in your application.

### Add credentials to `.env`

[](#add-credentials-to-env)

```
SNIPPE_API_KEY=snp_your_live_key_here
SNIPPE_BASE_URL=https://api.snippe.sh/v1
SNIPPE_WEBHOOK_URL=https://yourapp.com/snippe/webhook
SNIPPE_WEBHOOK_SECRET=your_webhook_signing_secret
SNIPPE_WEBHOOK_PATH=snippe/webhook
SNIPPE_CURRENCY=TZS
SNIPPE_TIMEOUT=30
```

> Find `SNIPPE_WEBHOOK_SECRET` in your Snippe Dashboard → Webhooks → Signing Secret.

### Exclude webhook route from CSRF

[](#exclude-webhook-route-from-csrf)

In **Laravel 11 / 12 / 13** (`bootstrap/app.php`):

```
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'snippe/*',
    ]);
})
```

Or if you register your own route (see [Custom Webhook Controller](#custom-webhook-controller)):

```
Route::post('/webhooks/snippe', [SnippeWebhookController::class, 'handle'])
    ->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class);
```

---

Quick Start
-----------

[](#quick-start)

```
use ShadrackJm\Snippe\Facades\Snippe;

// Mobile Money — USSD push sent to customer's phone
$payment = Snippe::mobileMoney(5000, '0754123456')
    ->customer('John Doe', 'john@email.com')
    ->send();

echo $payment->reference(); // "9015c155-9e29-..."
echo $payment->status();    // "pending"
```

---

Usage
-----

[](#usage)

### Payment Sessions (Hosted Checkout) — Recommended

[](#payment-sessions-hosted-checkout--recommended)

A Payment Session creates a Snippe-hosted checkout page. Redirect your customer there to pay. This is the recommended approach for subscriptions and one-off charges because you do not need to handle card/mobile-money UI yourself.

```
use ShadrackJm\Snippe\Facades\Snippe;

$session = Snippe::session(50000)
    ->customer('John Doe', 'john@email.com')
    ->allowedMethods(['mobile_money', 'bank_transfer']) // restrict payment methods
    ->redirectTo(
        route('subscription.success'),
        route('subscription.cancel')
    )
    ->webhook(route('snippe.webhook'))
    ->metadata(['subscription_id' => 42, 'plan_id' => 3, 'company_id' => 12])
    ->expiresIn(1800) // 30-minute checkout window
    ->send();

// Redirect the customer to Snippe's hosted checkout page
return redirect($session->checkoutUrl());
```

When payment completes, Snippe fires your webhook with the metadata you attached, allowing you to activate the subscription automatically on the server side.

#### Payment Session Builder Methods

[](#payment-session-builder-methods)

MethodSignatureDescription`session``session(?int $amount)`Start a session (omit amount to allow customer input)`customer``->customer(string $name, string $email)`Attach customer info`allowedMethods``->allowedMethods(array $methods)`Restrict to specific methods: `mobile_money`, `bank_transfer`, `card`, `qr``redirectTo``->redirectTo(string $success, string $cancel)`URLs Snippe redirects to after payment`webhook``->webhook(string $url)`Override the webhook URL for this session`metadata``->metadata(array $data)`Arbitrary key-value data included in webhook payload`expiresIn``->expiresIn(int $seconds)`Session expiry window (default: 3600)`allowCustomAmount``->allowCustomAmount()`Let the customer enter their own amount (e.g. top-ups)`send``->send()`Execute — returns a `PaymentSession` object#### Allow custom amount (e.g. top-ups)

[](#allow-custom-amount-eg-top-ups)

```
$session = Snippe::session()
    ->allowCustomAmount()
    ->customer('John Doe', 'john@email.com')
    ->redirectTo(route('topup.success'))
    ->send();

return redirect($session->checkoutUrl());
```

---

### Mobile Money

[](#mobile-money)

```
$payment = Snippe::mobileMoney(5000, '0754123456')
    ->customer('John Doe', 'john@email.com')
    ->webhook('https://yourapp.com/snippe/webhook')
    ->metadata(['order_id' => 'ORD-123'])
    ->description('Order #123 from My Shop')
    ->idempotencyKey('order-123-attempt-1')
    ->send();

// The customer receives a USSD push. You get a webhook when done.
```

### Bank Transfer

[](#bank-transfer)

```
$session = Snippe::session(100000)
    ->customer('John Doe', 'john@email.com')
    ->allowedMethods(['bank_transfer'])
    ->redirectTo(route('payment.success'), route('payment.cancel'))
    ->metadata(['invoice_id' => 'INV-001'])
    ->send();

return redirect($session->checkoutUrl());
```

### Card Payment

[](#card-payment)

```
$payment = Snippe::card(10000)
    ->phone('0754123456')
    ->customer('Jane Doe', 'jane@email.com')
    ->billing('123 Main Street', 'Dar es Salaam', 'DSM', '14101', 'TZ')
    ->redirectTo('https://yourapp.com/success', 'https://yourapp.com/cancel')
    ->send();

// Redirect the customer to the secure checkout page
return redirect($payment->paymentUrl());
```

### Dynamic QR Code

[](#dynamic-qr-code)

```
$payment = Snippe::qr(5000)
    ->customer('John Doe', 'john@email.com')
    ->redirectTo('https://yourapp.com/success', 'https://yourapp.com/cancel')
    ->send();

$qrData     = $payment->qrCode();    // Render as a QR image
$paymentUrl = $payment->paymentUrl(); // Or redirect to hosted page
```

### Raw Array Payloads

[](#raw-array-payloads)

```
// Generic payment with a full array
$payment = Snippe::createPayment([
    'payment_type' => 'mobile',
    'details'      => ['amount' => 5000, 'currency' => 'TZS'],
    'phone_number' => '255754123456',
    'customer'     => ['firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@mail.com'],
]);

// Convenience wrapper for mobile payments
$payment = Snippe::initiateMobilePayment([
    'amount'       => 5000,
    'phone_number' => '0754123456',
    'customer'     => ['firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@mail.com'],
]);
```

---

Payment Operations
------------------

[](#payment-operations)

### Verify / Check Status

[](#verify--check-status)

```
$payment = Snippe::verifyTransaction('9015c155-9e29-4e8e-8fe6-d5d81553c8e6');
// Or alias:
$payment = Snippe::find('9015c155-...');

echo $payment->status();      // "completed"
echo $payment->amount();      // 5000
echo $payment->completedAt(); // "2026-01-25T00:50:44Z"
```

### List Payments

[](#list-payments)

```
$result = Snippe::payments(limit: 20, offset: 0);
```

### Account Balance

[](#account-balance)

```
$balance   = Snippe::balance();
$available = $balance['data']['available']['value'];    // e.g. 6943
$currency  = $balance['data']['available']['currency']; // "TZS"
```

### Retry USSD Push

[](#retry-ussd-push)

```
Snippe::push('payment-reference-uuid');
Snippe::push('payment-reference-uuid', '+255787654321'); // Different number
```

---

Object Reference
----------------

[](#object-reference)

### Payment Object

[](#payment-object)

MethodReturnsDescription`reference()``?string`Unique payment UUID`status()``?string``pending` | `completed` | `failed` | `expired` | `voided``paymentType()``?string``mobile` | `card` | `dynamic-qr``amount()``?int`Payment amount in TZS`currency()``?string``TZS``isPending()``bool``isCompleted()``bool``isFailed()``bool``isExpired()``bool``isVoided()``bool``paymentUrl()``?string`Checkout URL (card / QR)`qrCode()``?string`QR data string`fees()``?int`Transaction fees (available after completion)`netAmount()``?int`Amount after fees`customer()``array`Customer info (`name`, `email`, `phone`)`completedAt()``?string`Completion timestamp (ISO 8601)`createdAt()``?string`Creation timestamp (ISO 8601)`toArray()``array`Full raw API response### Payment Session Object

[](#payment-session-object)

MethodReturnsDescription`reference()``?string`Session reference e.g. `sess_abc123``status()``?string``pending` | `completed` | `expired` | `cancelled``checkoutUrl()``?string`Hosted checkout URL — redirect the customer here`paymentLinkUrl()``?string`Shareable short link`shortCode()``?string`Abbreviated session code`amount()``?int`Fixed session amount (null when `allowCustomAmount` is set)`currency()``?string``TZS``isPending()``bool``isCompleted()``bool``isExpired()``bool``isCancelled()``bool``expiresAt()``?string`Session expiry timestamp (ISO 8601)`completedAt()``?string`Completion timestamp (ISO 8601)`toArray()``array`Full raw API response### Webhook Object

[](#webhook-object)

MethodReturnsDescription`fromRaw(string $body, array $headers)``static`Parse raw webhook payload`verifyOrFail(string $secret)``void`Throws `SnippeException` if signature is invalid`verify(string $secret)``bool`Returns false instead of throwing`isPaymentCompleted()``bool`True when event type is `payment.completed``isPaymentFailed()``bool`True when event type is `payment.failed``reference()``?string`Payment reference from the event`amount()``?int`Payment amount`currency()``?string``TZS``metadata()``array`The metadata array you attached to the session/payment`toArray()``array`Full raw webhook payload---

Phone Number Normalisation
--------------------------

[](#phone-number-normalisation)

The builder automatically normalises any Tanzanian number format:

```
0754123456      → 255754123456  ✓ local
+255754123456   → 255754123456  ✓ international with +
255754123456    → 255754123456  ✓ already normalised
754123456       → 255754123456  ✓ no prefix
0754 123 456    → 255754123456  ✓ with spaces
0754-123-456    → 255754123456  ✓ with dashes

```

---

Webhooks
--------

[](#webhooks)

### Built-in Route

[](#built-in-route)

Set `SNIPPE_WEBHOOK_PATH=snippe/webhook` and point your Snippe dashboard to:

```
https://yourapp.com/snippe/webhook

```

The package registers this route automatically and fires Laravel events:

```
// In a service provider boot() method
Event::listen('snippe.payment.completed', function (array $data) {
    // $data: reference, amount, currency, customer, metadata, payload
    $subscription = Subscription::find($data['metadata']['subscription_id']);
    $subscription->activate();
});

Event::listen('snippe.payment.failed', function (array $data) {
    // Handle failure — notify user, cancel pending record, etc.
});
```

### Custom Webhook Controller

[](#custom-webhook-controller)

Disable the built-in route with `SNIPPE_WEBHOOK_PATH=false` and register your own:

```
// routes/web.php
Route::post('/webhooks/snippe', [SnippeWebhookController::class, 'handle'])
    ->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class);
```

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

use App\Models\PaymentTransaction;
use App\Models\Subscription;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use ShadrackJm\Snippe\Exceptions\SnippeException;
use ShadrackJm\Snippe\Webhook;

class SnippeWebhookController extends Controller
{
    public function handle(Request $request): JsonResponse
    {
        try {
            $event = Webhook::fromRaw($request->getContent(), $request->headers->all());
        } catch (SnippeException $e) {
            return response()->json(['error' => 'Invalid payload'], 400);
        }

        // Verify signature
        $secret = config('snippe.webhook_secret');
        if ($secret) {
            try {
                $event->verifyOrFail($secret);
            } catch (SnippeException $e) {
                return response()->json(['error' => 'Invalid signature'], 401);
            }
        }

        $payload = $event->toArray();

        if ($event->isPaymentCompleted()) {
            $subscriptionId = $payload['data']['metadata']['subscription_id'] ?? null;
            $subscription   = Subscription::find($subscriptionId);

            if ($subscription && $subscription->status === 'pending') {
                // Record the transaction
                PaymentTransaction::create([
                    'reference'       => $payload['data']['reference'] ?? null,
                    'amount'          => $payload['data']['amount']['value'] ?? 0,
                    'currency'        => $payload['data']['amount']['currency'] ?? 'TZS',
                    'status'          => 'completed',
                    'channel'         => $payload['data']['channel']['type'] ?? null,
                    'provider'        => $payload['data']['channel']['provider'] ?? null,
                    'customer_name'   => $payload['data']['customer']['name'] ?? null,
                    'customer_phone'  => $payload['data']['customer']['phone'] ?? null,
                    'metadata'        => $payload,
                    'completed_at'    => now(),
                ]);

                // Auto-activate the subscription
                $subscription->activate();
            }
        }

        if ($event->isPaymentFailed()) {
            $subscriptionId = $payload['data']['metadata']['subscription_id'] ?? null;
            Subscription::where('id', $subscriptionId)
                ->where('status', 'pending')
                ->update(['status' => 'cancelled', 'cancelled_at' => now()]);
        }

        // Always return 200 so Snippe stops retrying
        return response()->json(['status' => 'received']);
    }
}
```

### Signature Verification

[](#signature-verification)

```
use ShadrackJm\Snippe\Webhook;

$event = Webhook::fromRaw($request->getContent(), $request->headers->all());

// Option A — throws SnippeException if invalid
$event->verifyOrFail(config('snippe.webhook_secret'));

// Option B — returns bool
if (! $event->verify(config('snippe.webhook_secret'))) {
    return response()->json(['error' => 'Invalid signature'], 401);
}
```

---

Subscription Integration Pattern
--------------------------------

[](#subscription-integration-pattern)

The recommended end-to-end pattern for SaaS subscription billing:

```
User clicks "Subscribe"
       │
       ▼
Controller creates a pending Subscription record
       │
       ▼
Snippe::session($amount)
    ->customer(...)
    ->allowedMethods(['mobile_money', 'bank_transfer'])
    ->metadata(['subscription_id' => $sub->id])
    ->redirectTo($successUrl, $cancelUrl)
    ->send()
       │
       ▼
redirect($session->checkoutUrl())   ← user pays on Snippe page
       │
       ▼
Snippe fires POST /webhooks/snippe
       │
       ▼
Webhook controller verifies signature
       │
       ├─ payment.completed → $subscription->activate()  ← auto-activation
       │                       record PaymentTransaction
       │                       send confirmation email
       │
       └─ payment.failed    → mark subscription cancelled
                               send failure email

```

---

Error Handling
--------------

[](#error-handling)

All API errors throw a `SnippeException`:

```
use ShadrackJm\Snippe\Exceptions\SnippeException;

try {
    $session = Snippe::session(50000)
        ->customer('John Doe', 'john@email.com')
        ->send();
} catch (SnippeException $e) {
    $e->getMessage();    // "amount must be at least 500"
    $e->getCode();       // 422 (HTTP status)
    $e->getErrorCode();  // "validation_error"
    $e->getResponse();   // full API error array
}
```

HTTPError CodeMeaning400`validation_error`Missing or invalid field401`unauthorized`Bad or missing API key403`insufficient_scope`API key lacks permission404`not_found`Payment or session not found422`validation_error`Unprocessable entity---

Testing
-------

[](#testing)

The package ships with a full Pest test suite:

```
composer test
```

In your own application, use Laravel's `Http::fake()` to mock Snippe API responses:

```
use Illuminate\Support\Facades\Http;
use ShadrackJm\Snippe\Facades\Snippe;

it('creates a payment session', function () {
    Http::fake([
        '*/sessions' => Http::response([
            'data' => [
                'reference'    => 'sess_test123',
                'status'       => 'pending',
                'checkout_url' => 'https://checkout.snippe.sh/sess_test123',
            ],
        ], 201),
    ]);

    $session = Snippe::session(50000)
        ->customer('John Doe', 'john@email.com')
        ->allowedMethods(['mobile_money', 'bank_transfer'])
        ->send();

    expect($session->isPending())->toBeTrue()
        ->and($session->checkoutUrl())->toContain('sess_test123');
});
```

---

Configuration Reference
-----------------------

[](#configuration-reference)

```
// config/snippe.php
return [
    // Your Snippe API key from the dashboard
    'api_key'        => env('SNIPPE_API_KEY', ''),

    // Snippe API base URL — do not change unless testing against a sandbox
    'base_url'       => env('SNIPPE_BASE_URL', 'https://api.snippe.sh/v1'),

    // The URL Snippe will POST webhook events to (must be publicly accessible)
    'webhook_url'    => env('SNIPPE_WEBHOOK_URL', null),

    // Webhook signing secret for verifying event authenticity
    'webhook_secret' => env('SNIPPE_WEBHOOK_SECRET', null),

    // Route path for the built-in webhook endpoint
    // Set to false to disable the built-in route and register your own
    'webhook_path'   => env('SNIPPE_WEBHOOK_PATH', 'snippe/webhook'),

    // HTTP request timeout in seconds
    'timeout'        => env('SNIPPE_TIMEOUT', 30),

    // Currency for all transactions (Tanzania: TZS)
    'currency'       => env('SNIPPE_CURRENCY', 'TZS'),
];
```

---

Changelog
---------

[](#changelog)

### v1.0.3 — 2026-04-11

[](#v103--2026-04-11)

- Added Laravel 13 support (`illuminate/support|http|routing ^13.0`)

### v1.0.2

[](#v102)

- Initial stable release with Mobile Money, Card, QR, Payment Sessions, Webhooks

---

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 85% of packages

Maintenance85

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

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 ~6 days

Total

4

Last Release

77d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/49c2117621be052cfc27df35b589989bd02f9a3a64d1db375c20cff1b568b3c5?d=identicon)[shadrackmballah](/maintainers/shadrackmballah)

---

Top Contributors

[![shadrackjm](https://avatars.githubusercontent.com/u/129520133?v=4)](https://github.com/shadrackjm "shadrackjm (6 commits)")

---

Tags

laravelpaymentmpesaairtelmobile-moneytanzaniasnippe

###  Code Quality

TestsPest

### Embed Badge

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

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

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76518.2M120](/packages/laravel-mcp)[api-platform/laravel

API Platform support for Laravel

59156.3k11](/packages/api-platform-laravel)

PHPackages © 2026

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