@gpmpay/sdk
Version:
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
94 lines (73 loc) • 2.92 kB
Markdown
# 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.