PROJECT

Paysasa

A driver-based payment gateway abstraction for Laravel, built for Kenya. One fluent API covers M-Pesa, Airtel Money, Stripe, Paystack, Pesapal, PesaLink and 7 other providers. It includes idempotency, verified webhooks and queue-based settlement

Paysasa

Problem

Every Kenyan Laravel project ends up writing the same brittle payment code. First it is Safaricom's Daraja API for M-Pesa, then Stripe for cards, then Pesapal or Flutterwave when a client wants hosted checkout. Each provider has its own auth scheme, its own webhook shape and its own retry behaviour. In most codebases that logic is scattered across controllers, with no shared persistence model, no protection against duplicate charges and no events for the rest of the app to react to.

The result is payment code that is hard to test, hard to audit and painful to change when a business adds a second provider.

Solution

Paysasa is a driver-based payment gateway abstraction for Laravel 11, 12 and 13, built for the Kenyan market. It gives an application one consistent, fluent API for every provider:

Payment::driver('mpesa')
    ->amount(2500)
    ->currency('KES')
    ->phone('254712345678')
    ->reference('INV1001')
    ->charge();

Switching to Stripe, Paystack or PesaLink is a one-word change. Every driver returns the same PaymentResponse object (successful(), pending(), failed(), transactionId(), receiptNumber() and so on), so business logic is written once against the abstraction and never against a specific gateway.

Supported rails (13 drivers):

  • Mobile money: M-Pesa (STK push, C2B, B2C, reversal, balance), Airtel Money, T-Kash
  • Cards: Stripe, Pesapal, Flutterwave, Paystack
  • Wallets: Google Pay, Apple Pay
  • Banking: PesaLink, EFT, RTGS, virtual accounts

System Architecture

Paysasa follows a Clean Architecture / DDD-lite layout, adapted to what a Laravel package needs. The aim is testability and provider-swappability, not framework independence.

  • Facade → Manager → Fluent builder. The Payment facade resolves drivers through a PaymentManager that follows Laravel's own Manager pattern, the same one used by Cache, Mail and Queue.
  • Contracts and capability interfaces. PaymentDriver only requires charge() and verify(). Optional abilities such as Refundable, PayoutCapable, Reversible and BalanceInquirable are separate interfaces (Interface Segregation). A caller checks instanceof Refundable instead of calling a method that throws on half the drivers.
  • Actions as the single orchestration layer. InitiatePayment, VerifyPayment, ProcessRefund and RunFraudChecks are the only places the sequencing is defined: fraud check, idempotency wrap, persist, call driver, persist result, fire event. The flow is identical whatever the entry point.
  • Thin, provider-faithful HTTP clients. DarajaClient, StripeClient and the others hold no business logic, so a provider's wire format can change without touching driver logic.
  • Repository pattern. Persistence sits behind a PaymentRepository contract with an Eloquent implementation.
  • Strategy pattern for webhooks. Each provider has its own SignatureVerifier, selected by a WebhookDispatcher.
  • Extensible by design. Third-party drivers plug in through extend() without touching core. Every driver constructor has the same signature, which keeps this trivial.

Wallets are adapters, not rails

Google Pay and Apple Pay are not independent settlement networks. They are tokenization layers on top of the card networks. Paysasa models them as adapters that delegate settlement to a configured card gateway (Stripe by default), rather than inventing a settlement API that doesn't exist. This keeps the system honest about where money actually moves, which matters when explaining it to a PCI auditor.

Reliability

Payment lifecycle

Payments move through an explicit state machine: Pending → Processing → Authorized → Successful, with Failed, Cancelled, Expired, Refunded, PartiallyRefunded and Reversed as the other states. The terminal states are defined in one place (PaymentStatus::isTerminal()).

Asynchronous providers

STK push and hosted checkouts don't settle in the request/response cycle. Paysasa handles this in two ways:

  1. Webhooks. The webhook is acknowledged fast and processed on the queue by DispatchWebhookJob, with retries and backoff.
  2. Polling safety net. A VerifyPaymentStatusJob re-queries the provider with increasing delays in case a webhook never arrives. It stops as soon as a terminal state is reached.

Event-driven integration

The package fires 11 lifecycle events (PaymentInitiated, PaymentSuccessful, PaymentFailed, PaymentRefunded, WebhookProcessed and others). The host application decides what happens next (mark an invoice paid, send a receipt) with ordinary Laravel listeners. PaymentSuccessful and PaymentFailed can also broadcast on a private channel, so a UI can update live while the customer is looking at the STK prompt.

Error handling

A typed exception hierarchy (ProviderApiException, WebhookVerificationException, IdempotencyConflictException, FraudSuspectedException and others) lets callers catch precisely. Normal "payment failed" cases come back as a PaymentResponse, not an exception. Exceptions are reserved for genuinely exceptional conditions such as misconfiguration.

Security

Security is built into the design rather than added afterwards:

  • Webhook verification. Every inbound webhook is checked by a provider-specific signature verifier before it is trusted. The raw payload is persisted before verification, so a failed attempt is kept as forensic evidence of spoofing.
  • Idempotency. Every charge() is wrapped by an IdempotencyManager. A repeated key with the same payload returns the original response without calling the provider again, which protects against double-taps, frontend retries and job replays. Reusing a key with a different payload fails loudly with IdempotencyConflictException.
  • Replay protection. Webhook payloads are fingerprint-deduplicated, and Stripe signatures older than five minutes are rejected.
  • Constant-time comparisons. HMAC checks use hash_equals().
  • Rate limiting. Webhook endpoints are throttled per source IP, independent of the host app's own throttling.
  • Encrypted credentials. Per-merchant provider credentials are encrypted at rest and hidden from serialization. Logs redact sensitive fields such as PINs, card numbers and CVVs.
  • Reduced PCI scope. The host app never handles raw card data. Card charges use client-side tokens, so integrators stay out of the heaviest PCI scope (SAQ A or A-EP rather than SAQ D).
  • Fraud hooks. A FraudCheck contract lets integrators run custom checks, such as a velocity limit, before any charge reaches a provider.

Data Model

Nine tables cover the payment lifecycle: payments, transactions, payment_attempts, refunds, webhooks, payment_logs, provider_accounts, payment_methods and audit_logs.

Multi-Merchant Support

Payment::forMerchant($id)->driver('mpesa') resolves credentials per merchant from encrypted provider_accounts rows, falling back to config defaults. This lets one application act on behalf of many merchants, as a marketplace or SaaS platform would.

Developer Experience

  • One-command install: composer require paysasa/payments and php artisan paysasa:install --migrate.
  • First-class testing: Payment::fake() never touches the database, the queue or the network, and ships assertions such as assertCharged(), assertChargedOn('mpesa') and assertChargedTimes(1).
  • Operational commands: paysasa:status checks every configured driver, and paysasa:mpesa:register-urls handles one-time Daraja URL registration.
  • Quality tooling: the repository includes a Pest test suite and PHPStan static analysis.
  • Documentation: a 14-document guide in the repo covers architecture, database schema, webhooks, security, testing, performance, troubleshooting and writing custom drivers.

Takeaway

Paysasa turns a tangle of per-provider integration code into one tested, extensible layer. It handles the hard parts of payments (idempotency, webhook trust, async settlement, auditability) once, so application code only has to say what to charge and react to what happened.

PORTFOLIO

Related Case Studies

2026 • completed
Aura & Oak Website Mockup

Aura & Oak Website

Aura & Oak is an artisanal, e-commerce platform and back-office management system engineered for quiet luxury. Built on the modern Laravel 13 + Inertia.js v3 + Vue 3 stack, it combines an aspirational editorial storefront with an operational, high-density atelier management panel.

Case Study Explore System →