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

lahirulhr/laravel-payhere

A Laravel package for integrating the PayHere payment gateway into your applications. Simplify checkout, payment initiation, and notification handling with a clean, framework-friendly API designed for quick and reliable Sri Lankan payment integration.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and publish config

    composer require lahirulhr/laravel-payhere
    php artisan vendor:publish --tag=payhere-config
    

    Confirm the service provider is auto-discovered (php artisan about or check bootstrap/providers.php on Laravel 11+).

  2. Set the credentials in .env — the values come straight from the PayHere merchant dashboard (Settings → Domains & Credentials):

    PAYHERE_MERCHANT_ID=1210000
    PAYHERE_MERCHANT_SECRET=xxxxxxxxxxxxxxxx
    PAYHERE_SANDBOX=true
    PAYHERE_RETURN_URL=/payhere/return
    PAYHERE_NOTIFY_URL=/payhere/notify
    

    Cache-bust after changing them: php artisan config:clear.

  3. Where to look first

    • config/payhere.php — merchant ID, secret, sandbox flag, and the return/notify route names. The sandbox flag is what swaps sandbox.payhere.lk for www.payhere.lk; verify it actually does so by dumping the generated checkout URL.
    • The src/ directory — look for a hashing helper (MD5 of merchant secret), a "checkout payload" builder, and a notify-verification method. These three are the whole surface area of the package; everything else is Laravel plumbing.
    • The README for the exact facade alias / class namespace and the shape of the returned array (associative array vs. DTO).
  4. First use case to get working: one-off credit-card/wallet payment for a fixed-price order. Create an Order row with status pending, generate the hash and payload, render the auto-submitting form to the PayHere checkout, then flip the order to paid only when the notify webhook arrives with a valid hash and status_code == 2.

Implementation Patterns

Persist the order before redirecting. Never build the checkout payload from request input alone — write the order (with a server-generated order_id, amount, currency) first, then generate the hash from the stored row. This gives you a single source of truth to compare against when the notify arrives.

Payload generation in a dedicated action. Keep a CreatePayHereCheckout class (or a service method) that takes an Order and returns the array the package expects. That isolates the amount formatting and hash logic from your controllers:

$payload = PayHere::checkout([
    'order_id'   => $order->reference,          // unique, you generate it
    'items'      => $order->description,
    'amount'     => number_format($order->total, 2, '.', ''),
    'currency'   => 'LKR',
    'first_name' => $order->first_name,
    'last_name'  => $order->last_name,
    'email'      => $order->email,
    'phone'      => $order->phone,
    'address'    => $order->address,
    'city'       => $order->city,
    'country'    => 'Sri Lanka',
]);
return view('payhere.redirect', compact('payload'));

Auto-submit form view. PayHere's hosted checkout expects a POST to the checkout URL, so you need a Blade view that renders hidden inputs and calls form.submit() on load. A spinner + "Redirecting to secure payment…" line is worth adding for slow mobile networks.

Notify handler as the source of truth. Treat the return URL as cosmetic only. The notify endpoint is the authoritative signal:

public function notify(Request $request)
{
    Log::channel('payhere')->info('notify', $request->all());

    abort_unless(PayHere::verifyNotify($request->all()), 403, 'Bad hash');

    $order = Order::where('reference', $request->input('order_id'))->firstOrFail();
    abort_unless($order->total == $request->input('payhere_amount'), 422, 'Amount mismatch');

    if ($order->status === 'paid') {
        return response('OK', 200); // idempotent replay
    }

    match ((int) $request->input('status_code')) {
        2  => $order->markAsPaid($request->input('payhere_id')),
        0  => $order->markAsPending(),
        -1 => $order->markAsCancelled(),
        -2 => $order->markAsFailed(),
        -3 => $order->markAsChargedback(),
    };

    return response('OK', 200);
}

Routing. PayHere posts form-encoded data with no CSRF token, so the notify route must live outside web middleware or be added to VerifyCsrfToken::$except (Laravel ≤10). Route it as POST and return literally OK with a 200 — anything else and PayHere retries.

Status flow. Model your order.status as a small enum: pending → paid | failed | cancelled | chargedback. Never show "payment successful" from the return URL alone; the return page should poll or simply say "we're confirming your payment" and link to the order.

Testing workflow. Sandbox credentials are separate from live ones. PayHere's sandbox test card is 4916217501611292, any future expiry, CVV 111, and the OTP step accepts 123456 in sandbox. Use php artisan serve with a tunnel (ngrok/Expose) so PayHere can reach your notify URL — localhost will silently never fire.

Integration tips.

  • Fire your own domain event (OrderPaid) from the notify handler and hang fulfilment, email, and stock decrement off that — keeps the webhook controller thin.
  • If the package exposes only checkout + hash verification, wrap it in your own PayHereGateway interface so you can swap to the on-site/JS SDK later without touching callers.
  • Recurring payments and refunds (PayHere's payhere.lk/pay/refund / subscription APIs) are usually not covered by small packages like this — plan to write raw Guzzle calls with the merchant token if you need them.

Gotchas and Tips

  • The hash is a double MD5 and it's easy to get wrong. The canonical pattern is strtoupper(md5($merchant_id . $order_id . $amount . $currency . strtoupper(md5($merchant_secret)))). If the package builds this for you, still verify it against PayHere's docs once with a manually computed value — a silently wrong hash produces a generic "Invalid request" page with no clue.
  • Amount formatting matters. Always two decimals, . separator, no thousands separator, no LKR prefix. 1000.5 will fail where 1000.50 works. Store money as integer cents and format at the boundary.
  • Uppercase the hash. PayHere rejects lowercase hashes; the notify hash check must also compare case-insensitively or uppercase both sides.
  • Notify before return is not guaranteed. Users can close the tab after paying. Never mark paid from the return handler — and never mark failed from it either. The notify will arrive regardless.
  • Idempotency. PayHere retries notifies until it gets 200 OK. Guard with a payhere_id column and a unique index; a duplicate notify should be a no-op, not a second receipt email.
  • Log the raw payload before parsing. payhere_amount, payhere_currency, status_code (not status), and payhere_id are the field names you need to verify against — dump them into a dedicated channel so you can reconcile disputes later.
  • Sandbox flag is the #1 config quirk. Confirm the generated checkout URL host. Many reports of "test payments work, live fails" are just a cached config or an env var named slightly differently from what the config file reads.
  • HTTPS is mandatory for both notify and return URLs in the live dashboard. A http:// notify silently never fires — which looks exactly like a package bug.
  • merchant_id should stay a string. It's numeric but leading zeros and env coercion can bite; validate it's non-empty and log it (not the secret) on boot in local.
  • Where to extend: publish the config, then subclass or decorate the package's main class if you need multi-currency, per-tenant credentials, or a second merchant account. If the package hardcodes the hash logic in a static method, copy that method into your own helper rather than forking — it's ~10 lines and you'll own it forever otherwise.
  • Debugging checklist for a failed payment: (1) is the checkout URL pointing at sandbox/live correctly, (2) does the hash match a value you compute by hand, (3) is the amount a fixed 2-decimal string, (4) is the merchant domain whitelisted in the PayHere dashboard, (5) is the notify route reachable over HTTPS from the internet. Those five cover almost every failure.
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.
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
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin