jomweb/billplz
A Laravel package for integrating Billplz, the Malaysian payment gateway. Create and manage bills, redirect customers to secure payment pages, and handle callbacks/webhooks through a clean, configurable API. Supports sandbox and production environments for fast checkout flows.
Architecture fit. jomweb/billplz is a framework-agnostic PHP client, not a Laravel package. That is a deliberate architectural choice with real trade-offs: you get a thin, portable SDK with no framework coupling, but you also get no ServiceProvider, no facade, no publishable config, no migration stubs, and no Laravel-native testing helpers. In a Laravel codebase this means the TPM should budget for a small internal integration layer — a config file, a singleton binding in AppServiceProvider, and a domain-level adapter — rather than assuming a drop-in. The upside is that the package cannot pull you into a Laravel version conflict, and the same client could be reused by an admin tool, a queue worker, or a future non-Laravel service (e.g. a Go/Node reconciliation job calling your own API). It fits cleanly behind a PaymentGateway interface; it does not fit as a direct dependency sprinkled through controllers.
Integration feasibility. High. The package targets Billplz API v3+ over HTTP with a JSON/PSR-style client surface, so the integration surface is small and well understood: create a bill (collection ID, amount in cents, email, name, callback URL, redirect URL, optional reference_1/reference_2), retrieve a bill, delete a bill, and verify the x_signature on callbacks. The critical feasibility question is not whether you can call the API — you can, in an afternoon — but whether the package exposes the signature helper and the bill-state fields you need for reconciliation. Confirm: (a) HMAC-SHA256 x_signature generation/verification is exposed as a first-class method for the exact sorted-key concatenation Billplz specifies, (b) bill retrieval returns paid, paid_at, state, and paid_amount, and (c) the HTTP transport is swappable (PSR-18/HTTPlug) so it does not fight Laravel's bundled Guzzle version. A sandbox spike (Billplz sandbox environment, separate keys) should be a one-day timebox, not a sprint.
Technical risk. Three risks dominate. First, bus factor and stewardship: 77 stars and a small maintainer base means community-grade support; the recent release date is reassuring, but the low opportunity delta suggests a stable, low-churn package — good for stability, less good if you need a fast fix for a Billplz API change. MIT licensing means you can fork and vendor it, which materially de-risks this. Second, API version drift: the description claims "v3 and beyond," but Billplz has been evolving toward payment-order style flows alongside classic bills. Verify which endpoints are actually wrapped before you commit to the claim. Third, security correctness is on you, not the package: Billplz callbacks are form-encoded POSTs (not JSON), are retried, and must be treated as untrusted input. The package can verify a signature; it cannot enforce idempotency, re-fetch the bill server-side before marking an order paid, or protect you from a misconfigured callback URL. Those are application responsibilities and should be explicit acceptance criteria.
Key questions. Does the package pin a PHP minimum that matches our runtime? Is the HTTP client injectable, and does it conflict with our Guzzle/PHP version matrix? Does it expose signature verification as a documented method with tests? Which Billplz endpoints and payment methods (FPX, card, e-wallet, BNPL) are covered versus requiring raw calls? Is there a test suite, and does it mock the transport or require live sandbox keys? Who are the maintainers and what is the historical response time on issues? What is Billplz's own rate limit and callback retry policy, documented from their side rather than inferred from the library?
Stack fit. Laravel 10/11/12 on PHP 8.1+ is a clean fit precisely because the package imposes no framework constraints. The integration pattern I would mandate: config/billplz.php (API key, collection ID, sandbox toggle, callback/redirect base URLs) sourced from environment; a singleton binding of the configured client in a dedicated BillplzServiceProvider; and a BillplzGateway class implementing an internal PaymentGateway contract that returns your own DTOs (PaymentIntent, PaymentResult) rather than package objects. This keeps the vendor type out of controllers, jobs, and tests, and makes a future switch to Stripe/Adyen/CHIP a swap of one adapter. Amounts must be handled as integer cents end to end — establish this in the DTO, not in a controller.
Migration path. If the product currently creates bills via hand-rolled cURL or a competing SDK, migrate behind the adapter in place: implement BillplzGateway against the package, keep the old implementation as a fallback bound by config flag, and switch with a feature flag. If this is greenfield, start with the adapter from day one — do not let the package leak into the domain. For existing orders in flight, run both implementations read-only against sandbox to diff bill creation payloads before cutover.
Compatibility. Watch three things: PHP version floor, Guzzle/PSR-18 major version (Laravel already ships Guzzle, so prefer the package's PSR-18 mode over a bundled concrete client to avoid duplicate-transport surprises), and the absence of Laravel auto-discovery — there is nothing to discover, so composer require alone will do nothing visible. Confirm the package does not rely on global state or a static config singleton, which would break under Octane and in parallel queue workers.
Sequencing. (1) Sandbox spike and signature verification proof, one day. (2) Config + service provider + adapter scaffold with contract tests. (3) Bill creation on checkout, with reference_1 bound to your internal order ID. (4) Callback endpoint: verify signature, re-fetch bill from API, enforce idempotency in a payments table with a unique constraint on bill ID, then transition order state. (5) Redirect handler for the user-facing return (treated as UI only, never as proof of payment). (6) Reconciliation job polling unpaid bills older than N minutes. (7) Refund/manual-ops runbook, then flagged production rollout at low traffic volume before full exposure.
Maintenance. Low surface, low cadence. Budget a small recurring allocation — roughly a few hours per month — for dependency updates and Billplz changelog review, plus a defined trigger (Billplz deprecation notice or a package issue going unanswered for two weeks) at which you fork under MIT and vendor the client internally. Pin the version with a caret constraint and commit composer.lock; do not float on dev-main.
Support. No SLA. Escalation for gateway-side incidents goes to Billplz merchant support, not the package maintainers, and the TPM should ensure merchant account contacts and sandbox/production key custody are documented before launch. Internally, one named owner for the payment integration with a runbook covering: signature failures, missing callbacks, duplicate callbacks, and stuck-pending orders.
Scaling. The client is stateless and request-scoped, so it scales horizontally without thought and is Octane-safe provided config is injected rather than read from globals. The real ceilings are external: Billplz API rate limits and callback delivery throughput. Decouple by queueing callback processing (respond 200 fast, do the work in a job), and keep the checkout path free of synchronous bill-status polling. Reconciliation should run as a scheduled command with backoff, not a tight loop.
Failure modes. The ones that will actually bite: a callback that never arrives (network, bad URL, firewall) leaving orders permanently pending — mitigated only by reconciliation polling; duplicate callbacks marking an order twice — mitigated by a DB unique constraint on bill ID, not by application logic alone; signature mismatches from parameter ordering or URL-encoding assumptions; sandbox and production keys or collection IDs crossed in config; amount rounding errors from treating MYR as a float; and duplicate bill creation on user retry, which reference_1 plus a create-lock in your own database must prevent. Treat every callback as a hint, and the API fetch as the source of truth.
Ramp-up. A mid-level Laravel developer can be productive with the package in half a day, because the API is small and the README-sized surface is easy to read. The genuine ramp-up cost is domain knowledge — webhook trust boundaries, idempotency, reconciliation, and the difference between redirect and callback — which is a two-to-three-day investment and is where integration bugs concentrate. Note also the regional constraint: Billplz is a Malaysian gateway (MYR, FPX and local e-wallets), so this package is only relevant if Malaysia is in the market plan, and it should sit behind the gateway abstraction so that geography does not dictate architecture.
How can I help you explore Laravel packages today?