npm package: @qrismu/sdk
Description: Official Node.js SDK for QRISMU Payment Gateway
Author: QRISMU
Latest version: 1.1.0

# @qrismu/sdk

Official Node.js SDK for the [QRISMU](https://qrismu.app) QRIS Payment Gateway.

Zero dependencies. Pure Node.js built-ins. Works with Node.js >= 14.

---

## Installation

```bash
npm install @qrismu/sdk
```

---

## Quick Start

```js
const QrismuClient = require('@qrismu/sdk');

const qrismu = new QrismuClient({
  apiKey:    'QRMU_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  secretKey: 'QRMUS_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
});

// Create a transaction
const tx = await qrismu.createTransaction({
  order_id:       'ORDER-001',
  amount:         50000,
  customer_name:  'Budi Santoso',
  customer_email: 'budi@example.com',
});

console.log(tx.qris_url);       // Payment page URL
console.log(tx.transaction_id); // QRM-20260305-000001
```

---

## Configuration

| Option      | Type     | Required | Default                              | Description                        |
|-------------|----------|----------|--------------------------------------|------------------------------------|
| `apiKey`    | `string` | Yes      | —                                    | Public API key (`QRMU_live_…`)     |
| `secretKey` | `string` | Yes      | —                                    | Secret API key (`QRMUS_…`)         |
| `baseUrl`   | `string` | No       | `https://api.qrismu.app/api/v1`      | Override API base URL              |
| `timeout`   | `number` | No       | `30000`                              | Request timeout in ms              |

---

## API Reference

### Transactions

#### `createTransaction(params)` → `Promise<Object>`

Create a new QRIS payment transaction.

| Parameter         | Type     | Required | Description                                        |
|-------------------|----------|----------|----------------------------------------------------|
| `order_id`        | `string` | Yes      | Unique order ID from your system                   |
| `amount`          | `number` | Yes      | Amount in IDR (integer, min 1000)                  |
| `customer_name`   | `string` | No       | Customer full name                                 |
| `customer_email`  | `string` | No       | Customer email address                             |
| `customer_phone`  | `string` | No       | Customer phone number                              |
| `expiry_minutes`  | `number` | No       | Expiry in minutes (1–1440, default: 5)             |
| `description`     | `string` | No       | Transaction description                            |
| `webhook_url`     | `string` | No       | Webhook URL override for this transaction          |
| `redirect_url`    | `string` | No       | Redirect URL after successful payment              |

```js
const tx = await qrismu.createTransaction({
  order_id: 'INV-2026-001',
  amount:   150000,
});
// tx.qris_url, tx.qris_base64, tx.expiry, tx.transaction_id, tx.fee, ...
```

---

#### `getTransaction(transactionId)` → `Promise<Object>`

Get a single transaction by its QRISMU ID.

```js
const tx = await qrismu.getTransaction('QRM-20260305-000001');
console.log(tx.status); // pending | paid | expired | failed
```

---

#### `listTransactions(params?)` → `Promise<Object>`

List transactions with optional filters.

| Parameter    | Type     | Description                                      |
|--------------|----------|--------------------------------------------------|
| `page`       | `number` | Page number (default: 1)                         |
| `limit`      | `number` | Items per page (default: 20, max: 100)           |
| `status`     | `string` | Filter: `pending` \| `paid` \| `expired` \| `failed` |
| `start_date` | `string` | ISO 8601 start date (inclusive)                  |
| `end_date`   | `string` | ISO 8601 end date (inclusive)                    |

```js
const result = await qrismu.listTransactions({ status: 'paid', limit: 50 });
// result.transactions → array
// result.pagination  → { page, limit, total, total_pages }
```

---

#### `pollTransaction(transactionId, opts?)` → `Promise<Object>`

Poll a transaction until it reaches a terminal status (`paid`, `expired`, or `failed`), or until the timeout is exceeded.

| Option       | Type     | Default   | Description               |
|--------------|----------|-----------|---------------------------|
| `intervalMs` | `number` | `3000`    | Polling interval in ms    |
| `timeoutMs`  | `number` | `600000`  | Total timeout in ms       |

```js
const tx = await qrismu.pollTransaction('QRM-20260305-000001', {
  intervalMs: 5000,
  timeoutMs:  120000,
});
console.log(tx.status); // paid
```

---

### Withdrawals

#### `createWithdrawal(params)` → `Promise<Object>`

Request a withdrawal to a registered bank account.

| Parameter         | Type     | Required | Description                                 |
|-------------------|----------|----------|---------------------------------------------|
| `amount`          | `number` | Yes      | Amount to withdraw in IDR                   |
| `bank_account_id` | `string` | Yes      | ID of the destination bank account          |
| `notes`           | `string` | No       | Optional notes for this withdrawal          |

```js
const withdrawal = await qrismu.createWithdrawal({
  amount:          500000,
  bank_account_id: 'ba_xxxx',
  notes:           'Penarikan mingguan',
});
```

---

#### `listWithdrawals(params?)` → `Promise<Object>`

List withdrawals with optional filters.

| Parameter    | Type     | Description                                                        |
|--------------|----------|--------------------------------------------------------------------|
| `page`       | `number` | Page number (default: 1)                                           |
| `limit`      | `number` | Items per page (default: 20, max: 100)                             |
| `status`     | `string` | Filter: `pending` \| `processing` \| `completed` \| `rejected`    |
| `start_date` | `string` | ISO 8601 start date                                                |
| `end_date`   | `string` | ISO 8601 end date                                                  |

```js
const result = await qrismu.listWithdrawals({ status: 'completed' });
```

---

#### `getWithdrawal(withdrawalId)` → `Promise<Object>`

```js
const withdrawal = await qrismu.getWithdrawal('WD-20260305-000001');
```

---

### Bank Accounts

#### `listBankAccounts()` → `Promise<Object[]>`

List all registered bank accounts / e-wallets for your merchant account.

```js
const accounts = await qrismu.listBankAccounts();
```

---

#### `createBankAccount(params)` → `Promise<Object>`

Register a new bank account or e-wallet for withdrawals.

| Parameter        | Type     | Required | Description                                               |
|------------------|----------|----------|-----------------------------------------------------------|
| `bank_name`      | `string` | Yes      | Bank or e-wallet name (e.g. `"BCA"`, `"DANA"`, `"OVO"`)  |
| `account_number` | `string` | Yes      | Account number or e-wallet phone number                   |
| `account_name`   | `string` | Yes      | Account holder full name                                  |

```js
const account = await qrismu.createBankAccount({
  bank_name:      'BCA',
  account_number: '1234567890',
  account_name:   'Budi Santoso',
});
```

---

#### `deleteBankAccount(bankAccountId)` → `Promise<Object>`

```js
await qrismu.deleteBankAccount('ba_xxxx');
```

---

### Profile & Balance

#### `getProfile()` → `Promise<Object>`

```js
const profile = await qrismu.getProfile();
```

---

#### `updateProfile(params)` → `Promise<Object>`

| Parameter       | Type     | Description          |
|-----------------|----------|----------------------|
| `business_name` | `string` | Merchant / business name |
| `business_type` | `string` | Business type / category |
| `address`       | `string` | Business address     |
| `phone`         | `string` | Contact phone        |
| `website`       | `string` | Website URL          |

```js
const updated = await qrismu.updateProfile({ business_name: 'Toko Budi' });
```

---

#### `getBalance()` → `Promise<Object>`

Returns current balance and fee configuration.

```js
const bal = await qrismu.getBalance();
// bal.balance, bal.fee_percentage, bal.fee_flat
```

---

#### `getDashboard()` → `Promise<Object>`

Returns a summary for your merchant dashboard: balance, recent transactions, and stats.

```js
const dash = await qrismu.getDashboard();
```

---

### Settings

#### `getWebhookConfig()` → `Promise<Object>`

```js
const config = await qrismu.getWebhookConfig();
// config.webhook_url, config.webhook_enabled, config.webhook_secret
```

---

#### `updateWebhookConfig(params)` → `Promise<Object>`

| Parameter         | Type      | Description                          |
|-------------------|-----------|--------------------------------------|
| `webhook_url`     | `string`  | HTTPS URL to receive webhook events  |
| `webhook_enabled` | `boolean` | Enable or disable webhook delivery   |

```js
await qrismu.updateWebhookConfig({
  webhook_url:     'https://yourapp.com/webhooks/qrismu',
  webhook_enabled: true,
});
```

---

#### `listWhitelistIPs()` → `Promise<Object[]>`

```js
const ips = await qrismu.listWhitelistIPs();
```

---

#### `addWhitelistIP(params)` → `Promise<Object>`

When at least one entry exists, requests from non-whitelisted IPs are rejected with `403`.

| Parameter | Type     | Required | Description                                   |
|-----------|----------|----------|-----------------------------------------------|
| `ip`      | `string` | Yes      | IPv4 address or CIDR range (e.g. `10.0.0.0/24`) |
| `label`   | `string` | No       | Human-readable label                          |

```js
await qrismu.addWhitelistIP({ ip: '203.0.113.5', label: 'Production Server' });
```

---

#### `removeWhitelistIP(whitelistId)` → `Promise<Object>`

```js
await qrismu.removeWhitelistIP('wl_xxxx');
```

---

#### `regenerateApiKey()` → `Promise<Object>`

Generates a new API key pair. **The old API key is invalidated immediately.**

```js
const keys = await qrismu.regenerateApiKey();
// keys.api_key, keys.secret_key
```

---

### Webhook Verification

QRISMU signs every webhook delivery with `HMAC-SHA256(webhookSecret, rawBody)` and sends the hex digest in the `X-Webhook-Signature` header.

#### `QrismuClient.verifyWebhook(opts)` — static

| Parameter       | Type     | Description                                    |
|-----------------|----------|------------------------------------------------|
| `signature`     | `string` | Value of `X-Webhook-Signature` header          |
| `rawBody`       | `string` | Raw request body as a string (not parsed JSON) |
| `webhookSecret` | `string` | Your webhook secret from Settings > Webhook    |

```js
// Express example
app.post('/webhooks/qrismu', express.raw({ type: 'application/json' }), (req, res) => {
  const valid = QrismuClient.verifyWebhook({
    signature:     req.headers['x-webhook-signature'],
    rawBody:       req.body.toString(),
    webhookSecret: process.env.QRISMU_WEBHOOK_SECRET,
  });

  if (!valid) return res.status(401).send('Invalid signature');

  const event = JSON.parse(req.body.toString());
  if (event.event === 'payment.success') {
    // fulfill the order
  }

  res.sendStatus(200);
});
```

#### Instance method alias

```js
// Falls back to your API secretKey if webhookSecret is omitted
const valid = qrismu.verifyWebhook({
  signature: req.headers['x-webhook-signature'],
  rawBody:   req.body.toString(),
});
```

---

### Webhook Payload

```json
{
  "event": "payment.success",
  "transaction_id": "QRM-20260305-000001",
  "order_id": "ORDER-001",
  "amount": 50000,
  "fee": 350,
  "net_amount": 49650,
  "status": "paid",
  "paid_at": "2026-03-05T10:30:00.000Z",
  "customer_name": "Budi Santoso",
  "customer_email": "budi@example.com"
}
```

Retry policy: 3 attempts at 0 s, 30 s, and 5 min. Any `2xx` response is treated as success.

---

## Error Handling

All failed requests throw a `QrismuError`.

```js
const { QrismuError } = require('@qrismu/sdk');

try {
  await qrismu.createTransaction({ order_id: 'ORD-1', amount: 50000 });
} catch (err) {
  if (err instanceof QrismuError) {
    console.error(err.message); // Human-readable message
    console.error(err.code);    // HTTP status code
    console.error(err.raw);     // Full response body or null
  }
}
```

| Code | Meaning                                       |
|------|-----------------------------------------------|
| 400  | Validation error / bad request                |
| 401  | Invalid or missing API key / signature        |
| 403  | IP not whitelisted                            |
| 404  | Resource not found                            |
| 409  | Duplicate `order_id`                          |
| 429  | Rate limit exceeded (100 req/min per API key) |
| 500  | Internal server error                         |
| 408  | Request timed out (client-side)               |
| 0    | Network error (no HTTP response)              |

---

## License

MIT © [QRISMU](https://qrismu.app)