savannabits/daraja
Laravel package for Safaricom's M-Pesa Daraja API, simplifying STK Push, C2B, B2C, transaction status and reversal requests with fluent, configurable methods so you can integrate mobile payments into your PHP app quickly.
Architecture fit. The package is a thin HTTP convenience wrapper, not a payments subsystem. What it actually buys you is (a) fluent Laravel-style method calls over raw Daraja payloads, (b) automatic OAuth token acquisition and caching, and (c) a publishable config file so credentials live in .env. Everything that makes a money path safe — idempotency, transaction state machine, callback authenticity, reconciliation, multi-tenant shortcode routing — is not in the package and lands on our team regardless of whether we adopt it. That reframes the decision: we are not evaluating a platform, we are evaluating whether to borrow roughly a few hundred lines of OAuth and request-shaping code. The architectural fit is therefore good only if we place it behind our own PaymentGateway interface and never let Daraja:: appear in a controller, job, or Livewire component. With that boundary, the package is swappable in a one-class diff; without it, it becomes an untested vendor coupling on the revenue path.
Integration feasibility. High for the code, low for the calendar. Engineering is realistically 1–3 days for the first end-to-end sandbox STK Push (persist row → request → callback handler → status query), plus ~1 day to read src/ end-to-end and enumerate the public surface. The real critical path is non-engineering: shortcode provisioning, Daraja portal app registration, callback URL registration, and Safaricom go-live approval. That is measured in weeks, and it must start the same week as the spike, not after it. Any plan that sequences "build it, then apply for go-live" is a plan that ships late. Secondary feasibility question: declared support is PHP 8.1 / Laravel 9 (released Nov 2022). If our target is Laravel 10/11/12, "it probably works" is not a support statement and must be verified rather than assumed.
Technical risk. Ranked:
| Risk | Severity | Notes / mitigation |
|---|---|---|
| Unauthenticated callbacks enabling forged credits | Critical | Daraja callbacks have no signature scheme. Handler must trust only CheckoutRequestID and read amount from our DB. Requires security sign-off before production. |
| Lost outbound response → charged customer, no record | Critical | Mandatory indeterminate state + reconciliation sweep. Non-negotiable. |
| Duplicate / out-of-order callbacks double-crediting | High | Unique index on checkout_request_id, lockForUpdate, absorbing terminal states, 200 OK to duplicates. |
ResultCode 1037 (timeout) silently charges |
High | Treat unknown codes as indeterminate → poll, never as failed. Taxonomy mapped to our own enum. |
| Unverified v1.0.0 rewrite (0.2.0 → 1.0.0, single PR, no migration guide) | Medium | Diff PR #1, pin exactly 1.0.0 (not ^1.0), commit the published config. |
| Maintenance abandonment: 1 maintainer, 12 stars, 0 dependents, no release since Nov 2022 | Medium | Acceptable only because fork cost is low. Pre-approve a fork budget and name an owner. |
| Multi-tenancy retrofit | Medium | Cheapest to verify now, most expensive to add later — resolve before the data model is committed. |
Key questions (need answers before we commit).
guzzlehttp/guzzle constraint reconcile with our lockfile? If Laravel 8/PHP 8.0, adoption implies a framework upgrade — that's a roadmap dependency, not a sprint surprise.Illuminate\Http\Client\Response? Anything but our own DTOs means coupling to Safaricom JSON field names.Stack fit. Good for a Laravel 9+ app that wants M-Pesa as one payment rail among several. The facade + config + token caching model is idiomatic and low-ceremony, which is genuinely the package's value proposition. It is a poor fit for anything unusual: multi-tenant marketplaces, per-request credential rotation, or non-Laravel services. If M-Pesa is the primary revenue rail with high volume and strict SLA, the thin wrapper stops paying for itself and we should expect to fork.
Migration path. There is nothing to migrate from if we're greenfield on M-Pesa. If we have existing raw-cURL Daraja code, treat migration as opportunistic: introduce PaymentGateway, implement it via the package, and port flows one at a time (STK Push → status query → C2B → B2C → reversal). Do not attempt a big-bang port of reversal/refund before the reversal credentials exist — that path is separately provisioned and routinely forgotten until the first refund is needed.
Compatibility. Pin savannabits/daraja:1.0.0 exactly, commit config/daraja.php into the repo rather than relying on package defaults, and record the verified Laravel/PHP/guzzle matrix somewhere durable (README + CI job). Add contract tests that replay recorded sandbox request/response fixtures through Http::fake() — this is the specific artifact that catches vendor payload drift on any future package bump and is what protects us during a fork.
Sequencing (with exit criteria).
src/, stand up ngrok/expose callback, complete one sandbox STK Push, and log a full real callback body. In parallel: submit shortcode and go-live documentation. Exit: a persisted row carries a CheckoutRequestID before the request is sent, and the callback handler finds it.PaymentGateway interface + MpesaPaymentService; state machine with row locks; queued outbound calls with bounded concurrency, timeout, 3 tries, backoff; 401 → flush cache and retry exactly once.pending > 90s and all indeterminate; escalate > 24h to manual review with alerting). Sandbox-test reversal on day three of adoption, not month three of production.Maintenance. The package itself is low-maintenance because it is small and we should expect to own it. Single maintainer, no release since Nov 2022, zero dependents. The honest position: we are adopting a pinned snapshot, not a supported dependency. Name an internal owner, watch the upstream repo, and pre-approve a fork budget — the surface is small enough that forking is cheap, and a fork removes the dependency risk entirely. The expensive maintenance is not the package; it is our own state machine, reconciliation job, and result-code taxonomy.
Support. Two distinct support burdens. First, engineering: a reconciliation job, alerting, and a status-query path that must be treated as the source of truth (the callback is only a fast path). Second, ops/business: a manual-review queue for transactions escalated past ~24h, plus a runbook covering the two-phase STK flow, result-code meanings, callback retry behaviour, portal registration steps, and the go-live checklist. Write this in our repo, not by pointing at the package README — the knowledge is ours to own and the package will not supply it. Budget real headcount for the review queue; it is not a set-and-forget job.
Scaling. Three constraints, in order of when they'll bite. (1) Per-shortcode throttling — M-Pesa rate-limits per shortcode, so outbound calls need a dedicated queue connection with bounded concurrency and a RateLimiter/circuit breaker so a Safaricom incident doesn't cascade into queue depth for unrelated work. (2) Synchronous calls in web requests — never; 30-second page loads during Safaricom incidents are the predictable outcome. (3) Multi-tenancy — if the client is a singleton with fixed config, per-merchant scaling requires one worker pool per shortcode plus runtime config([...]) scoped bindings, and per-instance token caching risks cache-key collisions across tenants. Verify before the data model lands; retrofitting this is the single most expensive thing on the list.
Failure modes. Define these up front and design the runbook around them: indeterminate (request may have succeeded, response lost — a real state, swept by reconciliation, never auto-failed); duplicate/out-of-order callbacks (normal, absorbed with 200 OK); 1037 timeout (may have charged — always poll); OAuth 401 on a business request (cached-but-revoked token — flush and retry once, never loop); cache store flushed in a deploy script (token hammering → rate limit — exclude the package's store from cache:clear and health-check it); Safaricom outage (circuit breaker open, queue drains on recovery, reconciliation catches the tail). The unifying rule: unknown is not failed — anything unrecognised routes to status query, and terminal states are absorbing.
Ramp-up. For a competent Laravel engineer: ~1 hour to read src/ end-to-end, ~1 day to a working sandbox STK Push, ~1 week to a hardened implementation with a state machine and queue. The package is small enough that reading it is a prerequisite, not an option — and it is on a money path, so "I skimmed the README" isn't acceptable. Do not gate CI on the sandbox: it is flaky enough that a manually-triggered smoke test is the right cadence. Total realistic ramp for the team including security review and runbook: 3–4 weeks of engineering-adjacent effort, with the launch date set by Safaricom's approval clock rather than ours.
Recommendation. Adopt for a pilot or a secondary payment rail, behind our own PaymentGateway interface, pinned to 1.0.0 with committed config, with a named owner and a pre-approved fork budget. Do not adopt it as a payments platform, and do not start the shortcode paperwork after the spike — start it the same week.
How can I help you explore Laravel packages today?