On this page
Pay as You Wish in Cambodia. A Laravel package for the ABA PayWay payment gateway. It covers every API in the PayWay developer docs:
- Checkout: cards, ABA Pay, KHQR, WeChat, Alipay and Google Pay
- Transactions: status, details, lists, closing a transaction, exchange rates
- Money back: refunds, plus completing and cancelling pre-authorisations
- ABA QR API: KHQR codes for your own UI
- Payment links
- Payouts: and the beneficiary whitelist
- Credentials on file: saving ABA accounts and cards, charging a saved token, and subscriptions
This is a community package. It isn't made by or affiliated with ABA Bank.
Installation
composer require crushjs/aba-paywayLaravel finds the service provider and the PayWay facade on its own. To publish the config file:
php artisan vendor:publish --tag=payway-configAdd the credentials ABA sent you to .env. Sandbox and production use different keys:
PAYWAY_MERCHANT_ID=ec000002
PAYWAY_API_KEY=your-public-key
PAYWAY_SANDBOX=true
# Only needed for refunds, pre-auth, payment links and payouts.
# Accepts the PEM text (escaped \n is fine), its base64 body, or a path to a .pem file.
PAYWAY_RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIGfMA0...\n-----END PUBLIC KEY-----"PayWay only accepts API calls from domains and IPs it has whitelisted. Ask the PayWay integration team to add yours, or every call fails with 6: wrong domain.
Responses
Every API call returns a Crushjs\AbaPayway\Response:
$response = PayWay::checkTransaction('INV-1001');
$response->successful(); // PayWay status code "00" or "0"
$response->message(); // PayWay's status message
$response->paymentStatus(); // APPROVED | PENDING | DECLINED | REFUNDED | CANCELLED ...
$response->isPaid(); // successful() && APPROVED
$response->get('data.total_amount'); // read a value with dot notation
$response->toArray();The package sets req_time/request_time and merchant_id, computes the hash, and does the base64 and RSA encoding each endpoint requires. You pass plain values: arrays for items, custom_fields, payout and return_deeplink, and plain URLs for return_url and callback_url. A failed HTTP request throws Crushjs\AbaPayway\Exceptions\PayWayException.
Checkout
1. Build the signed form (controller)
use Crushjs\AbaPayway\Laravel\Facades\PayWay;
public function checkout(Order $order)
{
$form = PayWay::checkoutForm([
'tran_id' => $order->reference, // unique, max 20 chars
'amount' => $order->total, // 12.5 becomes "12.50" (KHR: no decimals)
'currency' => 'USD', // USD | KHR
'firstname' => $order->first_name,
'lastname' => $order->last_name,
'email' => $order->email,
'phone' => $order->phone,
'payment_option' => 'abapay_khqr', // cards | abapay_khqr | abapay_khqr_deeplink | wechat | alipay | google_pay
'items' => [['name' => 'Coffee', 'quantity' => 2, 'price' => 6.25]],
'return_url' => route('payway.callback'),
'cancel_url' => route('orders.show', $order),
'continue_success_url' => route('orders.thanks', $order),
// Optional: type ('pre-auth'), shipping, lifetime (minutes), custom_fields, return_params,
// payout, return_deeplink, skip_success_page, view_type, payment_gate, additional_params
]);
return view('checkout', compact('form'));
}PayWay::purchaseFields([...]) returns the signed fields as an array if you'd rather build the form yourself. PayWay::checkoutUrl() returns the URL the form posts to.
2. Open the PayWay sheet (Blade)
{!! $form !!}
<button id="checkout_button">Pay with ABA PayWay</button>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://checkout.payway.com.kh/plugins/checkout2-0.js"></script>
<script>
$('#checkout_button').on('click', () => AbaPayway.checkout());
</script>To show the QR or deeplink in your own UI instead, call PayWay from the server. This works with abapay_khqr_deeplink:
$response = PayWay::purchase([...same params..., 'payment_option' => 'abapay_khqr_deeplink']);
$response->get('qr_string');
$response->get('abapay_deeplink');
$response->get('checkout_qr_url');3. Confirm the payment (callback)
PayWay POSTs to your return_url once the payment finishes. The posted body can't be verified, so ask PayWay for the real status:
Route::post('/payway/callback', function (Request $request) {
$order = Order::where('reference', $request->input('tran_id'))->firstOrFail();
if (PayWay::isPaid($order->reference)) {
$order->markAsPaid();
}
return response()->json(['ok' => true]);
})->name('payway.callback');PayWay doesn't send a CSRF token, so exclude this route from CSRF checks in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['payway/callback']);
})Transactions
PayWay::checkTransaction('INV-1001'); // payment status
PayWay::isPaid('INV-1001'); // bool
PayWay::transactionDetail('INV-1001'); // full details and operation history
PayWay::closeTransaction('INV-1001'); // stop an unpaid transaction from being paid
PayWay::exchangeRate();
PayWay::transactionList([
'from_date' => now()->subDays(2), // DateTimeInterface or 'Y-m-d H:i:s'; max range 3 days
'to_date' => now(),
'status' => ['APPROVED', 'REFUNDED'],
'from_amount' => 1, 'to_amount' => 100,
'page' => 1, 'pagination' => 40, // max 1000 per page
]);
PayWay::transactionsByMerchantRef('ref00001'); // not available on the sandboxRefunds and pre-auth
These calls need PAYWAY_RSA_PUBLIC_KEY.
PayWay::refund('INV-1001', 5.00); // full or partial refund
// Start a pre-auth with checkoutForm([... 'type' => 'pre-auth']), then:
PayWay::completePreAuth('INV-1002', 20.00);
PayWay::completePreAuth('INV-1002', 20.00, payout: [['acc' => '000133879', 'amt' => 20]]);
PayWay::cancelPreAuth('INV-1002');ABA QR API
$qr = PayWay::generateQr([
'tran_id' => 'INV-1003',
'amount' => 5,
'currency' => 'USD',
'payment_option' => 'abapay_khqr', // abapay_khqr | wechat | alipay
'callback_url' => route('payway.callback'),
'lifetime' => 6, // minutes (3 minutes to 120 days)
'qr_image_template' => 'template3_color',
]);
$qr->get('qrString'); // raw KHQR string
$qr->get('qrImage'); // data:image/png;base64,...Payment links
These calls need PAYWAY_RSA_PUBLIC_KEY.
$link = PayWay::createPaymentLink([
'title' => 'Gym membership',
'amount' => 25,
'currency' => 'USD',
'return_url' => route('payway.callback'),
'description' => 'October',
'payment_limit' => 1,
'expired_date' => now()->addWeek(), // or null for no expiry
'merchant_ref_no' => 'GYM-OCT-42',
], imagePath: storage_path('app/gym.png')); // image is optional
PayWay::paymentLinkDetail($link->get('data.id'));Payouts
These calls need PAYWAY_RSA_PUBLIC_KEY.
PayWay::addBeneficiary('318111358120004'); // ABA account or MID
PayWay::updateBeneficiaryStatus('318111358120004', false); // disable
PayWay::payout('PO-0001', [
['account' => '200030000', 'amount' => 1.72],
['account' => '012538302', 'amount' => 1.72],
], currency: 'USD', customFields: ['invoice' => 'INV-9']); // amount defaults to the sumCredentials on file
PayWay has to enable the token flags you need on your merchant profile first. Until it does, these calls answer 104: Merchant not enabled token flag.
// Link an ABA account: returns a deeplink and a QR string.
PayWay::linkAccount([
'ctid' => 'customer00042', // your customer id, 5-24 letters/numbers
'request_id' => 'link00001', // 5-24 letters/numbers, unique
'token_flag' => 'CITI_FLEX', // CITI_FLEX | CITO_FLEX
'currency' => 'USD',
'callback_url' => route('payway.token'),
'return_deeplink' => ['ios_scheme' => 'myapp://linked', 'android_scheme' => 'myapp://linked'],
]);
// Link a card: PayWay hosts the card form.
$url = PayWay::linkCard([...]); // URL of the hosted form, for an iframe or redirect
$form = PayWay::linkCardForm([...]); // or a form that posts into <iframe name="aba_link_card">
// Charge a saved token (pwt) that PayWay sent to your callback_url.
PayWay::payWithToken([
'tran_id' => 'INV-2001', 'amount' => 3.5, 'currency' => 'USD',
'ctid' => 'customer00042', 'pwt' => $token,
'token_flag' => 'CITU_FLEX', // CITU_FLEX | MITU_FLEX | MITR_FIX
]);
PayWay::tokenDetails('link00001');
PayWay::renewToken('customer00042', $token, 'renew00001');
PayWay::removeToken('customer00042', $token);Subscriptions (scheduled payments) start with a normal checkout that carries the token fields:
PayWay::checkoutForm([
'tran_id' => 'SUB-0001', 'amount' => 20, 'payment_option' => 'cards', // cards | abapay | abapay_deeplink
'ctid' => 'customer00042', 'token_flag' => 'CITR_FIX', 'frequency' => '1M', // 1W | 1M | 2M
]);
// After that, charge each billing cycle with payWithToken([... 'token_flag' => 'MITR_FIX']).Without Laravel
$payway = new Crushjs\AbaPayway\PayWay(
merchantId: 'ec000002',
apiKey: 'your-public-key',
sandbox: true,
rsaPublicKey: file_get_contents('payway_rsa_public.pem'),
);Testing
composer testLicense
MIT