Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Daraja Laravel Package

starnerz/laravel-daraja

Laravel Daraja wraps all seventeen Safaricom M-Pesa Daraja APIs in ordinary Laravel code: STK Push, C2B, B2C, B2B, Dynamic QR, Ratiba, Bill Manager, typed responses, cached tokens, and testable callbacks. PHP 8.3+, Laravel 12/13.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install with composer require starnerz/laravel-daraja. v5.2.0 accepts guzzlehttp/guzzle: ^7.8 || ^8.0, so it installs cleanly on both Laravel 12 (Guzzle 7) and Laravel 13 (Guzzle 8) stacks.
  • Publish the package config (php artisan vendor:publish and pick the Daraja provider) and commit config/daraja.php with env-driven values only — never the credentials themselves.
  • Populate the .env from your Safaricom developer portal app: consumer key/secret, shortcode (Paybill/Till), passkey, initiator name/password (for B2C/B2B), and the callback/result URLs. Set sandbox vs production via env so staging can never hit live money.
  • Read https://laraveldaraja.com/ first — the docs moved there in this release. Old starnerz.github.io/daraja-docs/ links still redirect, so bookmarks and blog posts keep working.
  • First use case to wire up: an STK Push (M-Pesa Express) checkout. You call the package to push a prompt to the customer's phone, store the returned CheckoutRequestID, and receive the result asynchronously at your callback URL. That single flow exercises almost everything else — token auth, request signing, callback routing, and reconciliation — so build it end-to-end in sandbox before touching B2C or C2B.
  • Run php artisan config:clear (and config:cache in deploy) after changing any Daraja env value; the package reads cached config.

Implementation Patterns

Keep the SDK at the edge of your domain. Wrap every package call in your own small service (e.g. App\Services\Mpesa\StkService) that translates your models into package calls and the package response into your own DTO. When request/response shapes shift, you fix one class instead of every controller.

One callback controller per product, one route each. Safaricom posts to distinct URLs for STK, C2B confirmation, C2B validation, B2C result, balance result, and reversal result. Registering them separately keeps parsing logic honest — the payload shapes genuinely differ (Body.stkCallback vs Result vs TransactionType), and a single "smart" handler becomes unreadable fast.

Acknowledge first, process later. Callbacks are fire-and-forget HTTP POSTs to your app; Safaricom does not retry reliably and will time out. Return 200 immediately, dispatch a queued job with the raw payload, and do the ledger work in the worker. Persist the raw JSON before dispatching so you can replay it.

Make the callback idempotent and unique-indexed. Store CheckoutRequestID / MerchantRequestID / TransID with a unique constraint. Safaricom retries, sandbox duplicates, and your own replay jobs will all hit the same row eventually. An upsert keyed on the request ID is the cheapest insurance you'll ever write.

Model the lifecycle, don't infer it. Give yourself a mpesa_transactions table with an explicit status enum (pending → success | failed | cancelled | timeout) plus the raw response. Your order fulfilment should key off that state, not off the raw ResultCode scattered through the codebase.

Reconcile the silences. STK pushes that never call back are the single most common production complaint. Ship a scheduled command that finds pending transactions older than ~2 minutes and calls the Transaction Status API. Never mark an order paid purely because a callback arrived — confirm with a status query for anything above a low value threshold.

Per-tenant / multi-shortcode config. If you serve multiple merchants, resolve shortcode and passkey at runtime rather than reading the singleton config. Cache each tenant's OAuth token separately; tokens are valid roughly an hour and re-fetching on every request is the fastest way to get throttled.

Test with Http::fake(), not the sandbox. Assert your request payload (amount is an integer, phone is 2547XXXXXXXX) and your callback handling by posting fixture JSON directly at your route. Reserve live sandbox credentials for a nightly/manual suite.

Version pinning. If another dependency in your app hard-requires guzzlehttp/guzzle: ^7.0, you simply won't resolve Guzzle 8 — that's fine, nothing in this package forces it. Nothing changed in the package code in 5.2.0, so upgrading from 5.1.x is a composer update with no code edits.

Gotchas and Tips

  • Callback URLs must be public HTTPS, unauthenticated, and CSRF-free. Put them in routes/api.php (or withoutMiddleware), and never behind auth — Safaricom's servers have no session. Local dev needs a tunnel (ngrok/expose); localhost callbacks silently never arrive.
  • Don't trust the callback body as proof of payment. It's an unauthenticated POST from the internet. Verify against Transaction Status or your own expected amount/shortcode/account reference before crediting anything.
  • Amounts are whole KES integers. 10.5 fails or gets truncated. Convert at the boundary, not in the package call.
  • MSISDN format is unforgiving: 2547XXXXXXXX / 2541XXXXXXXX. Strip +, leading 0, and spaces in one place.
  • Result codes you'll actually see: 0 success, 1032 cancelled by user, 1037 user unreachable/timeout, 2001 wrong PIN, 1 insufficient balance, 1019/1001 transaction in progress. Map them to human messages once and reuse.
  • The STK prompt times out in about a minute. Treat "no callback within 90s" as timeout, not failed, and surface a retry to the user.
  • Sandbox lies about edge cases. The simulator is optimistic; validate failure paths by posting crafted callback JSON at your own endpoint rather than waiting for the sandbox to produce a 1032.
  • B2C/B2B need the initiator credentials and, in production, IP allowlisting on the Safaricom side. Test credentials are not carried over from the developer portal's app view — request them explicitly.
  • Config caching bites in deploy pipelines. A stale cached daraja.php after an env change is a classic "why is it still hitting sandbox" bug. config:clear in the release step, always.
  • Extension points: the config file and the container binding are your hooks — bind your own client wrapper or swap the HTTP client via the container rather than editing vendor code. If you need custom retry/backoff on Daraja's 5xx responses, that's where it belongs.
  • Docs and security contact moved: documentation is now https://laraveldaraja.com/ (old GitHub Pages URLs redirect), and vulnerability reports go to [email protected] — update your internal runbook and any SECURITY.md pointer you mirrored.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums