Download the PHP package aaronkatema/laravel-smilepay without Composer

On this page you can find all versions of the php package aaronkatema/laravel-smilepay. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package laravel-smilepay

Laravel Smile&Pay

A production-grade Laravel SDK for Smile&Pay, ZB Bank's Zimbabwean payment gateway.

Supports EcoCash, OneMoney, O'mari, InnBucks, SmileCash (WalletPlus) and Visa/Mastercard — through both hosted Standard Checkout and Express Checkout, with a verified webhook pipeline, a full transaction audit trail and a reconciliation command.


Read this first: the security model

Smile&Pay callbacks are unsigned. There is no HMAC, no shared secret, no mutual TLS — just a JSON body POSTed to your resultUrl. That URL is not a secret either: it travels in every payment you initiate and is echoed back inside the callback payload.

The obvious integration reads status: "PAID" from the callback and marks the order paid. That integration ships goods to anyone who can write a POST request:

This package never lets a callback body move money.

A callback is treated as a hint that something changed. On receipt, the package calls GET /payments/transaction/{orderReference}/status/check over your own authenticated channel, and only that answer is allowed to change state. One extra round trip, in exchange for a checkout that cannot be talked into shipping for free.

When the callback and the gateway disagree — a body claiming PAID for a transaction ZB reports as PENDING — the package records it, refuses to act, and fires SuspiciousCallbackDetected. Alert on that event. A trickle usually means someone found your webhook URL and is testing whether you check. A burst means they stopped testing.


Installation

Requirements: PHP 8.2+, Laravel 11, 12 or 13.

Credentials

Register at the Smile&Pay Sandbox Portal, then generate an API key and secret under Settings → API Keys.

The API secret is a password. It belongs in .env on your server and nowhere else — never in a repository, never in a frontend bundle, never in a mobile app. ZB's own card documentation demonstrates calling the API from browser JavaScript with both credentials inline. Do not copy that pattern. Anyone who opens devtools gets your merchant credentials.

Verify your setup

Prints the resolved environment, base URL, masked credentials, and flags any unsafe setting.


Standard Checkout (recommended)

Redirect the customer to ZB's hosted page. Card data never touches your servers, which keeps you on PCI SAQ A rather than SAQ D.

Leave the payment method unset and the customer picks their own rail on ZB's page.

metadata never leaves your application — the API has no custom-field support. It is stored on the local transaction row and rejoined by orderReference when the callback arrives.


Express Checkout

Charge directly from your own UI. Each rail behaves differently, so branch on nextAction() rather than on the method:

Per-rail shortcuts

Two-step rails (SmileCash, O'mari)

Leg 1 triggers an SMS OTP. Leg 2 confirms it.

The trap ZB's own docs flag: leg 2 keys on the transactionReference returned by leg 1, not your orderReference. O'mari additionally requires the mobile number to be echoed back — pass it as the mobile argument. The package stores numbers masked and will not guess.

Card payments (MPGS)

This puts you in PCI-DSS scope. Passing a raw PAN through your own server moves you from SAQ A to SAQ D — quarterly ASV scans, penetration testing, network segmentation, an annual audit — and it applies to every machine the data touches, including log aggregators, queue workers and backups. Unless you have a concrete commercial reason and a compliance programme to match, use checkout() instead.

CardDetails defends what it can: it never appears in var_dump, dd(), json_encode, logs or exception traces, and refuses to be unserialised so a card cannot end up in a queue payload or session.

On the 3DS challenge, prefer toSafeHtml(). ZB returns a redirectHtml blob whose <script> you are told to extract and execute yourself — because browsers correctly refuse to run scripts inserted via innerHTML. Doing that means any change on ZB's side executes in your origin. toSafeHtml() posts the same acsUrl and cReq from a form you control.


Handling payment results

Webhooks

The package registers POST /smilepay/callback automatically. Point SMILEPAY_RESULT_URL at it and listen for events:

PaymentSucceeded is the only event that should release value. It carries two guarantees:

  1. The snapshot came from an authenticated status check, never from a callback body.
  2. It fires exactly once per order reference. ZB retries callbacks until it gets a 200, so duplicates are routine — the package deduplicates them.

Other events: PaymentInitiated, PaymentFailed, PaymentCancelled, PaymentStatusChanged, WebhookReceived, SuspiciousCallbackDetected.

Polling

For rails without a reliable callback, or as a belt-and-braces check. Run it in a queued job, never in a web request — a customer approving a USSD prompt can take a minute, and holding a PHP-FPM worker open that long is how a checkout takes the whole site down under load.

Reconciliation

Payments fail asynchronously. A customer walks away from a USSD prompt, a callback is lost, a deploy kills a poll job mid-flight — and the transaction sits open while nobody knows whether you were paid.

A Smile&Pay integration without this scheduled is not finished, however well the happy path works.


Hardening the webhook endpoint

Because callbacks are unsigned, add what defence you can:

Neither is authentication — an IP can be spoofed, a URL is not a credential — but together they take the endpoint from "anyone with the URL" to "anyone who can source traffic from ZB's range and knows a secret path". The real guarantee remains the status check.

Behind a load balancer, configure Laravel's TrustProxies first, or $request->ip() returns the balancer and the allowlist either blocks everything or trusts a forged X-Forwarded-For.


Testing

The fake makes no network calls but runs the real persistence and event pipeline, so a passing test exercises the same listeners production will.

Scripting helpers: willSucceed(), willFail(), willStayPending(), willRejectInitiation(), willReturn(). Assertions: assertInitiated(), assertNotInitiated(), assertNothingInitiated(), assertInitiatedCount(), assertPaid(), assertMethodUsed(), assertCancelled(), assertOtpConfirmed().

Sandbox test data

Rail Test value
EcoCash 263788687707 (approval is triggered manually — contact the Smile&Pay team)
OneMoney success 0713456789
OneMoney failure 0713456780
SmileCash 0711111111
O'mari 0731234567
SMS OTP 000000
Card — 3DS success 5123450000000008
Card — system error 5123450000000002 *
Card — declined 5123450000000010 *
Card CVV / expiry 100 / 01/39

* These two PANs do not satisfy the Luhn checksum, so CardDetails::make() rejects them by default. To exercise the failure paths, pass strictLuhn: false:

Keep the check on in production — it catches customer typos before they cost a gateway call and a decline on the card's record. Whether the invalid checksums are deliberate on ZB's side or a documentation typo is worth confirming with your integration contact.


Design decisions worth knowing

Money is stored in integer minor units. Money::fromDecimal('0.10')->plus(...'0.20') gives exactly 0.30. A float-based implementation cannot promise that, and the discrepancy surfaces in a merchant's ledger rather than in your tests.

Currency is transmitted as ISO numeric. Smile&Pay wants "840" and "924", not "USD" and "ZWG". The package keeps the readable code in your code and database and converts only at the wire boundary.

Initiation is never retried automatically. A timeout does not prove the transaction was not created — the customer may already have been prompted. Recovery is reconciliation, not repetition. Idempotent calls (status checks) are retried, with exponential backoff and full jitter so a fleet coming out of a ZB outage does not stampede it back down.

The transaction row is written before the gateway is called. If the call then fails, the payment is still visible and reconcilable. Write-after leaves an indeterminate payment nobody knows to look for.

HTTP 200 is not success. The gateway returns 200 with responseCode: "51" for a decline. Every response passes through the response-code check — this is the single most common way to build a payment integration that ships goods for free.

An unknown status degrades to UNKNOWN, never PAID. A false PAID gives goods away; a false UNKNOWN just triggers another status check. Add new ZB statuses via smilepay.status_map without touching code.

Secrets and card data never reach a log. Headers and bodies pass through a redactor before any logging, and contact details are masked rather than stored in the clear. A payments table links names to numbers to amounts; keeping it masked stops one leak becoming a ready-made target list.

A final state is never walked backwards. A late PENDING callback cannot un-pay a settled transaction. Enforced inside a row lock, because callbacks do not arrive in order.


Configuration reference

Key Env Default Notes
environment SMILEPAY_ENV sandbox Auto-switches on APP_ENV=production
default_currency SMILEPAY_CURRENCY USD USD or ZWG
defaults.return_url SMILEPAY_RETURN_URL — Required for hosted checkout
defaults.result_url SMILEPAY_RESULT_URL — Falls back to the package route
webhook.verify_with_status_check SMILEPAY_VERIFY_CALLBACKS true Leave on. Ignored in production
webhook.allowed_ips SMILEPAY_ALLOWED_IPS — CIDR or plain IPs
webhook.secret_path SMILEPAY_WEBHOOK_SECRET_PATH — Extra path segment
database.enabled SMILEPAY_PERSIST true Disabling removes dedup + reconciliation
http.timeout SMILEPAY_TIMEOUT 30 Wallet initiations can be slow
http.verify_ssl SMILEPAY_VERIFY_SSL true Refused in production if false
retry.attempts SMILEPAY_RETRY_ATTEMPTS 3 Idempotent calls only
reconciliation.stale_after_seconds SMILEPAY_STALE_AFTER 300

TLS note

Some zb.co.zw hosts have been observed serving an incomplete certificate chain, which strict clients reject. The fix is to install the missing intermediate on your server, or point Guzzle at an updated CA bundle — not to disable verification. Without it, anyone on the path can read your API secret and rewrite payment instructions. The package refuses to boot with verification off in production.


API surface


Known gaps in the upstream API

Not limitations of this package — things Smile&Pay does not currently expose:

If ZB adds any of these, the seams are already in place — Endpoints for paths, status_map for vocabulary, TransactionStore for persistence.


Licence

MIT © Aaron Gibson Katema


All versions of laravel-smilepay with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
ext-json Version *
guzzlehttp/guzzle Version ^7.8
illuminate/config Version ^11.0|^12.0|^13.0
illuminate/console Version ^11.0|^12.0|^13.0
illuminate/contracts Version ^11.0|^12.0|^13.0
illuminate/database Version ^11.0|^12.0|^13.0
illuminate/events Version ^11.0|^12.0|^13.0
illuminate/http Version ^11.0|^12.0|^13.0
illuminate/routing Version ^11.0|^12.0|^13.0
illuminate/support Version ^11.0|^12.0|^13.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package aaronkatema/laravel-smilepay contains the following files

Loading the files please wait ...