emmanuelsiziba / zb-laravel-smilepay
ZB Laravel package for SmilePay Payment Gateway integration (ZB Bank Zimbabwe)
Package info
github.com/Omni-Learning-Dev/zb-laravel-smilepay
pkg:composer/emmanuelsiziba/zb-laravel-smilepay
Requires
- php: ^8.0|^8.1|^8.2|^8.3
- guzzlehttp/guzzle: ^7.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0
- phpunit/phpunit: ^9.0|^10.0|^11.0
README
A comprehensive Laravel package for integrating SmilePay Payment Gateway (ZB Bank Zimbabwe) into your Laravel application. This package supports multiple payment methods including Ecocash, Innbucks, Omari, SmileCash, and Visa/Mastercard.
Features
✅ Multi-Payment Support – Ecocash, Innbucks, Omari, SmileCash, Visa/Mastercard
✅ Standard Checkout – Hosted payment page integration
✅ Express Checkout – Direct API integration for custom UIs
✅ Webhook Handling – Automatic payment status callbacks
✅ Auto-Configured Callbacks – No manual URL setup required; the package registers and wires its own routes
✅ Built-in Return Handler – /smilepay/return polls status, fires events, and redirects the user
✅ Dual-App Return URLs – Route mobile deep-links and web redirects independently via a platform param
✅ Transaction Management – Check status, cancel payments
✅ Event System – Laravel events for payment notifications
✅ Sandbox & Production – Easy environment switching
✅ Comprehensive Logging – Debug and track all API interactions
Table of Contents
- Requirements
- Installation
- Configuration
- How Callbacks Work
- App Return URLs
- Usage
- Events
- Testing
- Changelog
- Contributing
- License
Requirements
- PHP 8.0 or higher
- Laravel 9.x, 10.x, or 11.x
- Guzzle HTTP Client 7.x
Installation
Install the package via Composer:
composer require emmanuelsiziba/zb-laravel-smilepay
Publish Configuration
Publish the configuration file:
php artisan vendor:publish --tag=smilepay-config
This will create a config/smilepay.php configuration file.
Configuration
Environment Variables
The minimum configuration needed is just your credentials:
SMILEPAY_ENVIRONMENT=sandbox SMILEPAY_API_KEY=your_api_key_here SMILEPAY_API_SECRET=your_api_secret_here SMILEPAY_DEFAULT_CURRENCY=840
The package automatically registers and wires its own callback routes (/smilepay/webhook and /smilepay/return), so you do not need to set SMILEPAY_RETURN_URL, SMILEPAY_RESULT_URL, SMILEPAY_CANCEL_URL, or SMILEPAY_FAILURE_URL unless you want to override the defaults.
Optional: App Return URLs
After the hosted checkout completes, the package's /smilepay/return route will redirect your user. Configure where to send them:
# Single app (web or mobile) SMILEPAY_APP_RETURN_URL=https://yourapp.com/payments/return # OR — separate URLs for mobile deep-links and web redirects SMILEPAY_MOBILE_RETURN_URL=yourapp://payments/return SMILEPAY_WEB_RETURN_URL=https://yourapp.com/payments/return
If none of these are set, the /smilepay/return route returns a JSON response instead of redirecting.
Optional: Override Callback URLs
Only set these if you need to point to a custom handler instead of the package's built-in routes:
SMILEPAY_RETURN_URL=https://yourdomain.com/custom/return SMILEPAY_RESULT_URL=https://yourdomain.com/custom/webhook SMILEPAY_CANCEL_URL=https://yourdomain.com/custom/cancel SMILEPAY_FAILURE_URL=https://yourdomain.com/custom/failed
Logging (optional)
SMILEPAY_LOGGING=true SMILEPAY_LOG_CHANNEL=stack
Currency Codes
840- USD (United States Dollar)924- ZWG (Zimbabwean Dollar)
Getting API Credentials
- Sandbox: Register at https://zbnet.zb.co.zw/wallet_sandbox_merchant/
- Navigate to Settings > API Keys
- Click Generate New API Key
- Copy your API Key and API Secret
How Callbacks Work
When the package is installed, its service provider automatically:
-
Registers two routes in your application:
POST /smilepay/webhook— ZB calls this server-to-server to notify you of a payment resultGET /smilepay/return— ZB redirects the customer here after the hosted checkout page
-
Populates
returnUrl,resultUrl,cancelUrl, andfailureUrlon every payment request usingurl('/smilepay/return')andurl('/smilepay/webhook')as defaults, so you never need to set them manually.
What /smilepay/return Does
When ZB redirects the customer back to your server, this built-in controller:
- Reads the
orderReferencefrom the query string - Polls ZB for the definitive payment status
- Fires the appropriate package event (
PaymentReceived,PaymentFailed, orPaymentCanceled) - Redirects the user to your configured app return URL (with
?status=&orderReference=appended), or returns JSON if no URL is configured
App Return URLs
Use app return URLs to send the customer back to your application after the hosted checkout.
Single Application
SMILEPAY_APP_RETURN_URL=https://yourapp.com/payments/return
The customer will be redirected to:
https://yourapp.com/payments/return?status=success&orderReference=SP-ABC123
Mobile App + Web App (Dual Platform)
If you serve both a mobile app (deep link) and a web app, configure separate URLs:
SMILEPAY_MOBILE_RETURN_URL=yourapp://payments/return SMILEPAY_WEB_RETURN_URL=https://yourapp.com/payments/return
Then signal the platform when initiating a payment by appending ?platform=mobile or ?platform=web to the returnUrl, cancelUrl, and failureUrl:
$platform = 'mobile'; // or 'web', determined by your request context SmilePay::standardCheckout()->initiate([ 'amount' => 100.00, 'itemName' => 'Order #123', 'returnUrl' => url('/smilepay/return') . '?platform=' . $platform, 'cancelUrl' => url('/smilepay/return') . '?platform=' . $platform, 'failureUrl' => url('/smilepay/return') . '?platform=' . $platform, // resultUrl is always server-to-server — no platform param needed ]);
The ReturnController reads ?platform= and picks the matching configured URL:
?platform= |
Uses config key | .env variable |
|---|---|---|
mobile |
smilepay.mobile_return_url |
SMILEPAY_MOBILE_RETURN_URL |
web |
smilepay.web_return_url |
SMILEPAY_WEB_RETURN_URL |
| (absent) | smilepay.app_return_url |
SMILEPAY_APP_RETURN_URL |
If none match, a JSON response is returned.
Return URL Query Parameters
Regardless of which URL is used, the following query parameters are always appended:
| Parameter | Values | Description |
|---|---|---|
status |
success, failed, cancelled, pending |
Payment outcome |
orderReference |
string | Your order reference |
message |
string | Human-readable status message (only on non-success) |
Usage
Standard Checkout
Standard Checkout redirects customers to a SmilePay hosted payment page.
use Emmanuelsiziba\SmilePay\Facades\SmilePay; $response = SmilePay::standardCheckout()->initiate([ 'amount' => 100.00, 'itemName' => 'Premium Subscription', 'itemDescription' => 'Monthly subscription', 'email' => 'customer@example.com', 'mobilePhoneNumber' => '0771234567', 'firstName' => 'John', 'lastName' => 'Doe', ]); if ($response->isSuccessful()) { // Redirect customer to payment page return redirect($response->paymentUrl); }
Express Checkout
Express Checkout allows you to build custom payment UIs.
Ecocash
use Emmanuelsiziba\SmilePay\Facades\SmilePay; $response = SmilePay::expressCheckout()->ecocash([ 'amount' => 50.00, 'itemName' => 'Product Purchase', 'ecocashMobile' => '0771234567', 'email' => 'customer@example.com', ]); if ($response->isSuccessful()) { // Customer receives USSD push notification // Poll for payment status or wait for webhook echo "Payment initiated: {$response->transactionReference}"; }
Innbucks
$response = SmilePay::expressCheckout()->innbucks([ 'amount' => 75.00, 'itemName' => 'Service Payment', 'email' => 'customer@example.com', ]); if ($response->isSuccessful()) { // Get payment code $paymentCode = $response->innbucksPaymentCode; // Get deep link for mobile app $deepLink = $response->getInnbucksDeepLink(); // Display to customer or create QR code echo "Payment Code: {$paymentCode}"; echo "Deep Link: <a href='{$deepLink}'>Pay with InnBucks</a>"; }
Omari (Two-Step Process)
// Step 1: Initiate payment (sends OTP) $response = SmilePay::expressCheckout()->omari([ 'amount' => 100.00, 'itemName' => 'Order #12345', 'omariMobile' => '0771234567', ]); $transactionRef = $response->transactionReference; // Step 2: Confirm with OTP (customer enters OTP from SMS) $confirmResponse = SmilePay::expressCheckout()->omariConfirm( $transactionRef, '123456', // OTP from customer '0771234567' // Same mobile number ); if ($confirmResponse->isSuccessful()) { echo "Payment confirmed!"; }
SmileCash (Two-Step Process)
// Step 1: Initiate payment $response = SmilePay::expressCheckout()->smileCash([ 'amount' => 200.00, 'itemName' => 'Shopping Cart', 'smileCashMobile' => '0771234567', ]); $transactionRef = $response->transactionReference; // Step 2: Confirm with OTP $confirmResponse = SmilePay::expressCheckout()->smileCashConfirm( $transactionRef, '000000', // OTP (use '000000' in sandbox) '0771234567' );
Visa/Mastercard
$response = SmilePay::expressCheckout()->card([ 'amount' => 150.00, 'itemName' => 'Product Purchase', 'pan' => '5123450000000008', 'expMonth' => '01', 'expYear' => '39', 'securityCode' => '100', 'email' => 'customer@example.com', ]); if ($response->requires3DS()) { // Redirect to 3D Secure authentication $redirectHtml = $response->redirectHtml; echo $redirectHtml; // Renders 3DS form }
Transaction Management
Check Payment Status
use Emmanuelsiziba\SmilePay\Facades\SmilePay; $status = SmilePay::utility()->checkStatus('SP-ABC123-1234567890'); if ($status->isPaid()) { echo "Payment successful!"; echo "Amount: {$status->amount}"; echo "Payment Method: {$status->paymentOption}"; } // Other status methods $status->isPending(); // Returns true if pending $status->isFailed(); // Returns true if failed $status->isCanceled(); // Returns true if canceled
Cancel Payment
$result = SmilePay::utility()->cancel('SP-ABC123-1234567890'); if ($result['success']) { echo "Payment canceled successfully"; }
Helper Methods
// Quick boolean checks $isPaid = SmilePay::utility()->isPaymentSuccessful('ORDER-REF'); $isPending = SmilePay::utility()->isPaymentPending('ORDER-REF'); $isFailed = SmilePay::utility()->isPaymentFailed('ORDER-REF');
Webhook Handling
The package automatically registers a webhook route at /smilepay/webhook and sets it as the default resultUrl on every payment request — no manual configuration required.
To verify the route is registered:
php artisan route:list --name=smilepay
Listen to Payment Events
Create event listeners in app/Listeners:
// app/Listeners/HandlePaymentReceived.php namespace App\Listeners; use Emmanuelsiziba\SmilePay\Events\PaymentReceived; class HandlePaymentReceived { public function handle(PaymentReceived $event) { $transaction = $event->transaction; // Update order status $order = Order::where('reference', $transaction->orderReference)->first(); $order->update(['status' => 'paid']); // Send confirmation email Mail::to($order->email)->send(new PaymentConfirmation($order)); } }
Register the listener in app/Providers/EventServiceProvider.php:
use Emmanuelsiziba\SmilePay\Events\PaymentReceived; use Emmanuelsiziba\SmilePay\Events\PaymentFailed; use Emmanuelsiziba\SmilePay\Events\PaymentCanceled; protected $listen = [ PaymentReceived::class => [ HandlePaymentReceived::class, ], PaymentFailed::class => [ HandlePaymentFailed::class, ], PaymentCanceled::class => [ HandlePaymentCanceled::class, ], ];
Events
The package dispatches the following events:
| Event | Description |
|---|---|
PaymentReceived |
Fired when payment is successful (status: PAID) |
PaymentFailed |
Fired when payment fails (status: FAILED) |
PaymentCanceled |
Fired when payment is canceled (status: CANCELED) |
Each event contains a $transaction property of type TransactionStatus.
Testing
Sandbox Environment
Use the following test credentials in sandbox mode:
SmileCash
- Mobile:
0711111111 - OTP:
000000
Visa/Mastercard
- Card Number:
5123450000000008 - Expiry:
01/39 - CVV:
100
Ecocash
- Use any valid Zimbabwe mobile number
- Approve the USSD prompt in sandbox
Innbucks
- Any payment code generated will work in sandbox
Running Tests
composer test
API Reference
Payment Methods
| Method | Description |
|---|---|
WALLETPLUS |
Combined wallet option |
ECOCASH |
Ecocash mobile money |
INNBUCKS |
Innbucks digital wallet |
CARD |
Visa/Mastercard |
OMARI |
Omari payment platform |
ONEMONEY |
OneMoney mobile money |
Payment Statuses
| Status | Description |
|---|---|
PAID |
Payment successful |
PENDING |
Payment pending confirmation |
FAILED |
Payment failed |
CANCELED |
Payment canceled |
Advanced Usage
Custom Order Reference
$response = SmilePay::standardCheckout()->initiate([ 'orderReference' => 'CUSTOM-ORDER-' . time(), 'amount' => 100.00, 'itemName' => 'Product', // ... other fields ]);
Using Dependency Injection
use Emmanuelsiziba\SmilePay\Services\StandardCheckout; use Emmanuelsiziba\SmilePay\Services\ExpressCheckout; class PaymentController extends Controller { public function __construct( protected StandardCheckout $standardCheckout, protected ExpressCheckout $expressCheckout ) {} public function processPayment() { $response = $this->expressCheckout->ecocash([...]); } }
Error Handling
The package provides comprehensive error handling with user-friendly error messages:
use Emmanuelsiziba\SmilePay\Exceptions\SmilePayException; use Emmanuelsiziba\SmilePay\Exceptions\PaymentException; try { $response = SmilePay::expressCheckout()->ecocash([ 'amount' => 50.00, 'itemName' => 'Product Purchase', 'ecocashMobile' => '0771234567', ]); if ($response->isSuccessful()) { // Handle successful initiation } } catch (PaymentException $e) { // Payment validation or business logic errors Log::error('Payment validation failed: ' . $e->getMessage()); // Get additional context (API response details) $context = $e->getContext(); return back()->withErrors([ 'payment' => $e->getMessage() ]); } catch (SmilePayException $e) { // API communication errors (network, authentication, etc.) Log::error('SmilePay API error', [ 'message' => $e->getMessage(), 'code' => $e->getCode(), 'context' => $e->getContext(), ]); return back()->withErrors([ 'payment' => 'Unable to process payment. Please try again later.' ]); }
Common Error Scenarios
404 Not Found Error
try { $response = SmilePay::expressCheckout()->ecocash([...]); } catch (SmilePayException $e) { // User-friendly message: // "Endpoint not found: The payment method or endpoint '/payments/express-checkout/ecocash' // is not available. Please verify the API endpoint or contact support." if ($e->getCode() === 404) { // Check if you're using the correct environment // Verify the endpoint is available in your environment Log::warning('Payment endpoint not available', $e->getContext()); return back()->with('warning', 'This payment method is currently unavailable.'); } }
Authentication Error (401)
try { $response = SmilePay::expressCheckout()->innbucks([...]); } catch (SmilePayException $e) { if ($e->getCode() === 401) { // "Authentication failed: Invalid API credentials. // Please check your SmilePay configuration." Log::critical('Invalid SmilePay credentials'); return back()->withErrors(['payment' => 'Payment system configuration error.']); } }
Network/Connection Error
try { $response = SmilePay::standardCheckout()->initiate([...]); } catch (SmilePayException $e) { // "Connection error: Unable to connect to SmilePay API. // Please check your internet connection and try again." Log::error('SmilePay connection failed', [ 'message' => $e->getMessage(), 'context' => $e->getContext(), ]); return back()->withErrors([ 'payment' => 'Connection issue. Please check your internet and try again.' ]); }
Error Context
All exceptions include a context array with debugging information:
try { $response = SmilePay::expressCheckout()->ecocash([...]); } catch (SmilePayException $e) { $context = $e->getContext(); // Context includes: // - endpoint: The API endpoint that failed // - method: HTTP method (GET/POST) // - status_code: HTTP status code (if available) // - raw_response: Full API response (if available) Log::error('SmilePay error details', [ 'endpoint' => $context['endpoint'] ?? 'unknown', 'status' => $context['status_code'] ?? 'N/A', 'response' => $context['raw_response'] ?? 'No response', ]); }
Best Practices
- Always catch exceptions when initiating payments
- Log errors with context for debugging
- Show user-friendly messages to customers
- Handle specific error codes differently (404, 401, 500, etc.)
- Enable logging in
config/smilepay.phpfor production debugging
Troubleshooting
404 Not Found Error
If you encounter a 404 error when initiating payments:
Endpoint not found: The payment method or endpoint '/payments/express-checkout/ecocash'
is not available. Please verify the API endpoint or contact support.
Possible causes:
- Wrong environment: You may be using sandbox credentials with a production base URL (or vice versa)
- Endpoint not available: The specific payment method may not be enabled for your account
- Incorrect base URL: Check your
SMILEPAY_ENVIRONMENTsetting in.env - Outdated package version: Versions prior to v0.1.5 had URL construction issues with Guzzle
Solutions:
-
Update to the latest version (recommended):
composer update emmanuelsiziba/zb-laravel-smilepay
This fixes a critical bug where leading slashes in endpoints caused Guzzle to treat them as absolute paths when the base URL has a trailing slash.
-
Verify your environment setting:
SMILEPAY_ENVIRONMENT=sandbox # or 'production'
-
Ensure your API credentials match the environment
-
Check if the payment method is enabled in your SmilePay merchant dashboard
-
Enable logging to see the full request details:
SMILEPAY_LOGGING=true
API Credentials Not Working
- Ensure you're using the correct environment (sandbox/production)
- Verify API keys are generated from the correct environment
- Check for any whitespace in your
.envfile - Confirm credentials have the required permissions
Webhook Not Receiving Callbacks
- The
resultUrlis auto-set to{APP_URL}/smilepay/webhook— ensure yourAPP_URLin.envis publicly accessible (use ngrok for local testing) - Verify both routes are registered:
php artisan route:list --name=smilepay - Check webhook middleware in
config/smilepay.php - Check your server's firewall allows incoming POST requests
Payment Status Not Updating
- Always poll the status API in addition to webhook callbacks
- Implement retry logic for failed webhook deliveries
- Use Laravel queues for webhook processing
- Check your application logs for webhook processing errors
Changelog
Please see CHANGELOG for recent changes.
Contributing
Contributions are welcome! Please see CONTRIBUTING for details.
Security
If you discover any security-related issues, please email security@example.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.
Support
- Documentation: SmilePay Official Docs
- Issues: GitHub Issues
- Email: support@example.com
Made with ❤️ for the Laravel community