DEVELOPER HANDBOOK / EASYPOS × TICKET

ຈາກການເຊື່ອມຕໍ່ API
ສູ່ການອອກປີ້ທີ່ຖືກຕ້ອງ.

ຄູ່ມືສຳລັບ developer ແລະຜູ້ດູແລ: ກຽມ Partner, ລົງລາຍເຊັນ request, ສະແດງ LaoQR ແລະຮັບຜົນຊຳລະຜ່ານ webhook.

Node.js + PostgreSQLRSA → API · HMAC ← Webhookກວດອ້າງອີງ 18/09/2026
ເລີ່ມຈາກ workflow ↗ດາວໂຫຼດ Source Code ↓
ໜ້ານີ້ເປັນຄູ່ມືເທົ່ານັ້ນ — ບໍ່ສົ່ງ API, ບໍ່ສ້າງ QR ຫຼືເງິນຈິງ. IDs, signature ແລະ QR ໃນ code ເປັນ placeholder; ບໍ່ມີ credentials ຈິງ.
DEVELOPER STARTER / SOURCE ONLY

ດາວໂຫຼດ source code ໄປພັດທະນາຕໍ່

Web + API + PostgreSQL schema ແລະ migrations 6 ຊຸດ, ພ້ອມ tests ແລະ README ວິທີຕິດຕັ້ງ/ເຊື່ອມຕໍ່ EASYPOS.

Version 18/09/2026 · 116 files · 495 KiB (506,947 bytes)

ບໍ່ມີ credentials, private keys ຫຼືຂໍ້ມູນທຸລະກຳຈິງ. ບໍ່ຕ້ອງໃຊ້ລະຫັດ ZIP. ແຕກໄຟລ໌ແລ້ວອ່ານ README.md; ເລີ່ມໃນ local mock ແລະໃຊ້ credentials ຂອງທ່ານເອງ.

ດາວໂຫຼດ Source ZIP ດາວໂຫຼດ SHA-256 checksum
ກວດ SHA-256 ຂອງໄຟລ໌a3bef7579aa9219cc800ffbffba9d36318e0cba7bad394044dc27a5ddc0f2f83
01

ລະບົບຄວນມີຫຍັງ?

ແຍກວຽກຂອງ browser, Ticket backend ແລະ EASYPOS ໃຫ້ຊັດເຈນ.

  1. ເລືອກປີ້

    Browser ສົ່ງ product, quantity, visitDate; ບໍ່ກຳນົດຍອດເງິນ.

  2. ສ້າງ order & sign

    Ticket ຄຳນວນລາຄາຝັ່ງ server, ບັນທຶກ order ແລະ idempotency, ລົງ RSA signature.

  3. ຂໍ QR ຈາກ EASYPOS

    Backend POST new-transaction; ຮັບ transactionId, qrCode string ແລະ expiredTime.

  4. ສະແກນ & ຊຳລະ

    Browser render string ເປັນຮູບ QR; ລູກຄ້າສະແກນຜ່ານ bank app.

  5. ກວດ webhook → PAID

    EASYPOS ສົ່ງ PAYMENT_CONFIRMED; Ticket ກວດ HMAC/ຂໍ້ມູນ ແລະບັນທຶກຄັ້ງດຽວ.

  6. ອອກປີ້ & ກວດເຂົ້າ

    Browser ອ່ານສະຖານະ order; ປີ້ທີ່ຢືນຢັນແລ້ວໃຊ້ເຂົ້າໄດ້ຄັ້ງດຽວ.

01 / Frontend

ໜ້າຊື້ປີ້, QR/loading/expiry, ສະຖານະ ແລະປີ້. ບໍ່ມີ private key ຫຼື webhook secret.

02 / Backend + DB

Order/payment/receipt/ticket, catalog price snapshot, idempotency, transaction ແລະ reconciliation.

03 / HTTPS receiver

Public HTTPS, raw body, signature/time/source validation, durable dedupe, monitoring ແລະ backup.

04 / Operations

Partner/key lifecycle, secret storage, normal delivery + bounded retry, logs ແລະຜູ້ຮັບຜິດຊອບແກ້ລາຍການຄ້າງ.

QR ສ້າງໄດ້ ≠ ຈ່າຍແລ້ວ. ຫ້າມໃຫ້ browser redirect ຫຼືປຸ່ມ “ຈ່າຍແລ້ວ” ກຳນົດ PAID.
02

Portal & credentials

ຂໍ້ມູນໃດໄດ້ຈາກ provider? ຂໍ້ມູນໃດສ້າງເອງ?

ຂໍ້ມູນທີ່ມາ / ວິທີໃຊ້ຈຸດເກັບ
API base URL + Partner ID + Merchant IDEASYPOS ຢືນຢັນ onboarding/environment ແລະ merchant bindingBackend configuration
RSA private keyທີມ Partner ສ້າງເອງ; ໃຊ້ sign outbound requestProtected server file / secret store · 0600
RSA public key + fingerprintສະກັດຈາກ private key; upload public PEM ໃສ່ PortalPortal key lifecycle + backend fingerprint
Webhook secretຜູ້ມີສິດຈັດການຜ່ານ Portal secret lifecycleProtected file; ໃຊ້ກວດ HMAC ເທົ່ານັ້ນ
Portal loginບັນຊີຜູ້ດູແລ; ບໍ່ແມ່ນ API credentialPassword manager; ບໍ່ຝັງໃນ app/code
  1. ເຂົ້າ EASYPOS Portal ↗ → Payment gateway → Partners. ເລືອກ Partner ທີ່ຕ້ອງການ; ສ້າງ draft ສະເພາະ partner ໃໝ່.
  2. ລະບຸຊື່ທຸລະກິດ, email, merchant binding ແລະ permissions ທີ່ຈຳເປັນ. ໃຫ້ provider ກວດ outbound IP ທີ່ API ເຫັນຈິງ; ບໍ່ຄາດເດົາຈາກ DNS ຫຼື IP ໃນ Docker.
  3. ລົງທະບຽນ public key → ກຽມ HTTPS webhook → ຕັ້ງ secret → Challenge/Test. ການ Activate Partner ແຍກຈາກ Activate key.
  4. ກວດ live policy ຫຼັງ approval: ACTIVE, Create transactions, Receive webhook, key ແລະ endpoint readiness. ຄ່າ Draft ບໍ່ຮັບປະກັນວ່າ Live ໃຊ້ແລ້ວ.
ເບິ່ງຮູບ Portal: Login
Portal login
Portal loginຮູບຈິງກ່ອນປ້ອນ credentials; ບໍ່ໄດ້ບັນທຶກ password ໃນຄູ່ມື.

ປຸ່ມ ແລະສິດອາດຕ່າງກັນຕາມ role. ໜ້ານີ້ບໍ່ປ່ຽນ IP allowlist, network policy, secret ຫຼື key ໃດໆ.

03

ສ້າງ RSA key & ລົງລາຍເຊັນ

Private key ຢູ່ server ຂອງທ່ານ; EASYPOS ຮັບພຽງ public key.

OpenSSL · NEW demo key pair
# Example only: create NEW demo keys; never overwrite the active key.
umask 077
key_dir=$(mktemp -d ./partner-key-demo.XXXXXX)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$key_dir/partner-private.pem"
openssl pkey -in "$key_dir/partner-private.pem" -pubout -out "$key_dir/partner-public.pem"
openssl pkey -pubin -in "$key_dir/partner-public.pem" -outform DER | openssl dgst -sha256
# Upload ONLY partner-public.pem. Keep the private file server-side, mode 0600.

PEM ທີ່ upload ຕ້ອງເລີ່ມ -----BEGIN PUBLIC KEY-----. Fingerprint ແມ່ນ SHA-256 hex ຂອງ DER SubjectPublicKeyInfo; ບໍ່ແມ່ນ hash ຂໍ້ຄວາມ PEM.

  1. Partners → ເລືອກ Partner → Key lifecycle → ປ້ອນ public PEM, effective/expiry ແລະ audit reason.
  2. Validate only → ກວດ fingerprint → Upload pending key. ຂັ້ນນີ້ຍັງບໍ່ Activate.
  3. Activate ເມື່ອ backend ກຽມ key ຄູ່ກັນແລ້ວ ແລະໄດ້ອະນຸມັດ. ການ rotate ຕ້ອງມີ grace/rollback plan; ຢ່າແທນ key ເກົ່າໂດຍບໍ່ກວດ retries ຄ້າງ.
Public-key lifecycle
Public-key lifecycleຮູບ Portal ຈິງ: public-key metadata ແລະ pending-key controls; ບໍ່ມີ private key ແລະບໍ່ໄດ້ upload/rotate.

Canonical request · ລຽງຕາມນີ້

RSA canonical · LF, no trailing newline
easypay-partner-v1
<exact X-Partner-Timestamp>
<exact X-Partner-Nonce>
<compact JSON body>

ຕໍ່ແຕ່ລະແຖວດ້ວຍ LF (\n), UTF-8, ບໍ່ມີ newline ທ້າຍ. ສົ່ງ body string ດຽວກັບທີ່ sign; ຫ້າມ pretty-print ຫຼື serialize ໃໝ່ຫຼັງ sign.

Node.js · prepare signed request (ບໍ່ມີ network call)
RSA signing · server only
import { sign, constants, randomUUID } from "node:crypto";

// Server-side only. Input order/price must already be validated and persisted.
export function prepareSignedRequest(payload, privateKey, fingerprint, operationKey) {
  const timestamp = new Date().toISOString();
  const nonce = randomUUID().replaceAll("-", "");
  const body = JSON.stringify(payload);
  const canonical = ["easypay-partner-v1", timestamp, nonce, body].join("\n");
  const signature = sign("RSA-SHA1", Buffer.from(canonical, "utf8"), {
    key: privateKey, padding: constants.RSA_PKCS1_PADDING
  }).toString("base64");
  return { body, headers: {
    "Content-Type": "application/json",
    "X-Partner-Timestamp": timestamp, "X-Partner-Nonce": nonce,
    "X-Key-Fingerprint": fingerprint, "X-Signature": signature,
    "X-Idempotency-Key": operationKey, "X-Correlation-ID": "corr-" + randomUUID()
  }};
}
// No fetch here. Persist this exact envelope before an authorized send.
Contract v1 ໃຊ້ RSA PKCS#1 v1.5 + SHA-1. ນີ້ແມ່ນ compatibility requirement, ບໍ່ແມ່ນຄຳແນະນຳ algorithm ສຳລັບ protocol ໃໝ່. ປ່ຽນ algorithm ໄດ້ເມື່ອ provider ອອກ contract ໃໝ່ຮ່ວມກັນ.
04

API, headers & payload

ບໍ່ສັບສົນ API ຂອງ Ticket ກັບ API ຂອງ EASYPOS.

ເສັ້ນທາງຜູ້ເອີ້ນ → ຜູ້ຮັບໜ້າທີ່
POST /api/ordersBrowser → Ticketສ້າງ order; header Idempotency-Key (ບໍ່ມີ X-)
POST /payment/api/PaymentQr/new-transactionTicket backend → EASYPOSຂໍ LaoQR; RSA-signed JSON, ບໍ່ໃຊ້ Portal password/Bearer token
GET /api/orders/{publicToken}Browser → Ticketອ່ານ order/payment/ticket ຈາກ DB; ບໍ່ແມ່ນ provider lookup
POST /api/webhooks/easyposEASYPOS → Ticketຮັບ signed payment/challenge/test
GET /payment/api/PaymentQr/status/{paymentId}Authenticated customer → EASYPOSCustomer-owner-bound; ບໍ່ແມ່ນ Partner status API, ຫ້າມສົມມຸດວ່າ RSA ໃຊ້ໄດ້
Browser → Ticket: ສ້າງ order ແລະ poll ສະຖານະ
POST /api/orders · JSON
{
  "productId": "YOUR_PRODUCT_ID",
  "productRevision": 1,
  "quantity": 1,
  "visitDate": "2026-10-01"
}

Header: Content-Type: application/json, Idempotency-Key: order-<unique-id>; same-origin request. ສຳເລັດ 201 ສົ່ງ {"publicToken":"<private-token>","url":"<order-url>"}. Browser ໃຊ້ GET order ເພື່ອອ່ານສະຖານະ. ຢ່າສົ່ງ publicToken ຈິງໃນ logs/ຄູ່ມື: ມັນໃຫ້ສິດເບິ່ງ order.

POSThttps://api.qrlaos.com/payment/api/PaymentQr/new-transaction

ໃຊ້ຈາກ backend ເທົ່ານັ້ນ. ຢືນຢັນ environment ກັບ EASYPOS; ຄຳວ່າ UAT ບໍ່ຮັບປະກັນ sandbox ແລະ QR ອາດເປັນເງິນຈິງ.

Node.js · ສົ່ງ envelope ທີ່ sign ແລ້ວ 1 ຄັ້ງ
HTTP transport · illustration, not invoked
// Server-side illustration ONLY. No call is made by this guide.
// envelope is the persisted output of prepareSignedRequest.
export async function sendOnce(envelope, approved = false) {
  if (!approved) throw new Error("OUTBOUND_NOT_APPROVED");
  const response = await fetch(
    "https://api.qrlaos.com/payment/api/PaymentQr/new-transaction",
    { method: "POST", headers: envelope.headers, body: envelope.body,
      redirect: "error", signal: AbortSignal.timeout(12000) }
  );
  return { status: response.status, text: await response.text() };
}
// No automatic retries. Validate/classify the response before saving a QR.
// Timeout/unknown outcome -> REVIEW/reconcile, not a new payment.
// Caller must enforce authorization, mode, freshness and idempotency guards.

ຕົວຢ່າງ 12 ວິນາທີກົງກັບ client ນີ້, ບໍ່ແມ່ນ SLA ຂອງ provider. ບໍ່ຕັ້ງ Content-Length ເອງ; ໃຫ້ HTTP client ຄຳນວນຈາກ UTF-8 body. ຕົວຢ່າງນີ້ສະແດງ transport ເທົ່ານັ້ນ, ຕ້ອງມີ mode/permission guard, response-size limit, schema validation ແລະ DB commit ໃນ adapter ຈິງ.

Request headers

Headerຮູບແບບຫຼັກການ
Content-Typeapplication/jsonJSON ແບບ UTF-8
X-Partner-TimestampRFC3339 UTCຕ້ອງກົງກັບ canonical; ຄູ່ມື v1: ອະດີດ ≤300s / ອະນາຄົດ ≤60s, ກວດ policy ກັບ provider
X-Partner-Nonce16–128 charsA–Z a–z 0–9 . _ ~ -; ໃໝ່ຕໍ່ logical operation
X-Key-FingerprintSHA-256 lowercase hexDER SubjectPublicKeyInfo ຂອງ public key; ຄູ່ມືແນະນຳ, Ticket ສົ່ງສະເໝີ
X-SignatureBase64RSA PKCS#1 v1.5 + SHA-1 ຕາມ contract v1
X-Idempotency-Key8–128 chars[A-Za-z0-9][A-Za-z0-9._:-]{7,127}; 1 key / 1 operation
X-Correlation-IDcorr-<random UUID>ແນະນຳສຳລັບໄລ່ logs ທັງສອງຝັ່ງ

Request body

FieldTypeຄ່າ / ການກວດ
uuidstring · required14 ຕົວອັກສອນ; reference ບໍ່ຊ້ຳຕໍ່ payment
partnerIdstring · requiredຮັບຈາກ EASYPOS; ກົງກັບ key/policy
amountinteger · requiredຈຳນວນເຕັມ LAK >0; server ຄຳນວນລາຄາ × ຈຳນວນ
descriptionstringTicket ຈຳກັດ 1–30 ຕົວອັກສອນກ່ອນ sign
expiryTimeinteger · requiredນາທີ; ຕົວຢ່າງ 15, ໃຊ້ຄ່າທີ່ provider ຕົກລົງ
merchantIdstring · requiredmerchant ທີ່ provider ຜູກກັບ partner
platformstring · requiredTicket integration ນີ້ໃຊ້ IOS; ຢ່າປ່ຽນເປັນຊື່ໂໝດທົດສອບ
mobileNumstring · optional in PDFTicket ໃຊ້ 2000000000 ຖ້າຫວ່າງ; ຕັ້ງກ່ອນ serialize/sign
currencystring · requiredLAK; ກວດກົງກັບ order ແລະ callback
new-transaction · example JSON (NOT FOR SENDING)
{
  "uuid": "P0000000000001",
  "partnerId": "YOUR_PARTNER_ID",
  "amount": 30000,
  "description": "Kuang Si admission",
  "expiryTime": 15,
  "merchantId": "YOUR_MERCHANT_ID",
  "platform": "IOS",
  "mobileNum": "2000000000",
  "currency": "LAK"
}

30,000 LAK ແມ່ນຕົວຢ່າງອະທິບາຍ, ບໍ່ແມ່ນລາຄາ catalog ປັດຈຸບັນ. ເກັບ unit price/revision ໃນ order ກ່ອນເອີ້ນ provider; ບໍ່ເຊື່ອ amount ຈາກ browser.

200 · illustrative success response
{
  "success": true,
  "transactionId": "EXAMPLE_TRANSACTION_ID",
  "qrCode": "EXAMPLE_ONLY_NOT_A_PAYABLE_QR",
  "expiredTime": "2026-09-18T03:15:00Z",
  "amount": 30000,
  "uuid": "P0000000000001"
}

Response ອາດມີ id, code, message ເພີ່ມ. Adapter ຕ້ອງ validate success, transaction ID, QR string, expiry ແລະຂໍ້ມູນ order; HTTP 200 ຢ່າງດຽວບໍ່ພໍ.

Browser · render the returned QR string
import QRCode from "qrcode";
// qrCode comes from the validated, persisted provider response.
const imageUrl = await QRCode.toDataURL(qrCode, {
  width: 300, margin: 2, errorCorrectionLevel: "M"
});
// Render <img src={imageUrl} alt="Payment QR" />.
// Do NOT encode the whole JSON, transaction ID, or order URL.

ບໍ່ໃຊ້ third-party QR image service ເພາະຈະສົ່ງ payment payload ອອກຂ້າງນອກ. ຖ້າບໍ່ມີ QR string ຢ່າສ້າງ QR ປອມ; ສະແດງ pending/failed/review ຕາມຂໍ້ເທັດຈິງ.

05

Webhook: ຢືນຢັນການຊຳລະ

RSA ໃຊ້ຂາອອກ; HMAC-SHA256 ໃຊ້ກວດ callback ຂາເຂົ້າ.

POSThttps://tickets.qrlaos.com/api/webhooks/easypos

Portal → Partner → Webhook setup: ຕັ້ງ HTTPS endpoint ຂອງ server, ກວດ retry policy ແລະ audit reason. ຜູ້ມີສິດກຽມ secret ຜ່ານ protected channel ໃຫ້ backend ແລ້ວຈຶ່ງ Challenge/Test. ຢ່າ rotate secret ທີ່ໃຊ້ງານຢູ່ເພື່ອອ່ານຄ່າຄືນ.

Webhook endpoint & secret lifecycle
Webhook endpoint & secret lifecycleຮູບ controls ຈິງ. URL ໃນຟອມເປັນ placeholder, ບໍ່ແມ່ນການຢືນຢັນ live endpoint; ບໍ່ໄດ້ Save/Challenge/Test/rotate.
PAYMENT_CONFIRMED · headers
{
  "Content-Type": "application/json",
  "X-EasyPay-Webhook-Id": "pwh-123",
  "X-EasyPay-Webhook-Timestamp": "2026-09-18T03:01:00.000Z",
  "X-EasyPay-Webhook-Signature-Version": "easypay-webhook-v1",
  "X-EasyPay-Webhook-Signature": "sha256=<64 lowercase hex characters>"
}
PAYMENT_CONFIRMED · signed envelope
{
  "eventType": "PAYMENT_CONFIRMED",
  "deliveryId": "pwh-123",
  "timestamp": "2026-09-18T03:01:00.000Z",
  "replayProtected": true,
  "data": {
    "uuid": "P0000000000001",
    "amount": 30000,
    "billId": "EXAMPLE_TRANSACTION_ID",
    "currency": "LAK",
    "partnerId": "YOUR_PARTNER_ID",
    "paymentAt": "2026-09-18T03:00:50.000000Z",
    "paidStatus": "FNLD",
    "paymenBank": "LDB"
  }
}
Field / ກຸ່ມຫຼັກການ
eventType / replayProtectedPAYMENT_CONFIRMED / true; strict schema
deliveryId / timestampກົງກັບ headers ແບບ exact; delivery ID ຮູບ pwh-<positive int64>
data.billId / uuid / partnerIdProvider payment identity / Ticket reference / partner; ກົງກັບ order
data.amount / currencyກົງກັບຍອດເງິນ ແລະ LAK ທີ່ບັນທຶກ
data.paidStatusFNLD ແບບ exact; ຢ່າຮັບຄ່າອື່ນເປັນ PAID
data.paymentAtISO-8601 UTC; ຮັກສາ UTC/precision, ບໍ່ປ່ຽນເປັນເວລາລາວໃນ storage
data.paymenBankຊື່ field ຕາມ contract (paymenBank); ບໍ່ແກ້ການສະກົດເອງ
HMAC canonical · LF, no trailing newline
easypay-webhook-v1
<deliveryId>
<exact timestamp header>
<eventType>
<lowercase SHA256 hex of raw UTF-8 body>

HMAC-SHA256 ໃຊ້ UTF-8 bytes ຂອງ secret ໂດຍກົງ (ບໍ່ base64-decode). Output: sha256=<lowercase hex>. ຮັກສາ raw body ກ່ອນ JSON parsing; ບໍ່ hash JSON ທີ່ serialize ໃໝ່.

Node.js · HMAC comparison (ບໍ່ແມ່ນ receiver ຄົບຊຸດ)
HMAC verification · crypto only
import { createHash, createHmac, timingSafeEqual } from "node:crypto";

// CRYPTO CHECK ONLY: also apply the validation/DB gates listed below.
export function matchesHmac(rawBody, deliveryId, timestamp, eventType, signature, secret) {
  if (!/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const digest = createHash("sha256").update(rawBody, "utf8").digest("hex");
  const canonical = ["easypay-webhook-v1", deliveryId, timestamp, eventType, digest].join("\n");
  const expected = createHmac("sha256", Buffer.from(secret, "utf8"))
    .update(canonical, "utf8").digest();
  return timingSafeEqual(Buffer.from(signature.slice(7), "hex"), expected);
}

ກວດຄົບກ່ອນຕອບ 200

  1. ກວດ trusted ingress/source policy ທີ່ມີຢູ່; ບໍ່ເຊື່ອ forwarded headers ຈາກ client ໂດຍກົງ.
  2. ຈຳກັດ body 16 KiB ແລະ request time. ກວດ schema, version, exact header/body IDs, UTC timestamp (Ticket ໃຊ້ ±15 ນາທີ) ແລະ timing-safe HMAC.
  3. ກວດ partner/payment/reference/amount/currency/FNLD; ບໍ່ອະນຸມັດລາຍການບໍ່ຮູ້ຈັກ ຫຼືຂໍ້ມູນຂັດກັນ.
  4. Database unique/dedupe ຕາມ delivery ID ແລະ payment identity; atomic transaction ສຳລັບ receipt + PAID + ticket. Duplicate ທີ່ກົງກັນຕອບ 200 ໂດຍບໍ່ອອກປີ້ໃໝ່.
  5. Commit ສຳເລັດຈຶ່ງ acknowledge {"received":true,"duplicate":false}. DB ລົ້ມເຫຼວບໍ່ຕອບວ່າຮັບແລ້ວ.
Challenge & signed test · endpoint ດຽວກັນ

ໃຊ້ headers/HMAC algorithm ດຽວກັນ, ແຕ່ delivery ID ເປັນ UUID ແລະ eventType ຕ່າງກັນ. ຢ່ານຳ signature ຂອງ PAYMENT_CONFIRMED ມາໃຊ້ຊ້ຳ.

PARTNER_WEBHOOK_CHALLENGE · JSON
{
  "eventType": "PARTNER_WEBHOOK_CHALLENGE",
  "deliveryId": "00000000-0000-4000-8000-000000000001",
  "timestamp": "2026-09-18T03:01:00.000Z",
  "replayProtected": true,
  "challenge": "0123456789abcdef0123456789abcdef"
}

ຫຼັງ verify + durable replay claim ໃຫ້ຕອບ 200 {"challenge":"<same-value>"}.

PARTNER_WEBHOOK_TEST · JSON
{
  "eventType": "PARTNER_WEBHOOK_TEST",
  "deliveryId": "00000000-0000-4000-8000-000000000001",
  "timestamp": "2026-09-18T03:01:00.000Z",
  "replayProtected": true,
  "challenge": null
}

ຕອບ 200 {"received":true}; ບໍ່ຕ້ອງ echo challenge, ບໍ່ PAID/ອອກປີ້. ຕົວຢ່າງ IDs ໃນໜ້ານີ້ໃຊ້ອ່ານເທົ່ານັ້ນ.

ຄູ່ມື PDF/Postman ເກົ່າມີ flat unsigned callback. ນັ້ນບໍ່ແມ່ນ contract ຂອງ receiver ປັດຈຸບັນ. ຫ້າມປິດ signature verification ເພື່ອໃຫ້ sample ເກົ່າຜ່ານ. Challenge ຜ່ານບໍ່ແມ່ນຫຼັກຖານວ່າ normal payment delivery ພ້ອມ.
06

Retry, ລາຍການຊ້ຳ & ບັນຫາ

ບໍ່ໃຫ້ timeout ກາຍເປັນການສ້າງ payment ຊ້ຳ.

ເຫດການວິທີຈັດການ
Refresh / double clickໃຊ້ order/idempotency ເດີມ; ບໍ່ສ້າງ payment ໃໝ່ທຸກຄັ້ງ
Timeout / connection reset / ຜົນບໍ່ແນ່ນອນREVIEW + reconcile ກັບ provider; ບໍ່ສະຫຼຸບ FAILED/NO_RECORD ເອງ ແລະບໍ່ blind retry ດ້ວຍ UUID ໃໝ່
400 / 401 / 403ກວດ payload, exact signing, active key/partner ແລະ policy; ຢ່າ retry ບໍ່ຈຳກັດ ຫຼືປ່ຽນ IP policy ເອງ
429 / temporary failureກວດ outcome/idempotency/freshness ກ່ອນ retry ຕາມ contract; bounded backoff + jitter
Webhook 503 / DB failureProvider retry ຕາມ policy: delivery ID ເດີມ, timestamp/signature ໃໝ່; receiver ບໍ່ commit ເຄິ່ງທາງ
QR ໝົດອາຍຸ / late callbackUI ສະແດງ EXPIRED; ການຫັກເງິນ/late confirmation ຕ້ອງ reconcile ຕາມ business policy

ເກັບ correlation ID, UTC start/end, HTTP status, exception name/code, UTF-8 byte count ແລະ SHA-256 ຂອງ request body. ຢ່າ log password/private key/webhook secret/token/signature ຫຼື QR payload ແບບເປີດສາທາລະນະ. ການເພີ່ມ timeout ຢ່າງດຽວບໍ່ໄດ້ພິສູດວ່າ transport ຖືກແກ້.

07

Workflow ໃນໜ້າເວັບ Ticket

ຮູບສຳລັບອະທິບາຍ, ບໍ່ແມ່ນຄຳຢືນຢັນ runtime ຫຼືການຈ່າຍໃໝ່.

03 · ຈ່າຍສຳເລັດ → ປີ້ (LOCAL MOCK / TEST ONLY)
03 · ຈ່າຍສຳເລັດ → ປີ້ (LOCAL MOCK / TEST ONLY)ຮູບຈາກ automated local test, ບໍ່ແມ່ນການຈ່າຍຈິງຈາກ EASYPOS; QR/ປີ້ໃນຮູບໃຊ້ເຂົ້າສະຖານທີ່ຈິງບໍ່ໄດ້.
08

Checklist ກ່ອນເປີດໃຊ້ງານ

Local tests → server readiness → provider delivery → ທົດສອບເງິນຈິງທີ່ອະນຸມັດ.

  • Local PostgreSQL ແຍກ test DB, mock adapter ແລະ test keys; ບລັອກ external network ໃນ tests.
  • Tests ສຳລັບ signature/body tampering/time, mismatch, duplicate/concurrency, 503/restart, atomic receipt/PAID/ticket ແລະປີ້ໃຊ້ຊ້ຳ.
  • Versioned artifact, schema migration ຮັກສາຂໍ້ມູນ, backup/rollback. Secrets ຢູ່ protected file/secret store, ບໍ່ໃນ image/Git/NEXT_PUBLIC/localStorage.
  • Deploy receiver ໃສ່ server HTTPS ເດີມ; ບໍ່ຈຳເປັນ tunnel ເຂົ້າ Mac. ກວດ health, signature rejection, source identity ແລະ live policy ແບບ read-only.
  • ໃຫ້ EASYPOS ຢືນຢັນ normal signed delivery, secret decrypt, retry/monitoring; synthetic test ຕ້ອງມີ fixture, budget ແລະ stop conditions.
  • ເງິນຈິງຕ້ອງອະນຸມັດ amount/payer/ການແກ້ໄຂຍອດ; ກວດ QR → ຍອດຈ່າຍ → webhook → PAID → ປີ້ → ກວດເຂົ້າ. Mock PASS ບໍ່ເທົ່າ production-ready.
  • ກ່ອນໃຊ້ງານສາທາລະນະ ຄວນມີ authentication/RBAC ສຳລັບປ່ຽນລາຄາ, ກວດເຂົ້າ ແລະຂໍ້ມູນທຸລະກຳ. Anonymous admin ຂອງ demo ບໍ່ແມ່ນ security pattern ທີ່ຄວນນຳໄປໃຊ້ຈິງ.

ແຫຼ່ງອ້າງອີງ & ຂອບເຂດ

ອ່ານ PARTNER_PAYMENT_GATEWAY_API_EN.pdf v1.0 (20/08/2026), EASYPOS-Payment-Gateway-Partner.postman_collection.json ແລະ Ticket provider / payment-webhook-contract / orders / fulfillment code. ພາບ Portal/Ticket ກວດວັນທີ 18/09/2026 ແບບ read-only.

Postman ມີ create ແລະ replay ທີ່ໃຊ້ new-transaction ເສັ້ນດຽວ; replay ບໍ່ແມ່ນ endpoint ໃໝ່. ບໍ່ພົບ contract Partner query/list/refund/settlement ທີ່ຢືນຢັນໃນຊຸດນີ້, ຈຶ່ງບໍ່ປະດິດ URL/payload. ຂໍ specification ຈາກ EASYPOS ກ່ອນເພີ່ມ.

Webhook envelope ໃນໜ້ານີ້ອີງ receiver ປັດຈຸບັນ, ແທນ unsigned sample ເກົ່າ. ຮູບ/ຕົວຢ່າງບໍ່ແມ່ນ live configuration ຫຼືຫຼັກຖານການທົດສອບເງິນຈິງ.