jiannius/senangpay

SenangPay SDK Wrapper

Maintainers

Package info

github.com/jiannius/senangpay

pkg:composer/jiannius/senangpay

Statistics

Installs: 626

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0 2026-05-13 07:23 UTC

This package is auto-updated.

Last update: 2026-05-13 07:29:13 UTC


README

A Laravel SDK wrapper for SenangPay, the Malaysian payment gateway. It signs the outbound hash, validates inbound callbacks (regular and recurring), and wires the redirect / callback / webhook routes so you only have to write the controller actions.

Requirements

  • PHP 8.3+
  • Laravel 13

Installation

composer require jiannius/senangpay

The service provider is auto-discovered. Nothing to publish.

Configuration

Add a senangpay entry to config/services.php:

'senangpay' => [
    'merchant_id' => env('SENANGPAY_MERCHANT_ID'),
    'secret_key'  => env('SENANGPAY_SECRET_KEY'),
    'sandbox'     => env('SENANGPAY_SANDBOX', false),
],

In the SenangPay dashboard (Menu → Settings → Profile → Shopping Cart Integration Link), set Hash Type to SHA256. MD5 is not supported by this package and will silently mismatch every signature.

Routes & the host controller

The service provider registers three routes under the /__senangpay prefix and dispatches them to App\Http\Controllers\SenangpayController in your app. You must create that controller.

Method Path Action Purpose
GET /__senangpay/redirect redirect Return URL — browser lands here after payment
GET /__senangpay/recurring recurring Recurring-payment callback (new/remove/terminate)
POST /__senangpay/webhook webhook Server-to-server callback

Routes are registered with ->withoutMiddleware('web') so the webhook is not blocked by CSRF.

namespace App\Http\Controllers;

class SenangpayController extends Controller
{
    public function redirect()
    {
        $sp = app('senangpay');

        if (! $sp->validatePayload()) {
            abort(403);
        }

        return match ($sp->getStatus()) {
            'success' => view('checkout.success'),
            'failed'  => view('checkout.failed'),
            default   => view('checkout.pending'),
        };
    }

    public function webhook()
    {
        $sp = app('senangpay');

        if (! $sp->validatePayload()) {
            abort(403);
        }

        // Update your order here. MUST be idempotent — SenangPay calls this
        // webhook multiple times for the same transaction (see "Host-app
        // contracts" below).

        return 'OK'; // Literal string "OK". No HTML, no JSON, no redirect.
    }

    public function recurring()
    {
        $sp = app('senangpay');

        if (! $sp->validateRecurringPayload()) {
            abort(403);
        }

        $action = request()->input('action');
        // 'new_schedule' | 'remove_schedule' | 'terminate'
    }
}

Usage

Redirect to checkout

checkout() returns a Laravel RedirectResponse to SenangPay's hosted payment page. Return it directly from your controller.

return app('senangpay')->checkout([
    'order_id' => $order->id,
    'name'     => $order->customer_name,
    'email'    => $order->customer_email,
    'phone'    => $order->customer_phone,
    'detail'   => 'Order #' . $order->id,
    'amount'   => $order->total, // numeric, e.g. 49.90
]);

amount is normalised to a two-decimal string ("49.90") before hashing — pass it as a plain number, not a pre-formatted string.

Query an order's status (server-to-server)

$status = app('senangpay')->queryOrderStatus($orderId);

Returns the first record from SenangPay's query_order_status response.

Normalised payment status

When called inside the redirect or webhook action, getStatus() collapses SenangPay's mixed numeric/string status values into one of success, failed, pending, or null.

$result = app('senangpay')->getStatus(); // 'success' | 'failed' | 'pending' | null

Per-tenant credentials at runtime

Setters override the config('services.senangpay.*') defaults for the current request.

$sp = app('senangpay')
    ->setMerchantId($tenant->senangpay_merchant_id)
    ->setSecretKey($tenant->senangpay_secret_key)
    ->setSandbox($tenant->senangpay_sandbox);

Test the connection

['success' => $ok, 'error' => $err] = app('senangpay')->test();

Hits query_order_status with a dummy order id — useful for a settings-page "Test credentials" button.

Host-app contracts

These aren't enforced by the package. Get them wrong and integrations silently break.

  1. The webhook action must respond with the literal string OK. No HTML tags, no JSON, no redirect. If SenangPay doesn't see OK in the response body, the callback is treated as failed and the merchant gets an email.
  2. The webhook action must be idempotent on (order_id, status_id). SenangPay retries on a schedule: first call ~5 minutes after the transaction starts (if pending), real-time on any status change, and a final call ~1 hour after the transaction starts. A successful payment will hit your webhook multiple times.
  3. The dashboard hash type must be SHA256. This package does not support MD5.

Reference

SenangPay developer documentation:

License

MIT — see LICENSE.md.