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 Mtn Momo Laravel Package

bmatovu/laravel-mtn-momo

Laravel package for integrating MTN Mobile Money (MoMo) APIs. Collect payments, disburse funds, check transaction status, and handle callbacks via a clean, fluent interface, with sandbox and production environments plus publishable configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish

composer require bmatovu/laravel-mtn-momo
php artisan vendor:publish --provider="Bmatovu\MtnMomo\MtnMomoServiceProvider"

Auto-discovery is enabled on modern Laravel, so no manual provider registration is needed. After publishing, look at two things first: the generated config file (usually config/mtn-momo.php) and the package's README / src/ directory — the class names and config keys are the contract you'll code against, and they change between minor releases.

2. Provision credentials before writing any code

The MTN MoMo API requires three separate values per product, and they are not interchangeable:

  • Subscription key — one per product (Collections vs Disbursements). Sandbox keys only work against the sandbox host.
  • API user (UUID) — created via POST /v1_0/apiuser with an X-Reference-Id you generate.
  • API key — returned once when you call POST /v1_0/apiuser/{uuid}/apikey.

If the package ships artisan commands for this, use them; otherwise do it once by hand with cURL against the sandbox and paste the results into .env:

MTN_MOMO_TARGET_ENVIRONMENT=sandbox
MTN_MOMO_SUBSCRIPTION_KEY=xxxxxxxxxxxxxxxx
MTN_MOMO_API_USER=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
MTN_MOMO_API_KEY=xxxxxxxxxxxxxxxx
MTN_MOMO_CALLBACK_HOST=https://your-public-host.test

3. First use case: collect a payment (requestToPay)

Collections is the 80% case — pulling money from a customer's wallet. The flow is always: generate a UUID → POST requestToPay → get 202 Accepted → wait for callback or poll → read status. Anything that treats the 202 as "payment succeeded" is broken.

$referenceId = (string) Str::uuid();

MtnMomo::requestToPay([
    'amount'       => '5000',
    'currency'     => 'UGX',
    'externalId'   => $order->id,
    'payer'        => ['partyIdType' => 'MSISDN', 'partyId' => '256770000000'],
    'payerMessage' => 'Order #'.$order->id,
    'payeeNote'    => 'Thanks',
], $referenceId);

Persist $referenceId on the order immediately — it is the only key you'll have for reconciliation.

Implementation Patterns

Model the reference ID as your primary key for the transaction. Use a UUIDv4 generated by you, stored in a column with a unique index. Every status poll callback, retry, and support ticket resolves through this ID. MTN's own financialTransactionId arrives later and should be stored alongside it, not used as the lookup key.

Separate the intent from the confirmation. The cleanest integration is an internal state machine:

enum MomoStatus: string { case Pending = 'PENDING'; case Success = 'SUCCESSFUL'; case Failed = 'FAILED'; }

Create the record as Pending on the API call, flip it in a webhook controller, and fire domain events (MomoPaymentConfirmed, MomoPaymentFailed) so billing/fulfilment code never touches the MoMo client directly.

Webhook controller, then queue. MTN retries callbacks and does not guarantee ordering. Respond 200 fast, verify the payload shape, dedupe on referenceId + status, and push the actual work to a job:

Route::post('/webhooks/momo', function (Request $request) {
    MomoCallback::dispatch($request->all())->onQueue('payments');
    return response()->noContent();
})->withoutMiddleware([VerifyCsrfToken::class]);

Do not add the webhook route behind auth middleware, and remember php artisan route:list is the fastest way to confirm it's actually registered.

Poll as a fallback, not the default. Callbacks get lost during local dev, deploys, and network blips. A momo:poll-pending scheduled command (every minute, only Pending rows older than 30s, max N attempts) closes the loop cheaply. Give it a hard ceiling — abandoned pending transactions should be marked Failed after ~10–15 minutes rather than polled forever.

Disbursements are a separate client and separate key. If you're paying refunds or vendor payouts, expect a second subscription key, a second api user/api key pair, and separate credentials in config. Wrap each in its own service class rather than branching inside one god-object.

Wrap it in your own interface. The package is thin and its API surface is unstable (143 stars, one maintainer, last release Feb 2025). A MomoGateway interface with a FakeMomoGateway binding for tests means a future swap or a package-breaking change costs you one class.

Test against sandbox with a real callback host. Use ngrok/expose/Laravel Herd's share feature, set MTN_MOMO_CALLBACK_HOST to the tunnel, and approve the pending request in the MTN sandbox UI (the sandbox does not send a real prompt to a phone).

Gotchas and Tips

  • The 202 is not a receipt. requestToPay and transfer return 202 Accepted with an empty body. Only GET /requesttopay/{referenceId} or a callback tells you the outcome. Every "payment lost" bug I've seen traces back to this.
  • Reference IDs must be UUIDv4, not v1, not an auto-increment, not a ULID. MTN rejects non-v4 UUIDs with an opaque 400. Use Str::uuid() and nothing else.
  • Subscription keys are per-product and per-environment. A Collections key will 401 on Disbursements, and a sandbox key will 401 against mtnuganda/mtnghana. If you get 401s, check environment/key pairing first — it's the most common misconfiguration.
  • Access tokens expire in ~1 hour. If the package caches tokens, make sure the cache store is per-environment (sandbox vs production should never share a token cache). Redis key prefix collisions between staging and prod cause maddening intermittent 401s.
  • Amounts are strings, and some currencies reject decimals. UGX, RWF, and XAF have no minor units — send "5000", not "5000.00" and not 5000. Send currency explicitly every call; there is no default.
  • MSISDN formatting matters. Include the country code without a leading + (256770000000). +256... and 0770... both fail validation on some endpoints, and the error message is unhelpful.
  • Callbacks can arrive before your HTTP response is committed. A fast callback plus a slow DB transaction will race your own write. If you can't guarantee the record exists first, have the webhook handler upsert on the reference ID rather than assume a row is present.
  • Log the raw request/response bodies, but redact credentials. The MTN error bodies are short and cryptic ({"code":"INVALID_CALLBACK_URL"}). Without the raw payload and the X-Reference-Id you sent, debugging is guesswork. Never log Ocp-Apim-Subscription-Key or the API key.
  • Callback URLs must be HTTPS and publicly reachable. MTN validates reachability at registration time in some environments. http://localhost will silently never fire in sandbox.
  • Sandbox has different failure semantics. Insufficient-funds and payer-rejection paths are simulated in the portal, not on the device. Exercise FAILED and REJECTED explicitly in your state machine tests; many teams ship having only ever seen SUCCESSFUL.
  • Rate limits are real. Poll-on-every-page-load will get you throttled. Back off with exponential jitter, and cache the final status — terminal states never change.
  • Extension points to prefer: the package's binding in the container (rebind it to swap HTTP clients or add middleware for logging/retries), Laravel's Http client factory if the package exposes one, and the events it fires. Reach for those before editing vendor code — this package's release cadence means patches won't survive composer update.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package