laravel-asiapay maintained by levilabs
Laravel AsiaPay SDK — Payment Gateway & Webhooks
A modern Laravel SDK for the AsiaPay Payment Gateway (Iraq) — hosted checkout, JWT-signed merchant API calls, server notify webhooks, order status polling, and refunds. Typed DTOs and enums, cached access tokens, optional database persistence, and an Artisan sync command when notifications are missed.
Maintained by Levi Labs (GitHub) — built by Nizam Omer
AsiaPay: asiapay.iq · Integration: asiapay.iq/integration
Table of Contents
- Requirements
- Installation
- Configuration
- Creating a payment
- The webhook notification
- Checking payment status
- Missed notifications: sync command
- Refunding a payment
- Multiple merchants
- Automatic persistence
- Events
- Error handling
- Security
- Testing
- AsiaPay API reference
- Changelog
- Contributing
- Author
- License
Requirements
- PHP 8.2+ (Laravel 13.x requires PHP 8.3+ on the application)
- Laravel 11.x, 12.x, or 13.x
- AsiaPay merchant credentials (App Key, App Secret, Private Key, App ID, Merchant Code, Domain URL)
Installation
composer require levilabs/laravel-asiapay
php artisan vendor:publish --tag=asiapay-config
php artisan migrate
Publish step copies config/asiapay.php. Migrations create asiapay_payments and asiapay_refunds; lifecycle events upsert rows automatically when listeners are registered (default).
Configuration
ASIAPAY_MERCHANT=default
ASIAPAY_ENVIRONMENT=sandbox
ASIAPAY_BASE_URL=https://apitest.asiapay.iq:5443/apiaccess
ASIAPAY_APP_KEY=
ASIAPAY_APP_SECRET=
ASIAPAY_PRIVATE_KEY=
ASIAPAY_APP_ID=
ASIAPAY_MERCH_CODE=
ASIAPAY_REDIRECT_URL=https://your-app.test/payments/asiapay/return
ASIAPAY_NOTIFY_URL=https://your-app.test/payments/asiapay/webhook
ASIAPAY_WEBHOOK_ACK=SUCCESS
ASIAPAY_CURRENCY=IQD
ASIAPAY_TIMEOUT_EXPRESS=30m
Production Domain URL: the public integration page documents sandbox (
apitest.asiapay.iq). Production Domain URL is issued with your merchant account — setASIAPAY_BASE_URLexplicitly before going live.
redirect_url and notify_url on each order fall back to ASIAPAY_REDIRECT_URL and ASIAPAY_NOTIFY_URL when omitted on PreOrderRequest.
Creating a payment
Obtains a cached access token, signs a payment.preorder request, and returns a redirect URL for AsiaPay’s hosted paygate — send the customer there to pay.
use LeviLabs\LaravelAsiapay\Data\PreOrderRequest;
use LeviLabs\LaravelAsiapay\Enums\TradeType;
use LeviLabs\LaravelAsiapay\Facades\AsiapayPayment;
$payment = AsiapayPayment::createOrder(new PreOrderRequest(
merchOrderId: 'ORDER-'.$order->id.'-'.time(),
totalAmount: (float) $order->total,
title: 'Order #'.$order->id,
tradeType: TradeType::Checkout, // Checkout | CrossApp | Pwa
));
return redirect($payment->redirectUrl);
merch_order_id must be unique per merchant. Amounts are sent as IQD strings with two decimal places (trans_currency defaults to IQD).
The webhook notification
AsiaPay POSTs JSON to your notify_url when a payment updates. Verify the JWT sign field before trusting the body:
use LeviLabs\LaravelAsiapay\Facades\AsiapayPayment;
public function webhook(Request $request)
{
if (! AsiapayPayment::verifyWebhook($request->all())) {
return response()->json(['message' => 'Invalid signature'], 400);
}
$notification = AsiapayPayment::parseWebhook($request->all());
if ($notification->isSuccessful()) {
// trade_status Completed — still confirm amount & merch_order_id against your order
}
return response(AsiapayPayment::webhookAckBody(), 200);
}
Confirm the exact acknowledgment body with devops@asiapay.iq if SUCCESS is rejected in your environment (ASIAPAY_WEBHOOK_ACK).
Checking payment status
Authoritative outcome for fulfillment — use from the webhook handler, the return URL after redirect_url, or anywhere you need confirmation:
$status = AsiapayPayment::queryOrder('ORDER-123');
$status->orderStatus; // OrderStatus::PaySuccess | PayFail | …
$status->isSuccessful(); // PAY_SUCCESS
$status->transId;
$status->paymentOrderId;
Missed notifications: sync command
php artisan asiapay:sync-statuses
Re-checks rows in asiapay_payments that are not in a terminal order_status via payment.queryorder. Schedule in bootstrap/app.php if you rely on webhooks:
->withSchedule(function (Illuminate\Console\Scheduling\Schedule $schedule) {
$schedule->command('asiapay:sync-statuses')->everyFiveMinutes();
})
Refunding a payment
AsiapayPayment::refund(
merchOrderId: 'ORDER-123',
refundRequestNo: 'REF-'.time(),
refundReason: 'Customer request',
);
refund_request_no must be unique per refund attempt.
Multiple merchants
Add entries under merchants in config/asiapay.php, then pass the merchant key as the last argument:
AsiapayPayment::createOrder($request, merchant: 'second_store');
AsiapayPayment::queryOrder('ORDER-123', merchant: 'second_store');
Automatic persistence
| Table | When rows update |
|---|---|
asiapay_payments |
PaymentCreated, PaymentStatusChecked, WebhookReceived |
asiapay_refunds |
PaymentRefunded |
Optional payable morph on AsiapayPayment for linking to your Order model.
Events
PaymentCreatedPaymentStatusCheckedPaymentRefundedWebhookReceived
Error handling
use LeviLabs\LaravelAsiapay\Exceptions\AsiapayException;
use LeviLabs\LaravelAsiapay\Exceptions\AsiapayMerchantException;
try {
AsiapayPayment::createOrder($request);
} catch (AsiapayException $e) {
// API failure (result/code/msg from AsiaPay)
} catch (AsiapayMerchantException $e) {
// Unknown merchant or missing credentials in config
}
Security
- Credentials belong in
.env, never in version control (ASIAPAY_APP_SECRET,ASIAPAY_PRIVATE_KEY). - Verify webhook signatures before updating order state; match
merch_order_id, amount, and currency. - Use
queryOrderbefore fulfilling high-value orders — do not rely on redirect URLs alone. - TLS verification is never disabled in this SDK.
See SECURITY.md for vulnerability reporting.
Testing
composer test # Pest
composer analyse # PHPStan level 8
composer format # Laravel Pint
AsiaPay API reference
Official merchant integration guide:
- AsiaPay
- Integration documentation
- B2B onboarding:
b2bchannel@asiapay.iq - Technical support:
devops@asiapay.iq
SDK mapping
| SDK | AsiaPay method |
HTTP path (under Domain URL) |
|---|---|---|
| (token) | — | POST /payment/gateway/payment/v1/token |
AsiapayPayment::createOrder() |
payment.preorder |
POST .../merchant/preOrder |
AsiapayPayment::queryOrder() |
payment.queryorder |
POST .../merchant/queryOrder |
AsiapayPayment::refund() |
payment.refund |
POST .../merchant/refund |
| Environment | Base URL (example) |
|---|---|
| Sandbox | https://apitest.asiapay.iq:5443/apiaccess |
| Production | Issued per merchant (set ASIAPAY_BASE_URL) |
| Response field | Example |
|---|---|
Query order_status |
PAY_SUCCESS |
Webhook trade_status |
Completed |
Refund refund_status |
REFUND_SUCCESS |
Changelog
See CHANGELOG.md.
Contributing
Bug reports and feature ideas are welcome via GitHub Issues. See CONTRIBUTING.md.
Author
Levi Labs — GitHub · hello@levilabs.dev
Created and maintained by Nizam Omer — nizaamomer.com · nizam@nizaamomer.com
License
MIT — see LICENSE.md.