Search by

levilabs / laravel-asiapay

nizaamomer

Modern Laravel SDK for the AsiaPay Payment Gateway (Iraq, asiapay.iq): JWT-signed orders, hosted checkout, status polling, refunds, and verified webhooks.

dev-main / 1.0.x-dev 2026-11-05 12:42 UTC

This package is not auto-updated.

Last update: 2026-11-06 07:35:38 UTC


README

Latest Version on Packagist Tests Total Downloads PHP Version Laravel Version License

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

  • 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 — set ASIAPAY_BASE_URL explicitly 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

  • PaymentCreated
  • PaymentStatusChecked
  • PaymentRefunded
  • WebhookReceived

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 queryOrder before 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:

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.