@gpmpay/sdk
Version:
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
222 lines (159 loc) • 6.69 kB
Markdown
# Getting started
[Tiếng Việt](../vi/01-getting-started.md) · [Index](./README.md)
This guide takes you from nothing to a real, confirmed payment.
## How GPM Pay works
No gateway holds the money. The customer makes an ordinary bank transfer into
your account. GPM Pay watches that account and POSTs a webhook for **every**
incoming transaction, carrying the amount and the transfer content.
That is the whole product. Your job: put your own order code in the transfer
content, then match on it when the webhook arrives.
> **Reconciliation is yours.** GPM Pay mints no codes and holds no order state.
> Your own orders table stays the source of truth — see
> [the payment model](./02-payments.md#the-model).
## 1. Create an API token
Go to <https://app.gpmpay.com/api-tokens> → **Create token**.
| Scope | Needed for |
|---|---|
| `webhooks:manage` | creating / updating webhooks from code |
| `bank-accounts:read` | fetching `bankAccountId` and the bank BIN, `client.ping()` |
| `transactions:read` | listing transactions, periodic reconciliation |
Those three are the only scopes that exist. Minimum to get started:
`webhooks:manage` + `bank-accounts:read`.
> The token is shown **once**. Store it in an environment variable immediately.
```bash
# .env
GPMPAY_API_TOKEN=gpm_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyy
```
## 2. Install and verify the token
```bash
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:read
```
`ping` probes each scope with a real request, so the `Missing` line tells you
what the token actually lacks — before you discover it as a 403 in production.
Requires Node >= 18.17. No runtime dependencies.
Exit codes are script-friendly: `0` ok · `2` missing token / bad usage · `3`
token rejected (401) · `4` network failure.
## 3. Create a client
```ts
import { GpmPay } from '@gpmpay/sdk';
const client = new GpmPay({ apiToken: process.env.GPMPAY_API_TOKEN! });
```
Or read `GPMPAY_API_TOKEN` straight from the environment:
```ts
const client = GpmPay.fromEnv();
```
The constructor **throws immediately** if the token is missing or malformed —
before any request is sent. Do not wrap it in a try/catch that swallows the
error: a missing token should crash the app at boot, not at checkout.
```ts
new GpmPay({}); // GpmPayConfigError — missing_api_token
new GpmPay({ apiToken: 'sk_live_x' }); // GpmPayConfigError — invalid_api_token_format
```
> **Server-side only.** Never put the token in a `NEXT_PUBLIC_*` variable, a
> client component, or a mobile app. It has full access to your GPM Pay account.
## 4. Fetch the bank account
```ts
const account = (await client.bankAccounts.list({ status: 'ACTIVE' })).data[0]!;
account.id // used when simulating transactions in tests
account.accountNumber // used to build the QR
account.bank!.bin // NAPAS BIN, also for the QR
```
In practice you'll store these in config rather than fetching them every time.
## 5. Build the QR with your own code
```ts
import { buildPaymentInstructions } from '@gpmpay/sdk/vietqr';
const code = `DH${localOrder.id}`; // your code — max 25 chars, keep it A-Z0-9
const { qrPayload, qrImageUrl, transferContent } = buildPaymentInstructions({
bankAccount: account, // carries the bank BIN
amount: Math.round(localOrder.total), // VND, INTEGER
transferContent: code,
});
```
Pure local computation — no network call, so this cannot fail on a GPM Pay
outage. Store `code` on your local order.
Two things to get right:
- **The amount must be an integer number of VND.** A float from a currency
library produces a QR with a slightly different amount, and the transfer will
not match your order.
- **Pick a code that is easy to extract.** A fixed prefix plus a numeric id
(`DH123`) is plenty. Avoid diacritics, spaces, and punctuation — banks may
normalize the content differently.
## 6. Show it to the customer
```tsx
<img src={qrImageUrl} alt="Scan to pay" />
<p>Amount: {formatVnd(localOrder.total)}</p>
<p>Transfer content: <strong>{code}</strong></p>
```
If you render the QR yourself from `qrPayload`, any QR library works (`qrcode`,
`react-qr-code`, …).
Customers who scan the QR get the content pre-filled. Customers who transfer
manually need the code shown prominently with one-tap copy — in practice this
is where things go wrong most often.
## 7. Register a webhook
```ts
const { secret } = await client.webhookSettings.createHmacEndpoint({
url: 'https://shop.example.com/webhooks/gpmpay',
});
// store `secret` as GPMPAY_WEBHOOK_SECRET — shown once only
```
## 8. Receive and reconcile
```ts
import express from 'express';
import { gpmpayWebhook } from '@gpmpay/sdk/webhooks';
app.post(
'/webhooks/gpmpay',
express.raw({ type: 'application/json' }), // REQUIRED
gpmpayWebhook({
secret: process.env.GPMPAY_WEBHOOK_SECRET!,
onEvent: async (event) => {
const { id: transactionId, content, transferAmount, transferType } = event.payload;
if (transferType !== 'in') return;
const code = /DH(\d+)/.exec(content)?.[0];
if (!code) return;
const order = await db.orders.findByCode(code);
if (!order) return;
// NOTHING compares the amount for you.
if (order.total !== transferAmount) {
await flagUnderpayment(order, transferAmount);
return;
}
await fulfil(order, transactionId);
},
}),
);
```
The handler must be **idempotent on `event.payload.id`** — the same transaction
arrives more than once whenever your first response is slow or fails. Full
details: [Webhooks](./03-webhooks.md).
## 9. Test the whole flow without real money
```ts
const client = new GpmPay({ apiToken, sandbox: true });
await client.simulator.createTransaction({
bankAccountId: account.id,
amount: Math.round(localOrder.total),
transferContent: `DH${localOrder.id}`, // the same code you gave the customer
});
// the webhook fires immediately and the step-8 handler runs
```
`simulator` refuses to run against production unless you pass
`{ allowOnProduction: true }`.
To catch webhooks on your own machine:
```bash
npx gpmpay webhook listen --port 4444 --secret $GPMPAY_WEBHOOK_SECRET
ngrok http 4444
```
## Next
- [Payments & reconciliation](./02-payments.md) — choosing a code, matching pitfalls, VietQR
- [Webhooks end to end](./03-webhooks.md)
- [Errors & testing](./04-errors-and-testing.md)
- [API reference](./05-api-reference.md)