hadimazalan / ibayaq-laravel
Laravel SDK and webhook integration for the iBayaq Payment Gateway (cart_v3 API).
v1.0.0
2026-06-08 16:42 UTC
Requires
- php: ^8.2
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- nesbot/carbon: ^2.72|^3.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
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/passportline_items[]— each withproduct_code,vote_code,description,amount(sen),reference- Line item total must exactly match
amount - Optional
module_type(defaults toFR) appended tono_rujukan
Handling Callbacks
iBayaq sends results via two paths:
- Browser redirect —
GET/POST /redirect/ibayaq - Server webhook —
POST /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()returnsUnknown - 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