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 Azampay Laravel Package

alphaolomi/laravel-azampay

A Laravel package for integrating AzamPay, Tanzania's payment gateway, into your PHP apps. It offers a clean, fluent API for mobile money and bank payments, checkout flows, callbacks, and transaction handling, making payment integration simple and fast.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin the major

composer require alphaolomi/laravel-azampay:^1.0

v1.0.0 is the first stable major. The release notes are dominated by Dependabot bumps, a rebuilt test workflow, and Laravel 11 support — there is no documented breaking-change list. Treat it as "stable API, undocumented delta from 0.2.0" and diff the source before upgrading an existing integration (git diff v0.2.0..v1.0.0 -- src config).

Publish config and set credentials

php artisan vendor:publish --tag=azampay-config   # verify the tag name in the package's ServiceProvider

Then populate .env with the credentials AzamPay issues you (client ID, client secret, app name) plus the environment/base URL toggle. Read config/azampay.php immediately after publishing — this file is the source of truth for endpoint selection, sandbox vs production, and which keys the client actually reads.

Where to look first, in order

  1. config/azampay.php — credential keys, base URL, sandbox flag.
  2. src/ — the client/service class and the facade (if one is registered). This tells you the real method names for checkout, collection, and disbursement; don't trust blog posts over the source.
  3. src/Http/ or src/Client.php equivalents — the request payload shape, which is what you will spend most of your debugging time on.
  4. tests/ — the fastest way to see a working payload and the expected response envelope, especially for sandbox behaviour.
  5. README.md — updated in this release (PR #19), so it is more likely current than any third-party tutorial.

First use case: mobile-money checkout

The canonical day-one task is a TZS checkout for M-Pesa / Airtel Money / Tigo Pesa / Halopesa. The flow is: your controller validates the order → calls the package to create a checkout → AzamPay returns a redirect/hosted-payment URL or an immediate push → the customer approves on their handset → AzamPay calls your callback → you verify the transaction server-side and mark the order paid. Build a throwaway route against sandbox credentials first and log the raw response body; the response envelope is the thing you need to know before designing your schema.

Implementation Patterns

Wrap it, don't sprinkle it

Never call the facade from controllers. Put a single AzamPayService (or an action class per operation) behind an interface, bind it in a service provider, and inject it. AzamPay's API is the part most likely to change or need a sandbox/production switch, and a one-class seam means an upgrade to ^2.0 touches one file.

final class AzamPayService
{
    public function __construct(private AzamPay $client) {}

    public function startCheckout(Order $order): CheckoutResult { /* ... */ }
}

Token/authentication lifecycle

The package handles the OAuth token exchange internally, but you should know whether it caches the token or re-authenticates per call. If it re-auths on every request, cache the token yourself in the cache store with a TTL slightly below the expiry, or you will double your HTTP calls on every page that shows a payment button.

Checkout orchestration pattern

Keep the transaction lifecycle in your own table, not in AzamPay's:

  • Generate your own externalId / merchant reference (ULID on the order) and send it to AzamPay. This is the only reliable key for reconciliation, idempotent retries, and support tickets — the provider's transaction ID is not available until after the call.
  • Persist pending before the HTTP call, not after. If the request times out you still need a row to reconcile.
  • Store the raw provider response in a json column. You will need it when a customer says "I paid" and the callback never arrived.
  • Drive state transitions (pending → paid → failed → refunded) through a state machine or explicit guarded updates; webhooks and polling both write to the same row and will race.

Callback/webhook handling

  • Register the callback route outside web middleware or explicitly exclude it from CSRF.
  • Accept the request, validate the signature/shared secret if the package exposes one, persist a PaymentCallback row, dispatch a job, and return 200 immediately. AzamPay will retry on non-2xx — you want those retries to be cheap and idempotent.
  • Process in a queued job and key the job on the provider reference so duplicate deliveries collapse (use ShouldBeUnique or a DB unique constraint).
  • Never trust the callback payload as the final word: re-query the transaction status via the package's lookup endpoint before fulfilling the order.

Polling fallback

Webhooks get lost. Run a scheduled command (everyFiveMinutes) over pending transactions older than N minutes and call the status/lookup endpoint. This is the single highest-value piece of glue code you will write around this package.

Testing

Http::fake() around the client's base URL gives you hermetic tests without a sandbox round-trip. Fake the success envelope, the "insufficient balance" envelope, and a 500, and assert your state machine lands in the right place each time. Keep one sandbox smoke test in a separate, manually-run suite — the CI workflow was reworked in this release (PR #28), so mirror what the package itself fakes rather than inventing payloads.

Disbursement / collection

If you use payouts (B2C) as well as collections, keep them in separate service methods and separate credential scopes. Payouts deserve their own queue, their own retry policy, and an approval step; a bug in a checkout that double-pays a customer is a much worse day than one that double-charges.

Gotchas and Tips

The v1.0.0 bump is under-documented. A major version with only CI and dependency changes in the notes usually means one of two things: accumulated breaking changes were finally stabilised, or the maintainer simply cut 1.0 to signal Laravel 11 readiness. Do not upgrade in production on the strength of the changelog. Run the diff, run your Http::fake test suite, and do a real sandbox transaction before merging.

Laravel 11 shape-shifting. With Laravel 11 support added (PRs #34/#35), remember there is no app/Http/Kernel.php. If the package ships routes or middleware, register them in bootstrap/app.php. Also check the package's PHP constraint against your runtime — Laravel 11 shops often need PHP 8.2+, and older pinned packages in the same project can conflict.

config:cache bites. Any deployment that runs php artisan config:cache freezes config/azampay.php values. Reading env('AZAMPAY_...') anywhere outside the config file will silently return null in production while working fine locally. Keep all env reads inside the config file, and re-run config:cache on every deploy.

Sandbox versus production is a config decision, not a code decision. Do not branch on app()->environment() to pick credentials. Use an explicit config flag so staging can point at sandbox and a production hotfix can point at production without a code change. Log the resolved base URL once at boot — a surprising number of "payment failing" tickets are a production deploy pointed at sandbox.

Phone number formatting is the most common 400. AzamPay expects the local format with country code (2557xxxxxxxx), not +255, not 07xxxxxxxx. Normalise and validate before you call, and store the normalised form on the order so support can see exactly what was sent.

Amounts and currency. Send integers in TZS with no decimals handling assumed; if you let a float through, 1000.0 can serialise as 1.0e3 in some JSON encoders. Cast explicitly and assert currency is TZS — the API error for a wrong currency is not always descriptive.

Don't retry checkout creation blindly. Retrying a POST that creates a payment can produce two pushes to the customer's handset. Retry only on connection-level failures with the same externalId, and reconcile via status lookup before retrying on a timeout.

Redact secrets and tokens in logs. The package's exception messages may include the full request; if you log them wholesale you will write the client secret and bearer token into your log aggregator. Wrap calls in a try/catch that logs a sanitised payload.

Debugging checklist when a payment "doesn't work": resolved base URL → token acquired (or not) → full request body → HTTP status → response body → your transaction row's stored raw response. The package's own test fixtures are the reference for what a healthy response looks like.

Extension points. The practical levers are: publish and edit the config, bind a fake/decorator in the container for tests, and listen for any events the package dispatches rather than polling your own table. If you need custom HTTP behaviour (proxy, timeout, retry), prefer decorating the underlying client in a service provider over forking — forking is what makes the next major painful.

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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
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