# 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)
