UNPKG

@gpmpay/sdk

Version:

Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.

94 lines (73 loc) • 2.92 kB
# Self-reconcile — start here You mint the code, build the QR from it, and match incoming webhooks against your own database. This is the only payment model GPM Pay has: it reports transactions, it does not hold order state. ## Run ```bash npm install express @gpmpay/sdk export GPMPAY_API_TOKEN=gpm_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyy node server.mjs ``` Scopes needed: `bank-accounts:read` and `webhooks:manage`. ## Wire up the webhook ```bash # terminal 2 ngrok http 3000 # terminal 3 curl -X POST localhost:3000/setup-webhook \ -H 'content-type: application/json' \ -d '{"publicUrl":"https://<id>.ngrok.app"}' ``` The secret is printed to the server console **once**. Export it and restart: ```bash export GPMPAY_WEBHOOK_SECRET=<printed value> node server.mjs ``` ## Try it ```bash curl -X POST localhost:3000/checkout \ -H 'content-type: application/json' \ -d '{"orderId":"123","amount":50000}' ``` ```json { "qrPayload": "00020101021238...", "qrImageUrl": "https://img.vietqr.io/image/970422-1234567890-compact.png?amount=50000&addInfo=DH123&accountName=NGUYEN+VAN+A", "amount": 50000, "transferContent": "DH123", "bankName": "MBBank", "bankBin": "970422", "accountNumber": "1234567890", "accountName": "NGUYEN VAN A" } ``` Simulate the payment (sandbox / localhost only): ```ts await client.simulator.createTransaction({ bankAccountId: account.id, amount: 50_000, transferContent: 'DH123', // the same code the checkout handed out }); ``` The server logs `✅ DH123 paid 50.000 ₫`. ## What to copy - **`buildPaymentInstructions({ bankAccount, amount, transferContent })`** — one local call, no network. `transferContent` becomes what the payer types, and it is the only thing tying a bank transfer back to your order. - **`express.raw({ type: 'application/json' })` on the webhook route only**, and mounted *before* any global `express.json()`. - **`claimTransaction(payload.id)`** — deliveries retry up to 6 times. Replace the `Set` with a unique constraint in your database. - **The `transferType !== 'in'` guard** — you receive a webhook for *every* transaction on the account, including outgoing ones. - **The amount check.** Nothing compares it for you. The example marks the order `UNDERPAID` rather than silently fulfilling it. - **Integer VND.** `Math.round()` at the boundary; a fractional amount in the QR produces a transfer that will not equal your stored total. - **A regex, not `===`.** Banks normalize the content and some prepend their own prefix, so `CODE_REGEX.exec(content)` beats comparing the whole string. ## Two things this example leaves to you 1. **Expiry.** There is no `expiresAt` here — decide how long a `PENDING` order stays payable and sweep the stale ones yourself. 2. **Durability.** `orders` and `processedTransactions` are in-memory `Map`/`Set` and do not survive a restart or a second instance.