hadimazalan/ibayaq-laravel

Laravel SDK and webhook integration for the iBayaq Payment Gateway (cart_v3 API).

Maintainers

Package info

github.com/hadimazalan/ibayaq-laravel

pkg:composer/hadimazalan/ibayaq-laravel

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-06-08 16:42 UTC

This package is auto-updated.

Last update: 2026-06-08 16:44:04 UTC


README

Laravel package for integrating with the iBayaq state payment gateway via the cart_v3 API.

Requirements

  • PHP 8.2+
  • Laravel 10, 11, or 12

Installation

composer require hadimazalan/ibayaq-laravel

Publish configuration (optional):

php artisan vendor:publish --tag=ibayaq-config

Configuration

Add the following to your .env:

IBAYAQ_USERNAME=your_api_username
IBAYAQ_PASSWORD=your_api_password
IBAYAQ_URL=https://ibayaq.kedah.gov.my
# IBAYAQ_CART_URL=https://ibayaq.kedah.gov.my/api/cart_v3
# IBAYAQ_CALLBACK_URL=https://your-app.test/redirect/ibayaq
IBAYAQ_TIMEOUT=30
IBAYAQ_TIMEZONE=Asia/Kuala_Lumpur
Variable Description
IBAYAQ_USERNAME iBayaq API username (Basic Auth)
IBAYAQ_PASSWORD iBayaq API password (Basic Auth)
IBAYAQ_URL Base gateway URL
IBAYAQ_CART_URL Optional full override for the cart endpoint
IBAYAQ_CALLBACK_URL Optional callback base URL (append attempt_id when building)

Initiating a Payment

use Hadimazalan\Ibayaq\Data\AddressData;
use Hadimazalan\Ibayaq\Data\InitiatePaymentData;
use Hadimazalan\Ibayaq\Data\LineItemData;
use Hadimazalan\Ibayaq\Data\PayerData;
use Hadimazalan\Ibayaq\Facades\Ibayaq;
use Hadimazalan\Ibayaq\Support\CallbackUrlBuilder;

$callbackUrl = app(CallbackUrlBuilder::class)->build(
    configuredCallback: config('ibayaq.callback_url'),
    defaultBaseUrl: url('/redirect/ibayaq'),
    attemptId: $attemptId,
);

$result = Ibayaq::initiatePayment(new InitiatePaymentData(
    amount: 10025, // sen (RM 100.25)
    callbackUrl: $callbackUrl,
    payer: new PayerData(
        idNo: '900101025555',
        name: 'Ahmad Bin Ali',
        address: new AddressData(
            line1: 'No 1 Jalan Test',
            postcode: '05000',
            state: 'Kedah',
        ),
    ),
    lineItems: [
        new LineItemData(
            productCode: 'H0171257',
            voteCode: 'G001',
            description: 'License Fee',
            amount: 10025,
            reference: 'ITEM-001',
        ),
    ],
    reference: 'APP-001',
    email: 'payer@example.test',
    phoneNo: '60123456789',
    moduleType: 'FR',
));

return redirect()->away($result->redirectUrl);

Required payment metadata

iBayaq cart_v3 requires Kedah state integration fields:

  • payer.id_no — payer NRIC/passport
  • line_items[] — each with product_code, vote_code, description, amount (sen), reference
  • Line item total must exactly match amount
  • Optional module_type (defaults to FR) appended to no_rujukan

Handling Callbacks

iBayaq sends results via two paths:

  1. Browser redirectGET/POST /redirect/ibayaq
  2. Server webhookPOST /webhook/ibayaq

Callbacks support two payload formats:

  • Status codes: status_code / status (00 = success, 01 = failed, PENDING)
  • Receipt (iHasil): no_resit, tarikh_bayar, jumlah_amaun, cara_bayar

Option A: Extend the abstract controller

use Hadimazalan\Ibayaq\Data\NormalizationContext;
use Hadimazalan\Ibayaq\Data\NormalizedPayment;
use Hadimazalan\Ibayaq\Http\Controllers\AbstractIbayaqCallbackController;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class IbayaqCallbackController extends AbstractIbayaqCallbackController
{
    protected function resolveAttemptId(Request $request): ?string
    {
        return $request->query('attempt_id') ?? $request->input('attempt_id')
            ?? $this->attemptRepository->findByCartReferences(
                $request->input('no_akaun'),
                $request->input('no_rujukan'),
            );
    }

    protected function attemptExists(string $attemptId): bool
    {
        return $this->attemptRepository->exists($attemptId);
    }

    protected function isAttemptAlreadyProcessed(string $attemptId): bool
    {
        return $this->attemptRepository->isProcessed($attemptId);
    }

    protected function normalizationContext(string $attemptId, ?Request $request = null): NormalizationContext
    {
        $order = $this->orderRepository->findByAttemptId($attemptId);

        return new NormalizationContext(
            attemptId: $attemptId,
            amount: $order->amount,
            currency: $order->currency,
            reference: $order->reference,
            processingTimestamp: now(),
        );
    }

    protected function onPaymentSuccess(NormalizedPayment $payment, string $attemptId): void
    {
        // Mark order paid, dispatch events, etc.
    }

    protected function onPaymentFailed(NormalizedPayment $payment, string $attemptId): void
    {
        // Handle failure
    }

    protected function onPaymentPending(NormalizedPayment $payment, string $attemptId): void
    {
        // Handle pending
    }

    protected function redirectAfterPayment(NormalizedPayment $payment, string $attemptId, Request $request): RedirectResponse
    {
        return redirect()->route('payment.thank-you', [
            'status' => $payment->status->value,
            'reference' => $payment->reference,
        ]);
    }
}

Register routes in config/ibayaq.php:

'routes' => [
    'enabled' => true,
    'controller' => \App\Http\Controllers\IbayaqCallbackController::class,
],

Or publish routes and wire them manually:

php artisan vendor:publish --tag=ibayaq-routes

Option B: Manual callback parsing

use Hadimazalan\Ibayaq\CallbackNormalizer;
use Hadimazalan\Ibayaq\Data\CallbackPayload;
use Hadimazalan\Ibayaq\Data\NormalizationContext;

$payment = app(CallbackNormalizer::class)->normalize(
    CallbackPayload::fromArray($request->all()),
    new NormalizationContext(
        attemptId: $attemptId,
        amount: 10025,
        reference: 'APP-001',
        processingTimestamp: now(),
    ),
);

CSRF exemption

iBayaq POSTs to your callback URLs without CSRF tokens. Exempt the routes in your app:

// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    'redirect/ibayaq',
    'webhook/ibayaq',
];

Known Limitations

  • No requery — iBayaq does not expose a status inquiry endpoint; Ibayaq::requery() returns Unknown
  • No refund API — not supported by this SDK
  • No inbound signature verification — callbacks are trusted via attempt/reference resolution in your application

Testing

composer test

License

MIT