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

Billplz Bundle Laravel Package

fd6130/billplz-bundle

A Laravel bundle for integrating the Billplz payment gateway into your PHP applications. Simplifies creating bills, handling payment callbacks, and verifying transactions.

View on GitHub
Deep Wiki
Context7

Getting Started

This release (v1.0.3) is a small patch: it fixes an optional parameter deprecation notice — the kind PHP 8.0+ emits when a parameter with a default value is declared before a required one. If you're on PHP 8.1/8.2/8.3 and were seeing Deprecated: Optional parameter $x declared before required parameter $y in your logs on v1.0.2, upgrading silences it. There are no API, config, or signature changes, so it's a drop-in bump.

Minimal steps:

  1. composer require fd6130/billplz-bundle (or composer update fd6130/billplz-bundle if already installed — anything below 1.0.3 will spam deprecations on modern PHP).
  2. Locate the bundle's configuration file (typically config/packages/billplz.yaml in Symfony; in Laravel you'll be wiring the service yourself — see below) and set your API key, x-signature key, and the sandbox vs. production base URL.
  3. Look first at two things: the API client/service class (this is where createBill, getBill, deleteBill, and collection methods live) and the Billplz v3 API docs for the exact payload shape. The bundle is a thin wrapper; the authoritative contract is Billplz's, not the bundle's.
  4. First use case to get working end-to-end: create a bill → redirect the customer to the returned url → handle the redirect back with getBill. Only after that works should you wire the webhook.

Note on framework fit: despite the "Laravel developer" framing, this is a Symfony bundle, not a Laravel package. Expect no Billplz facade, no php artisan vendor:publish, no config file in config/. You'll register the bundle (Symfony) or bind the client class into Laravel's container with a small service provider of your own.

Implementation Patterns

The three-leg payment flow. Billplz is redirect-based, and every integration has three legs that must all be handled:

// 1. Create — server side, before redirect
$bill = $billplz->createBill($collectionId, [
    'email'        => $order->email,
    'name'         => $order->name,
    'amount'       => $order->total_cents,   // integer, cents
    'callback_url' => route('billplz.webhook'),
    'redirect_url' => route('billplz.return'),
    'reference_1'  => $order->id,            // your idempotency anchor
]);
$order->update(['billplz_bill_id' => $bill['id']]);
return redirect($bill['url']);

// 2. Return — user-facing, cosmetic only
$bill = $billplz->getBill(request('billplz')['id']);
// show "paid / pending / failed" — do NOT fulfil here

// 3. Webhook — authoritative, fulfils the order

Wrap the client in your own service. Don't inject the bundle's client into controllers. A thin BillplzService gives you one place to swap base URLs for sandbox, add logging, and map the bundle's return types to your domain. In Laravel, bind it as a singleton in a BillplzServiceProvider and read credentials from config/services.php / .env.

Always verify x_signature on the webhook. This is the single most-skipped step. The signature is an HMAC-SHA256 over the callback parameters, sorted by key, joined as key + value concatenated, excluding x_signature itself:

$payload = collect($request->all())->except('x_signature')->sortKeys();
$source  = $payload->map(fn ($v, $k) => $k.$v)->implode('');
$valid   = hash_equals(hash_hmac('sha256', $source, config('services.billplz.x_signature')), $request->x_signature);
abort_unless($valid, 403);

Use hash_equals, not ===.

Treat the webhook as idempotent and out-of-order. Billplz will retry on non-2xx, and paid events can arrive twice. Key your handler on billplz_bill_id + paid_at/state, make fulfilment a no-op if already processed, and return 200 fast — push real work to a queued job.

Reconcile with getBill as a backstop. For orders stuck in pending past a timeout, a scheduled command that polls getBill for open bills catches webhooks you never received. Same code path as the webhook handler, so keep the state transition in one method.

Sandbox and production are separate worlds. Different base URL, different API key, different x-signature key, different collection ID. Model this as two config blocks selected by an env var — not a single key you swap manually before deploying.

Gotchas and Tips

  • Upgrade to 1.0.3 if you're on PHP ≥ 8.0. The deprecation fix is the entire release; nothing else changed. Low risk, but pin ^1.0.3 so a fresh install doesn't resolve back to the noisy version.
  • Amounts are integers in cents, MYR only. No decimals, no float arithmetic anywhere near the amount. round($total * 100) is fine; $total * 100 is not. Billplz does not do multi-currency.
  • Don't fulfil on the redirect. Users can close the tab, share the return URL, or arrive before the webhook. The redirect is for UX; the webhook (or a getBill poll) is the source of truth.
  • Both URLs must be publicly reachable. Billplz cannot POST to localhost or *.test. Use a tunnel (ngrok/expose) in local dev, and remember HTTPS in production.
  • The bundle is a thin wrapper — errors are shallow. Don't assume non-2xx is the only failure mode. Check the response body for the message/code fields, log the raw payload (redact keys), and surface a generic message to users. A createBill that returns null instead of throwing will silently produce a broken checkout.
  • Collections must exist and be active. A stale or unverified collection ID is the most common cause of "bill creation works in sandbox, fails in prod" — right after swapped keys.
  • No Laravel ergonomics out of the box. No facade, no config publishing, no artisan commands, no migration for storing bills. Budget an hour for the service provider + config binding; it's the difference between a clean integration and app(BillplzClient::class) scattered through controllers.
  • Redact the API key and x-signature key from every log line. Laravel's Http client logging and exception reporting will happily dump the Authorization header. Exclude them explicitly.
  • Extension points are limited. There's no event dispatcher or pluggable HTTP layer exposed by the bundle, so if you need retry logic, a custom Guzzle handler, or per-request timeouts, wrap the client behind your own interface and let your wrapper own the HTTP concerns. That also makes it trivial to swap in a fake for tests.
  • Test against a fake, not the sandbox, in CI. Sandbox is rate-limited and stateful. Stub your wrapper's methods and keep one manual sandbox smoke test for the happy path.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings