Engineering Standard 10/10 • Sobin Digital

Cambodia's Modern Payment Gateway Engine

Type-safe, timing-safe TypeScript monorepo combining ABA PayWay popup/direct checkout and National Bank of Cambodia’s Bakong KHQR EMVCo generator.

npm i @sobindigital/pay
Developer GuideEnterprise Tier-1

SobinPay Documentation

SobinPay is an enterprise-grade TypeScript payment gateway SDK for Cambodia engineered by Sobin Digital. It delivers a unified, zero-dependency, type-safe interface for ABA PayWay and National Bank of Cambodia’s Bakong KHQR.

Zero Dependencies

No bloated HTTP or crypto libraries. Relies strictly on native Node.js 18+ runtime APIs.

Timing-Safe Security

Constant-time HMAC-SHA512 verification and 2048-bit RSA encryption to prevent side-channel timing attacks.

Dual ESM + CJS

Pre-compiled with dual modern ESM and CommonJS bundles with complete TypeScript .d.ts declarations.

Installation

Install the main unified facade package @sobindigital/pay using your preferred package manager:

npm install @sobindigital/pay

Alternatively, if you only need a specific provider, you can install individual scoped packages:

  • @sobindigital/provider-payway — Dedicated ABA PayWay client
  • @sobindigital/provider-khqr — Dedicated Bakong KHQR generator & parser
  • @sobindigital/core — Shared types, money math, and crypto primitives
Install AI Agent Skills (Cursor, Claude Code, GitHub Copilot)New

Equip your AI assistant with official SobinPay rules, security constraints, and TypeScript payment patterns:

bash
npx @sobindigital/pay skills add cursor claude copilot
• cursor: .cursor/rules/sobinpay.mdc• claude: .claude/skills/sobinpay.md• copilot: .github/copilot-instructions.md

Architecture & Multi-Provider Design

SobinPay is structured as an enterprise TypeScript monorepo with high separation of concerns:

@sobindigital/pay (Unified Facade)

Unified developer entrypoint providing sobin.aba.*, sobin.abapay.*, and SobinPay.khqr.*.

@sobindigital/provider-payway

Implements ABA PayWay REST & Web checkout APIs, HMAC-SHA512 hashing, status checks, close transaction, exchange rates, and RSA refunds.

@sobindigital/provider-khqr

Implements National Bank of Cambodia EMVCo KHQR specifications, Tag-Length-Value parsing, 16-bit CRC-CCITT calculation, and deep links.

@sobindigital/core (Shared Foundation)

Shared type contracts, error hierarchy (SobinPayError, ProviderApiError), timing-safe crypto, and safe money math.

Core Concepts & Financial Safety

1. Zero Floating-Point Precision Loss

Standard JavaScript floating-point numbers can cause subtle precision errors (e.g. 0.1 + 0.2 === 0.30000000000000004). In financial transaction processing, even a 1-cent discrepancy results in cryptographic hash mismatches and rejected payments.

Automated Decimal Sanitization

SobinPay automatically normalizes amounts according to currency rules: USD amounts are formatted strictly with 2 fixed decimal places (e.g. 12.50), while KHR amounts are rounded to integer units (e.g. 50000).

2. Timing-Safe Cryptographic Comparisons

Comparing webhook signatures with standard string equality (a === b) leaks byte-by-byte comparison timing, allowing remote attackers to deduce valid cryptographic digests through side-channel attacks. SobinPay always uses Node’s crypto.timingSafeEqual for signature validation.

Constant-Time Signature Check
import { timingSafeEqualString } from "@sobindigital/pay";

// Safely compares received signature without timing leakage
const isValid = timingSafeEqualString(receivedSignature, computedSignature);

Live Sandbox & QR Tester

Live Execution

Interactive multi-provider sandbox. Test ABA PayWay popups and render scannable Bakong KHQR codes in real time.

Transaction Parametersdecimal: 2

$
Active Tran ID:sb-100001

Signed PayWay Session

Ready to Test Popup Checkout

Select your payment method (ABA PAY KHQR or Cards), then click "Launch ABA PayWay Popup" to test the native popup modal.

Clean 3-Line Node / TypeScript Call:
import { SobinPay } from "@sobindigital/pay";
const sobin = new SobinPay({ aba: { merchantId, apiKey } });
const purchase = sobin.aba.createPurchase({ amount: 1.5, currency: "USD", tranId: "sb-100001" });
ABA PayWayv2.0 API

ABA PayWay Quickstart

Initialize the SobinPay client by supplying your ABA PayWay credentials. In production, provide your live merchant ID and API key. In development, point to the ABA sandbox gateway.

Initialize SobinPay Client
1
2
3
4
5
6
7
8
9
10
11
12
13
import { SobinPay } from "@sobindigital/pay"; const sobin = new SobinPay({ aba: { merchantId: process.env.ABA_MERCHANT_ID || "your_merchant_id", apiKey: process.env.ABA_API_KEY || "your_aba_api_key", apiUrl: process.env.ABA_API_URL || "https://checkout-sandbox.payway.com.kh", timeoutMs: 15000, // Optional request timeout (default: 15s) }, }); // Access the PayWay client via any of these equivalent aliases: // sobin.aba, sobin.abapay, or sobin.payway

Web Checkout (Popup & Hosted)

Generate an authenticated checkout session for web browsers using sobin.aba.createPurchase(...). This method synchronously calculates the HMAC-SHA512 signature, packages the items and customer info into Base64 format, and generates a ready-to-submit HTML form.

Create Purchase Session
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { AbaPaymentOption, AbaViewType } from "@sobindigital/pay"; const purchase = sobin.aba.createPurchase({ amount: 25.0, currency: "USD", returnUrl: "https://example.com/checkout/success", cancelUrl: "https://example.com/checkout/cancel", paymentOption: AbaPaymentOption.ABAPAY_KHQR, // or "cards", "alipay", etc. viewType: AbaViewType.POPUP, // or AbaViewType.HOSTED customer: { firstName: "Dara", lastName: "Chan", email: "dara@example.com", phone: "012345678", }, items: [ { name: "Pro Developer License", quantity: 1, price: 25.0 }, ], }); // Returns: // { // actionUrl: "https://checkout-sandbox.payway.com.kh/api/payment-gateway/v1/payments/purchase", // payload: { req_time, merchant_id, tran_id, amount, hash, ... }, // checkoutUrl: "...", // formHtml: "<form method='POST' action='...'>...</form>", // hash: "...", // tranId: "sb-..." // }
Client-Side Popup Integration:

Add jQuery and the ABA PayWay checkout scripts to your HTML header or root layout:<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://checkout.payway.com.kh/plugins/checkout2-0.js"></script>

When the form with target="aba_webservice" is mounted into the DOM, simply call window.AbaPayway.checkout() to display the native ABA modal popup.

Direct KHQR & Deeplinks (createQR)

For mobile applications, automated kiosks, or custom POS screens, you can request a dynamic ABA PayWay KHQR code directly via server-to-server REST API.

Direct Server-to-Server QR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const qr = await sobin.aba.createQR({ amount: 10.0, currency: "USD", returnUrl: "https://example.com/checkout/success", customer: { firstName: "Sok", lastName: "Chan", phone: "012345678", }, }); if (qr.isSuccess) { console.log("Raw QR String:", qr.qrString); console.log("ABA Mobile App Deeplink:", qr.abapayDeeplink); console.log("QR Image URL:", qr.checkoutQrUrl); }

Transaction Status Check (checkTransaction)

Poll or verify the current status of any transaction by invoking sobin.aba.checkTransaction(tranId). This calls ABA PayWay’s latest check-transaction-2 endpoint.

Query Payment Status
1
2
3
4
5
6
7
8
9
10
11
12
import { AbaPaymentStatus } from "@sobindigital/pay"; const status = await sobin.aba.checkTransaction("sb-n80c75123"); if (status.isApproved) { console.log("Payment completed! Status:", status.status); // AbaPaymentStatus.APPROVED console.log("Amount paid:", status.originalAmount, status.originalCurrency); } else if (status.status === AbaPaymentStatus.PENDING) { console.log("Payment is still awaiting customer scan/authorization."); } else { console.log("Payment was not approved:", status.status); }

Transaction Details (getTransactionDetail)

Retrieve complete transaction metadata including bank authorization codes (APV), card brand information, masked card numbers, and raw gateway response dumps.

Retrieve Detailed Audit Log
const details = await sobin.aba.getTransactionDetail("sb-123456");

console.log("Bank Reference:", details.bankRef);
console.log("Payment Status:", details.paymentStatus);
console.log("Gateway Response:", details.rawResponse);

Cancel / Close Transaction (closeTransaction)

Explicitly cancel or void a pending, uncompleted transaction on the ABA PayWay gateway.

Close Pending Transaction
const result = await sobin.aba.closeTransaction("sb-123456");

if (result.isSuccess) {
  console.log("Transaction successfully voided:", result.message);
}

Live Exchange Rates (getExchangeRate)

Query the official real-time USD to KHR currency exchange rate provided directly by ABA Bank.

Query Live USD/KHR Rate
const rate = await sobin.aba.getExchangeRate();

console.log("1 USD =", rate.usdToKhr, "KHR");
Click to query live gateway exchange rates via REST API
Critical SecurityHMAC-SHA512

Webhook Verification (verifyWebhook)

ABA PayWay sends server-to-server pushback notifications upon payment completion. You must verify the cryptographic signature to guard against fraudulent or replayed requests.

Verify Pushback Webhook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Inside your webhook route handler (e.g. Next.js, Express, or NestJS) const payload = req.body; // Incoming JSON pushback payload const signature = req.headers["x-payway-hmac-sha512"] || payload.hash; const verification = sobin.aba.verifyWebhook(payload, signature); if (!verification.isValid) { console.error("Untrusted pushback! Cryptographic signature mismatch."); return res.status(401).send("Invalid Signature"); } if (verification.status === "APPROVED") { // Safe to credit customer balance or mark order as paid await markOrderPaid(verification.tranId); }
EnterpriseRSA-SHA512

Full & Partial Refunds (refundTransaction)

Execute full or partial refunds directly against settled transactions. ABA PayWay requires refund payloads to be encrypted using 2048-bit RSA-SHA512 with the bank's public key.

Execute RSA-Encrypted Refund
1
2
3
4
5
6
7
8
9
10
const refundResult = await sobin.aba.refundTransaction({ tranId: "sb-123456", amount: 10.0, // Amount to refund reason: "Customer order cancellation", rsaPublicKey: process.env.ABA_RSA_PUBLIC_KEY, // Or configured in client init }); if (refundResult.isSuccess) { console.log("Refund processed successfully! Refund ID:", refundResult.refundId); }

Webhook Signature Debugger

HMAC-SHA512Constant-Time

Paste any ABA PayWay pushback webhook payload to inspect the canonical normalization string and verify the signature in constant time.

Quick Presets:

Webhook Request Body (JSON)

Cryptographic Trace

Ready to Verify Webhook

Click a quick preset above or paste your payload to inspect canonical string generation and verify HMAC signatures.

Timing Attack Protection

SobinPay strictly uses Node.js crypto.timingSafeEqual under the hood to ensure execution time does not leak cryptographic digest bytes.

Bakong KHQRIndividual (Tag 29)

Individual EMVCo KHQR Generation

Generate standard National Bank of Cambodia EMVCo QR codes for individual Bakong account IDs (e.g. username@bank or phone number accounts).

Generate Individual KHQR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { SobinPay } from "@sobindigital/pay"; // Generate a Dynamic Individual KHQR (with specified amount & invoice) const qrString = SobinPay.khqr.generateIndividual({ bakongAccountId: "sobin_digital@aclb", // Recipient Bakong Account ID merchantName: "Sobin Digital", // Payee display name (max 25 chars) merchantCity: "Phnom Penh", // Operational city (default: Phnom Penh) amount: 15.50, // Amount (USD or KHR) currency: "USD", // "USD" (code 840) or "KHR" (code 116) billNumber: "INV-2026-001", // Optional invoice number (Tag 62) }); console.log(qrString); // Output: 00020101021229300018sobin_digital@aclb520459995303840540515.505802KH5913Sobin Digital6010Phnom Penh62160112INV-2026-0016304...
Static vs Dynamic QR:

If you omit the amount parameter, SobinPay automatically generates a Static QR (Point of Initiation Tag 01 = "11"), allowing the payer to enter any custom amount in their banking app. When amount is provided, it generates a Dynamic QR (Tag 01 = "12").

Bakong KHQRMerchant (Tag 30)

Merchant EMVCo KHQR Generation

For registered commercial businesses and organizations, generate merchant QR codes with acquiring bank credentials, merchant IDs, and store/terminal identifiers (EMVCo Tag 30 structure).

Generate Merchant KHQR
1
2
3
4
5
6
7
8
9
10
11
12
const merchantQr = SobinPay.khqr.generateMerchant({ bakongAccountId: "sobin_digital@aclb", merchantId: "000000001", // Acquiring Bank Merchant ID acquiringBank: "Bakong Bank", // Acquiring Bank Name merchantName: "Sobin Digital Store", merchantCity: "Phnom Penh", amount: 60000, currency: "KHR", billNumber: "ORD-9912", storeLabel: "Central Branch", terminalLabel: "POS-03", });
ParserCRC-16/CCITT

KHQR Parser & CRC-16 Validation (parse)

Parse any incoming or scanned KHQR string into structured JavaScript objects and automatically verify the 16-bit CRC checksum (polynomial 0x1021, initial value 0xFFFF).

Parse & Validate KHQR
1
2
3
4
5
6
7
8
9
10
11
12
const parsed = SobinPay.khqr.parse(qrString); if (!parsed.isValid) { throw new Error("Invalid KHQR string or CRC16 checksum mismatch!"); } console.log("Account ID:", parsed.bakongAccountId); // e.g. "sobin_digital@aclb" console.log("Payee Name:", parsed.merchantName); // e.g. "Sobin Digital" console.log("Amount:", parsed.amount); // e.g. 15.5 console.log("Currency:", parsed.currency); // "USD" or "KHR" console.log("CRC:", parsed.crc); // e.g. "E5F2" console.log("All EMVCo Tags:", parsed.tags); // { "00": "01", "01": "12", ... }

EMVCo KHQR Tag Inspector & CRC16 Validator

Tag-Length-ValueCRC-16/CCITT

Deconstruct any raw Bakong KHQR EMVCo string down to individual TLV tags and verify the 16-bit CRC checksum.

Sample Codes:
ReferenceStrict TypeScript

TypeScript Enums & Constants

SobinPay ships 100% strict TypeScript types and enums for compile-time safety and IDE autocompletion.

AbaPaymentStatus

Enum MemberString ValueDescription
APPROVED"APPROVED"Payment completed and settled successfully
PENDING"PENDING"Awaiting customer scan or approval
PRE_AUTH"PRE-AUTH"Funds authorized / held on card
DECLINED"DECLINED"Payment declined by bank or insufficient funds
CANCELLED"CANCELLED"Customer cancelled or session closed
REFUNDED"REFUNDED"Transaction has been refunded

AbaPaymentOption

Enum MemberValueMethod
ABAPAY_KHQR"abapay_khqr"ABA Pay + National Bakong KHQR
ABAPAY_KHQR_DEEPLINK"abapay_khqr_deeplink"Mobile deeplink directly launching ABA Mobile
CARDS"cards"Visa, Mastercard, JCB, and UnionPay
ALIPAY"alipay"Alipay Global
WECHAT"wechat"WeChat Pay
GOOGLE_PAY"google_pay"Google Pay Mobile Wallet

AbaStatusCode (Gateway Status Codes)

CodeMeaningResolution
"00"SuccessRequest processed successfully
"1"Invalid HashVerify your API key matches the merchant ID
"2"Invalid ParametersCheck required fields, amount decimals, and URLs
"5"Transaction Not FoundTransaction ID does not exist in gateway records
"26"Invalid MerchantMerchant account inactive or unwhitelisted
Defensive EngineeringError Hierarchy

Error Handling & Hierarchy

SobinPay errors inherit from the native JavaScript Error class and include typed machine-readable error codes.

Safe Error Handling Pattern
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { SobinPay, SobinPayError, ProviderApiError } from "@sobindigital/pay"; try { const status = await sobin.aba.checkTransaction(tranId); } catch (err: unknown) { if (err instanceof ProviderApiError) { console.error("Gateway HTTP Status:", err.statusCode); console.error("Gateway Message:", err.message); } else if (err instanceof SobinPayError) { console.error("SobinPay Code:", err.code); // e.g. "INVALID_TRANSACTION_ID" } else { console.error("Unexpected error:", err); } }
Next.js 15 / 16App Router & Server Actions

Next.js App Router Recipe

Production recipe for Next.js App Router utilizing Server Actions for purchase generation and Route Handlers for pushback webhooks.

app/actions/checkout.ts (Server Action)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"use server"; import { SobinPay, AbaPaymentOption, AbaViewType } from "@sobindigital/pay"; const sobin = new SobinPay({ aba: { merchantId: process.env.ABA_MERCHANT_ID!, apiKey: process.env.ABA_API_KEY!, apiUrl: process.env.ABA_API_URL, }, }); export async function createCheckoutSession(amount: number) { const purchase = sobin.aba.createPurchase({ amount, currency: "USD", returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/checkout/success`, cancelUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/checkout/cancel`, paymentOption: AbaPaymentOption.ABAPAY_KHQR, viewType: AbaViewType.POPUP, }); return { actionUrl: purchase.actionUrl, payload: purchase.payload, }; }
app/api/webhooks/payway/route.ts (Webhook Route Handler)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { NextResponse } from "next/server"; import { SobinPay } from "@sobindigital/pay"; const sobin = new SobinPay({ aba: { merchantId: process.env.ABA_MERCHANT_ID!, apiKey: process.env.ABA_API_KEY!, }, }); export async function POST(req: Request) { try { const payload = await req.json(); const signature = req.headers.get("x-payway-hmac-sha512") || payload.hash; const verification = sobin.aba.verifyWebhook(payload, signature); if (!verification.isValid) { return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); } if (verification.status === "APPROVED") { // Perform database update in constant time console.log("Order paid:", verification.tranId); } return NextResponse.json({ success: true }); } catch (err: unknown) { return NextResponse.json({ error: "Webhook processing error" }, { status: 400 }); } }
Express.jsREST API

Express.js Recipe

Standard Node.js / Express integration with pushback webhook verification.

server.ts (Express)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import express from "express"; import { SobinPay, AbaPaymentOption } from "@sobindigital/pay"; const app = express(); app.use(express.json()); const sobin = new SobinPay({ aba: { merchantId: process.env.ABA_MERCHANT_ID!, apiKey: process.env.ABA_API_KEY!, }, }); // Create checkout app.post("/api/checkout", (req, res) => { const purchase = sobin.aba.createPurchase({ amount: req.body.amount, currency: "USD", returnUrl: "https://example.com/success", paymentOption: AbaPaymentOption.ABAPAY_KHQR, }); res.json(purchase); }); // Receive Pushback Webhook app.post("/api/payway/webhook", (req, res) => { const signature = req.headers["x-payway-hmac-sha512"] as string; const verification = sobin.aba.verifyWebhook(req.body, signature); if (!verification.isValid) { return res.status(401).send("Unauthorized Webhook"); } // Handle successful transaction res.status(200).send("OK"); }); app.listen(4000, () => console.log("Server listening on port 4000"));
NestJSDependency Injection

NestJS Recipe (Service & Guard)

Enterprise NestJS provider injection and custom canActivate guard.

sobinpay.service.ts (NestJS Injectable)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { SobinPay } from "@sobindigital/pay"; @Injectable() export class SobinPayService { public readonly client: SobinPay; constructor(private configService: ConfigService) { this.client = new SobinPay({ aba: { merchantId: this.configService.getOrThrow<string>("ABA_MERCHANT_ID"), apiKey: this.configService.getOrThrow<string>("ABA_API_KEY"), apiUrl: this.configService.get<string>("ABA_API_URL"), }, }); } }
AEO Knowledge Base & FAQ

Frequently Asked Questions

Comprehensive answers to common architectural, security, and integration questions regarding ABA PayWay, Bakong KHQR, and SobinPay.

SobinPay is the official enterprise-grade TypeScript payment gateway monorepo for Cambodia, developed and maintained by Sobin Digital. It unifies Cambodia’s two primary payment channels — ABA PayWay (Advanced Bank of Asia) and Bakong KHQR (National Bank of Cambodia) — under a single, strictly-typed SDK.

Unlike legacy unmaintained scripts, SobinPay is built for production enterprise workloads with zero runtime dependencies, dual ESM/CJS output, and active long-term maintenance by Sobin Digital.