@gpmpay/sdk
v0.4.0
Published
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
Maintainers
Readme
@gpmpay/sdk
SDK Node.js chính thức cho GPM Pay — dựng QR VietQR, nhận webhook giao dịch, tự đối soát thanh toán.
Zero dependency · Node >= 18.17 · TypeScript sẵn có · ESM + CommonJS · MIT
🇬🇧 English · 📚 Tài liệu trực tuyến
Tài liệu chuyên sâu nằm ngay trong package. Đã cài rồi thì mở
node_modules/@gpmpay/sdk/docs/vi/(5 guide) ·AGENTS.md(chỉ dẫn cho AI agent) ·examples/(ví dụ chạy được). Chưa cài thì đọc thẳng trên web: duyệt toàn bộ file của package — hoặc xem bảng "Tài liệu" ở cuối trang.
⚠️ Chỉ dùng phía server. API token là secret. Đưa nó ra trình duyệt hoặc app mobile đồng nghĩa trao quyền truy cập tài khoản GPM Pay của bạn cho bất kỳ ai xem được mã nguồn.
Cài đặt
pnpm add @gpmpay/sdk # hoặc: npm i @gpmpay/sdk / yarn add @gpmpay/sdkGPM Pay làm gì
Không có cổng thanh toán nào giữ tiền. Khách chuyển khoản bình thường vào tài khoản của bạn; GPM Pay theo dõi biến động số dư và bắn webhook cho mọi giao dịch tiền vào, kèm số tiền và nội dung chuyển khoản.
Việc đối soát là của bạn. Bạn tự sinh mã đơn, nhét vào nội dung chuyển khoản, rồi khi webhook về thì dò lại mã đó trong payload.content và so số tiền. GPM Pay không sinh mã, không giữ đơn, không khớp lệnh thay bạn — nó là đường ống báo giao dịch.
Toàn bộ mô hình, kèm các bẫy đối soát thực tế: docs/vi/02-payments.md.
Bắt đầu trong 60 giây
1. Tạo API token tại https://app.gpmpay.com/api-tokens với scope webhooks:manage và bank-accounts:read.
2. Cài đặt và kiểm tra token:
export GPMPAY_API_TOKEN=gpm_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyy
pnpm add @gpmpay/sdk
npx gpmpay ping✓ Connected to https://api.gpmpay.com/api/v1 (182 ms)
Token gpm_a1b2c3d4••••••••
Scopes bank-accounts:read, webhooks:manage
Missing transactions:read3. Dựng QR với mã của bạn:
import { GpmPay } from '@gpmpay/sdk';
import { buildPaymentInstructions } from '@gpmpay/sdk/vietqr';
const client = new GpmPay({ apiToken: process.env.GPMPAY_API_TOKEN! });
const account = (await client.bankAccounts.list({ status: 'ACTIVE' })).data[0]!;
const code = `DH${localOrder.id}`; // mã của bạn — khách phải ghi chuỗi này
const { qrImageUrl, transferContent } = buildPaymentInstructions({
bankAccount: account,
amount: Math.round(localOrder.total), // VND, số nguyên
transferContent: code,
});4. Đối chiếu khi webhook về:
onEvent: async (event) => {
const code = /DH(\d+)/.exec(event.payload.content)?.[0];
const order = code && await db.orders.findByCode(code);
if (order && order.total === event.payload.transferAmount) {
await giaoHang(order);
}
}Cách dựng webhook đầy đủ ở mục dưới.
Cấu hình
const client = new GpmPay({
apiToken: process.env.GPMPAY_API_TOKEN!, // BẮT BUỘC
sandbox: false, // true → môi trường thử nghiệm
timeoutMs: 30_000,
maxRetries: 2,
userAgent: 'my-shop/2.1',
defaultHeaders: { 'X-Trace-Id': traceId }, // không ghi đè được Authorization
onRequest: (e) => logger.debug(e), // không bao giờ nhận token
onResponse: (e) => metrics.timing(e.durationMs),
});
const client = GpmPay.fromEnv(); // đọc GPMPAY_API_TOKENSDK không khởi tạo được nếu thiếu token — sai cấu hình lộ ra lúc deploy, không phải lúc khách đầu tiên bấm thanh toán:
new GpmPay({ apiToken: 'sk_live_x' }); // ném GpmPayConfigError — 'invalid_api_token_format'SDK không bao giờ in token ra. client.toString() và console.log(client) chỉ hiện prefix công khai (gpm_a1b2c3d4••••••••), an toàn để log và dán vào ticket hỗ trợ.
Bảng scope
Backend chỉ còn ba scope:
| Scope | Method dùng được |
|---|---|
| webhooks:manage | webhookSettings.*, webhookHistories.* |
| bank-accounts:read | bankAccounts.list, bankAccounts.retrieve, banks.list |
| transactions:read | transactions.list, transactions.listAll, transactions.retrieve, simulator.createTransaction |
Token thiếu scope sẽ nhận GpmPayPermissionError với .missingScope chỉ đúng scope còn thiếu.
⚠️
ApiTokenGuardcủa backend là fail-closed. Endpoint nào không khai báo scope thì mọi API token đều bị chặn (403), bất kể sở hữu tài khoản. Vì vậy SDK chỉ mô hình hoá đúng những route API token gọi được — vdclient.apiTokenschỉ córemove(), vì các route quản lý token còn lại là dashboard-only. Trường hợp này trảGpmPayPermissionErrorvới.reason === 'endpoint'.
Webhook
Xác thực chữ ký
Header gửi kèm phụ thuộc vào authorizationType bạn đặt trên webhook setting — ba chế độ dùng ba header hoàn toàn khác nhau:
| authorizationType | Header GPM Pay gửi | Cách kiểm tra |
|---|---|---|
| HMAC (mặc định) | X-GPMPay-Signature: t=<unix>,v1=<hex> — đổi tên được qua authorizationHeaderName | constructWebhookEvent() |
| API_KEY | Header bạn tự đặt, mặc định Authorization; giá trị là secret thô, không có prefix Bearer | verifyApiKeyHeader(received, expected) |
| NONE | Không có header xác thực nào | Không xác thực được — chỉ dùng cho endpoint nội bộ |
Ba điểm hay bị hiểu nhầm:
- Không tồn tại header
X-GPMPay-Timestamp. Timestamp nằm trongt=bên trong giá trị chữ ký. - Driver
HTTPkhông gửiX-GPMPay-Event— chỉ driver WordPress gửi.event.typelà giá trị mặc định phía SDK. - Enum là
HMAC, không phảiHMAC_SHA256. Thuật toán là SHA-256, tên enum thì không.
Với HMAC: chữ ký ký trên chuỗi `${t}.${rawBody}`, cửa sổ lệch giờ ±300 giây.
import { constructWebhookEvent } from '@gpmpay/sdk/webhooks';
const event = constructWebhookEvent({
rawBody, // BYTE THÔ, không phải object đã parse
signature: headers['x-gpmpay-signature'],
secret: process.env.GPMPAY_WEBHOOK_SECRET!,
});
if (event.payload.transferType === 'in') {
await doiSoat(event.payload);
}Với API_KEY:
import { verifyApiKeyHeader } from '@gpmpay/sdk/webhooks';
if (!verifyApiKeyHeader(headers.authorization, process.env.GPMPAY_WEBHOOK_SECRET!)) {
return res.status(401).end();
}⚠️ Phải dùng raw body.
JSON.stringify(req.body)làm đổi thứ tự key và khoảng trắng, chữ ký sẽ luôn sai. SDK phát hiện và báo lỗi rõ ràng thay vì để bạn ngồi đoán.
Express
import express from 'express';
import { gpmpayWebhook } from '@gpmpay/sdk/webhooks';
app.post(
'/webhooks/gpmpay',
express.raw({ type: 'application/json' }), // ← bắt buộc
gpmpayWebhook({
secret: process.env.GPMPAY_WEBHOOK_SECRET!,
onEvent: async (event) => {
const code = /DH(\d+)/.exec(event.payload.content)?.[0];
if (code) await doiSoat(code, event.payload.transferAmount);
},
}),
);Nếu express.json() đã chạy toàn cục, bắt raw body bằng hook verify:
app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));Next.js (App + Pages Router), Fastify, Hono / Cloudflare Workers / Deno, và framework bất kỳ: docs/vi/03-webhooks.md §4.
Đăng ký endpoint và lấy secret
const { setting, secret } = await client.webhookSettings.createHmacEndpoint({
url: 'https://shop.example.com/webhooks/gpmpay',
});
console.log(secret); // ← chỉ hiện MỘT LẦN, lưu ngay vào GPMPAY_WEBHOOK_SECRETRetry và idempotency
GPM Pay huỷ delivery sau 5 giây và thử lại theo lịch 10s → 30s → 2m → 10m → 1h → 6h, tối đa 6 lần.
- Trả
200nhanh, xử lý sau (gpmpayWebhookmặc định làm vậy —respondEarly: true). - Endpoint của bạn phải idempotent theo
payload.id— cùng một giao dịch có thể tới nhiều lần.
import { WEBHOOK_RETRY_SCHEDULE_SECONDS, WEBHOOK_MAX_ATTEMPTS } from '@gpmpay/sdk/webhooks';Payload
event.payload.id // id giao dịch — DÙNG LÀM KHOÁ IDEMPOTENCY
event.payload.content // nội dung chuyển khoản, cắt còn 100 ký tự — MÃ CỦA BẠN Ở ĐÂY
event.payload.transferAmount // number, không phải string
event.payload.referenceCode // mã giao dịch CỦA NGÂN HÀNG — không phải mã đơn của bạnĐủ 11 field, kèm chỗ dễ nhầm giữa content và referenceCode: docs/vi/03-webhooks.md §2.
VietQR
Toàn bộ phần này chạy thuần client, không gọi mạng — backend không có endpoint VietQR nào.
import {
buildPaymentInstructions,
buildVietQrPayload,
buildVietQrImageUrl,
} from '@gpmpay/sdk/vietqr';
const account = await client.bankAccounts.retrieve(bankAccountId);
// Cách gọn nhất: một lần gọi ra đủ thứ trang thanh toán cần.
const info = buildPaymentInstructions({
bankAccount: account, // phải kèm quan hệ `bank` (BIN nằm trong đó)
amount: 250_000,
transferContent: 'DH1042', // MÃ CỦA BẠN — tự sinh, tự đối soát
});
// → { qrPayload, qrImageUrl, amount, transferContent, bankName, bankBin, accountNumber, accountName }
// Hoặc dựng từng phần nếu bạn đã có sẵn BIN và số tài khoản:
buildVietQrPayload({ bankBin: '970422', accountNumber: '1234567890', amount: 250_000, description: 'DH1042' });
buildVietQrImageUrl({ bankBin: '970422', accountNumber: '1234567890', amount: 250_000, description: 'DH1042' });⚠️ Mã đối soát là của bạn. GPM Pay không sinh mã nào cả. Hãy chọn mã ngắn, không dấu, và đặt ở đầu nội dung chuyển khoản — VietQR cắt phần mô tả còn 25 ký tự, và một số ngân hàng còn chèn thêm tiền tố của riêng họ vào
content. Nên dò bằng regex thay vì so bằng===.
CLI
gpmpay ping Kiểm tra token + probe từng scope xem có thật không
gpmpay accounts list Liệt kê tài khoản ngân hàng — nguồn của --account
gpmpay accounts get <id>
gpmpay transactions list [--limit <n>] [--account <uuid>] [--type IN|OUT]
gpmpay simulate tx --account <uuid> --amount <vnd> --content <text>
gpmpay webhook send --url <url> Ký payload mẫu rồi POST vào handler của bạn
gpmpay webhook listen [--port 4444] [--secret <s>]
gpmpay webhook verify --signature "t=..,v1=.." [--file body.json]
gpmpay webhook settings Endpoint đã đăng ký + chế độ xác thực của từng cái
gpmpay webhook history Lịch sử giao, mã lỗi, số lần thử
gpmpay webhook retry <id> Đẩy lại một lần giao thất bại
--token --sandbox --json --no-color -h -vExit code: 0 OK · 1 lỗi chung · 2 sai cú pháp / thiếu token · 3 xác thực thất bại (401) · 4 mạng/timeout. --json tự che mọi trường secret.
Test luồng mà không cần tiền thật
npx gpmpay accounts list # copy một uuid ra
npx gpmpay webhook listen --port 4444 --secret $GPMPAY_WEBHOOK_SECRET
# terminal khác — bắn một event đã ký vào handler của bạn.
# Không cần API token, không gọi API GPM Pay:
npx gpmpay webhook send --url http://localhost:4444 --secret $GPMPAY_WEBHOOK_SECRET
# handler phải TỪ CHỐI hai lệnh này:
npx gpmpay webhook send --url http://localhost:4444 --secret $GPMPAY_WEBHOOK_SECRET --bad-signature
npx gpmpay webhook send --url http://localhost:4444 --secret $GPMPAY_WEBHOOK_SECRET --skew 600Muốn chính GPM Pay bắn webhook thật (thay vì CLI giả lập) thì dùng simulate trên sandbox:
npx gpmpay simulate tx --sandbox --account <uuid> --amount 50000 --content DH1042
npx gpmpay webhook history --sandbox # xem đã giao chưa, mã lỗi là gì
simulatetừ chối chạy trên production trừ khi truyền--allow-production. Webhook chỉ bắn cho endpoint bậtfireOnSimulated— kiểm tra bằnggpmpay webhook settings.
Xử lý lỗi
GpmPayError
├── GpmPayConfigError lỗi cục bộ, chưa gọi mạng
├── GpmPayConnectionError DNS/TCP/TLS (.syscallCode)
├── GpmPayTimeoutError (.timeoutMs)
├── GpmPayWebhookSignatureError (.reason)
└── GpmPayAPIError (.status, .requestId, .rawBody)
├── GpmPayBadRequestError 400 (.validationMessages)
├── GpmPayAuthenticationError 401 (.reason)
├── GpmPayPermissionError 403 (.missingScope, .reason)
├── GpmPayNotFoundError 404 (.resource)
├── GpmPayRateLimitError 429 (.retryAfterSeconds)
└── GpmPayServerError 5xxGpmPayPermissionError.reason phân biệt ba kiểu 403:
| .reason | Nghĩa |
|---|---|
| 'scope' | Token hợp lệ nhưng thiếu scope — xem .missingScope |
| 'endpoint' | Route này dashboard-only, không scope nào mở được |
| 'ownership' | Tài nguyên tồn tại nhưng thuộc tài khoản khác |
try {
await client.webhookSettings.create({ ... });
} catch (error) {
if (error instanceof GpmPayPermissionError) {
console.error('403:', error.reason, error.missingScope);
}
}Mọi GpmPayAPIError đều mang .requestId — dán vào ticket hỗ trợ để tra log server. Nếu lẫn bản CJS và ESM trong cùng tiến trình, instanceof có thể sai; dùng GpmPayError.isGpmPayError(error).
Kiểu dữ liệu — 3 điểm cần nhớ
| | Đọc về | Gửi đi |
|---|---|---|
| Tiền | string ("50000", do Prisma Decimal) | number nguyên (50000) |
| Thời gian | ISO string | string \| Date |
| Enum | string-literal union, không phải TS enum | |
import { toVnd, formatVnd } from '@gpmpay/sdk';
toVnd(transaction.amount); // 50000
formatVnd(transaction.amount); // '50.000 ₫'Phân trang: limit bị API giới hạn tối đa 50, SDK tự clamp và cảnh báo một lần. Cần duyệt hết thì dùng transactions.listAll() — nó tự đi từng trang.
Sandbox & testing
const client = new GpmPay({ apiToken, sandbox: true });
// Trả envelope, không phải Transaction trần.
const { transaction, historyIds } = await client.simulator.createTransaction({
bankAccountId,
amount: 50_000,
transferContent: 'DH1042', // đúng mã bạn sẽ đối soát
});
// historyIds rỗng = chưa endpoint nào bật fireOnSimulated, handler sẽ không được gọi.
console.log(transaction.id, historyIds.length);simulator từ chối chạy trên production trừ khi truyền { allowOnProduction: true }. Trong unit test thì inject fetch (new GpmPay({ apiToken, fetch: myMockFetch })) thay vì gọi mạng thật — chi tiết ở docs/vi/04-errors-and-testing.md.
Chuyển từ fetch thô
| Trước | Sau |
|---|---|
| JSON.parse(res).data.data + .meta | client.transactions.list() → { data, meta } |
| if (res.status === 403) { ... } | catch (e) { if (e instanceof GpmPayPermissionError) ... } |
| Tự viết HMAC verify | constructWebhookEvent() |
| Tự nối chuỗi EMVCo + CRC16 | buildVietQrPayload() |
| Number(tx.amount) rải rác | toVnd(tx.amount) |
Tài liệu
Mọi file dưới đây ship kèm package (có sẵn trong node_modules/@gpmpay/sdk/) và
đọc được ngay trên web mà không cần cài gì:
| Tài liệu | Nội dung |
|---|---|
| docs/vi/01-getting-started.md | Từ 0 đến khoản thanh toán đầu tiên |
| docs/vi/02-payments.md | Mô hình tự đối soát, VietQR, các bẫy khớp mã |
| docs/vi/03-webhooks.md | Đăng ký, 3 chế độ xác thực, Express/Next/Fastify/Hono, retry, debug |
| docs/vi/04-errors-and-testing.md | Cây lỗi, retry, sandbox, unit test |
| docs/vi/05-api-reference.md | Mọi option, method, kiểu dữ liệu |
| AGENTS.md | Chỉ dẫn cho AI coding agent |
| examples/ | Ví dụ chạy được |
| docs/en/ | Toàn bộ nội dung trên, bản tiếng Anh |
Duyệt toàn bộ file của package: https://unpkg.com/browse/@gpmpay/sdk/ · Trang docs: https://app.gpmpay.com/docs#nodejs-sdk. Cần markdown thô (cho AI agent, curl, script) thì đổi unpkg.com/browse/ thành cdn.jsdelivr.net/npm/.
Tương thích
- Node >= 18.17 (cần
fetch,AbortSignal.timeout,node:util.parseArgs) - ESM và CommonJS đều dùng được
- TypeScript: khai báo kiểu đi kèm, không cần
@types/* - Không hỗ trợ trình duyệt — API token là secret phía server
License
MIT © GPM Softwares
