openpesa/pesa
OpenPesa Pesa is a Laravel/PHP package for integrating Tanzania's OpenPESA payment gateway, enabling mobile money collections, disbursements, and transaction status checks through a simple API.
Install the package and immediately inspect what you actually got:
composer require openpesa/pesa
Open these first:
vendor/openpesa/pesa/README.md — authoritative payload keys and method names for this release.vendor/openpesa/pesa/composer.json — PHP/Laravel constraints and whether a service provider is auto-discovered.vendor/openpesa/pesa/config/ or src/ — config keys, client class, exception types, and whether it uses Laravel’s HTTP client or Guzzle directly.If the package exposes a Laravel provider, publish its config:
php artisan vendor:publish --provider="OpenPesa\PesaServiceProvider"
Set credentials in .env and never commit them:
PESA_PUBLIC_KEY=
PESA_SECRET_KEY=
PESA_ENVIRONMENT=sandbox
# PESA_BASE_URL= # if supported by your version
If there is no auto-discovered provider, bind the client yourself in AppServiceProvider using the constructor shown in the README:
$this->app->singleton(\OpenPesa\Pesa::class, function () {
return new \OpenPesa\Pesa(config('pesa')); // adjust to actual constructor
});
Your first use case should be a sandbox collection: initiate a USSD push for a known test MSISDN, persist the raw request and response, then query the transaction status. Do this from an Artisan command or temporary route before wiring it into checkout:
$reference = 'PAY-'.Str::uuid();
$response = app(\OpenPesa\Pesa::class)->pushUSSD([
'amount' => 1000,
'msisdn' => '255712345678',
'reference' => $reference,
// remaining keys depend on the API version — copy them from the README
]);
logger()->info('pesa.push', [
'reference' => $reference,
'response' => $response,
]);
Do not treat a successful HTTP response as a completed payment. The first milestone is “request accepted and traceable,” not “customer charged.”
Wrap the package behind your own gateway. Given the package’s age and small surface area, do not let OpenPesa\Pesa leak into controllers, jobs, and views. Create an application-level service or interface:
interface MobileMoneyGateway
{
public function collect(Payment $payment): PesaResult;
public function status(Payment $payment): PesaResult;
}
Bind the OpenPesa implementation in the container. This lets you fake it in tests, swap providers later, and centralize payload normalization.
Persist before calling the API. Create a payments record first with your own UUID reference, amount, currency, normalized MSISDN, status (pending), and request payload. Then dispatch a queued job. Store the provider response, provider transaction ID, and raw error body when it returns. This is your audit trail and reconciliation source.
Use a queued job with conservative retries. Network timeouts happen, but retrying a collection blindly can double-charge if the first request actually succeeded. Prefer:
WithoutOverlapping.unknown rather than failed when the outcome is ambiguous.Model payment status as a state machine. Suggested states: pending, processing, successful, failed, cancelled, unknown. Only transition forward from pending/processing, and make updates idempotent:
Payment::where('reference', $reference)
->whereIn('status', ['pending', 'processing'])
->update([...]);
Handle callbacks/webhooks as the primary completion signal, with polling as fallback. Exclude the callback route from CSRF, accept the raw body, log it, verify any signature the provider supports, persist it, respond 200 quickly, and process in a job. Use the provider transaction ID plus your reference for idempotency. Add a scheduled reconciliation command that queries stale pending payments.
Normalize inputs at the boundary. Validate amount as a positive integer in TZS, normalize phone numbers to the provider’s expected format (commonly 2557XXXXXXXX, no +), enforce unique merchant references, and reject unsupported currencies before calling the API.
Test at your own boundary. Unit and feature tests should use a fake MobileMoneyGateway, not the real package. For HTTP-level tests, use Http::fake() if the package uses Laravel’s HTTP client; otherwise use Guzzle’s MockHandler. Keep a small, manually run sandbox smoke test separate from CI.
Use config, not env(), in application code. Read config('pesa.*') so php artisan config:cache works. If the package itself calls env() internally, verify behavior after caching and consider forking or overriding that config.
composer why-not openpesa/pesa if installation conflicts.pending, not instant success..env, run php artisan config:clear locally. In production, redeploy or run config:cache as part of release.How can I help you explore Laravel packages today?