binderpay-nodejs
v1.0.0
Published
Official BinderPay SNAP API SDK for Node.js (Virtual Account & QRIS)
Maintainers
Readme
binderpay-nodejs
Official BinderPay SNAP API SDK for Node.js — Virtual Account & QRIS.
Implements RSA-SHA256 (SNAP Bank Indonesia) signature automatically on every request and provides callback verification helpers.
Installation
npm install binderpay-nodejsRequires Node.js >= 18.0.0
Configuration
import { BinderPay } from 'binderpay-nodejs';
import * as fs from 'fs';
const client = new BinderPay({
partnerId: '170041', // X-PARTNER-ID
privateKey: fs.readFileSync('private.pem', 'utf8'), // RSA private key (PEM)
channelId: 'BCA', // CHANNEL-ID
isProduction: false, // default sandbox; true = https://api.binderpay.id
});| Option | Type | Default | Description |
| --- | --- | --- | --- |
| partnerId | string | — | Registered partner ID (required) |
| privateKey | string | Buffer | — | RSA private key PEM (required) |
| channelId | string | — | Bank channel code, e.g. BCA (required) |
| isProduction | boolean | false | false = sandbox, true = production |
| baseUrl | string | — | Override base URL |
Default base URLs: Sandbox https://api-sandbox.binderpay.id, Production https://api.binderpay.id.
Virtual Account
// Create VA (Service 27)
await client.virtualAccount.create({
customerNo: '000003212',
virtualAccountName: 'Chus Pandi',
trxId: 'INV-000000023212',
totalAmount: { value: '25000.00', currency: 'IDR' },
virtualAccountTrxType: 'C', // C | O | R
expiredDate: '2023-09-05T19:30:14+07:00',
additionalInfo: { channel: 'CIMB' },
});
// Inquiry active VA (Service 30)
await client.virtualAccount.inquiry({
trxId: 'INV-000000023212',
additionalInfo: { contractId: 'ci302a21c9' },
});
// VA payment status (Service 26)
await client.virtualAccount.status({
virtualAccountNo: '2269141693898987',
trxId: 'INV-000000023212',
additionalInfo: { contractId: 'ci302a21c9', channel: 'BCA' },
});
// Delete VA (Service 31)
await client.virtualAccount.delete({
trxId: 'INV-000000023212',
virtualAccountNo: '2269141693898987',
additionalInfo: { channel: 'BCA', contractId: 'ci302a21c9' },
});VA types: C (one-off), O (open recurring), R (close recurring).
Channels: BRI, BNI, MANDIRI, MANDIRIPC, PERMATA, BSI, MUAMALAT, BCA, CIMB, SINARMAS, BNC, MAYBANK.
QRIS
// Generate QRIS (Service 47)
await client.qris.generate({
partnerReferenceNo: 'INV-000000023212',
amount: { value: '45000.00', currency: 'IDR' },
validityPeriod: '2024-01-11T17:00:00+07:00', // required if isStatic = false
additionalInfo: { isStatic: false },
});
// Query status (Service 51)
await client.qris.query({
originalPartnerReferenceNo: 'INV-000000023212',
serviceCode: '47',
additionalInfo: { contractId: 'ci302a21c9' },
});
// Cancel (Service 77)
await client.qris.cancel({
originalPartnerReferenceNo: 'INV-000000023212',
reason: 'cancel order',
additionalInfo: { contractId: 'ci302a21c9' },
});Webhook / Callback Validation
Callbacks from BinderPay are sent with the X-TIMESTAMP, X-SIGNATURE, and X-PARTNER-ID headers. Verify the signature with the BinderPay public key (download from https://binderpay.id/docs/binderpay-public.pem, not your private key).
There are three separate path concepts:
- Merchant callback route: your application-owned route, for example
/api/binderpay/callback. - Signed callback path: the exact path BinderPay includes in the callback string-to-sign:
/v1.0/transfer-va/paymentfor VA or/v1.0/qr/qr-mpm-notifyfor QRIS. - Outbound API endpoint: an SDK request path such as
/v1.0/transfer-va/create-va.
import fs from 'fs';
import express from 'express';
import {
verifyCallbackSignature,
parseCallback,
successResponse,
} from 'binderpay-nodejs';
const app = express();
// Load BinderPay public key (PEM format) from file or environment variable
const binderpayPublicKey = fs.readFileSync('binderpay-public.pem', 'utf8');
// Use express.raw({ type: 'application/json' }) or keep rawBody
app.post('/api/binderpay/callback', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8'); // exactly as received; do not re-serialize
const valid = verifyCallbackSignature(
req.headers,
rawBody,
binderpayPublicKey,
);
if (!valid) {
return res.status(401).json({ message: 'Cannot verify signature' });
}
const callback = parseCallback(JSON.parse(rawBody)); // auto-detects VA or QRIS
// ... process payment idempotently ...
// Return the matching SNAP success code for the detected callback type.
return res.json(successResponse(callback.type));
});parseCallback() inspects the payload and validates the required fields of the detected type — a VA callback (has trxId) or a QRIS callback (has originalReferenceNo). It throws ValidationError with the detected type in the message when a required field is missing, and rejects payloads that match neither type. Use successResponse(callback.type) to acknowledge — it returns 2002500 for va and 2005200 for qris.
For a standard VA or QRIS callback, use the same verifyCallbackSignature(...) function. It automatically checks /v1.0/transfer-va/payment and /v1.0/qr/qr-mpm-notify, and handles case-insensitive headers and array values. For non-standard integrations, use verifyCallbackSignatureForPath(...) with an explicit path. You can also use validatePublicKey(publicKey) to validate public key presence and RSA format upfront.
Important:
rawBodymust be exactly as received by the server; do not decode and re-serialize it before verification.binderpayPublicKeyis strictly validated; passing an empty/missing key or invalid PEM throws aValidationError.- The merchant route remains application-owned; the unified helper selects the BinderPay signed callback path automatically.
- Replay prevention and idempotent transaction handling remain the merchant application's responsibility.
- Return HTTP 200 with the appropriate BinderPay response code after successful processing.
Error Handling
import { BinderPayError, ValidationError, SignatureError, TransportError } from 'binderpay-nodejs';
try {
await client.virtualAccount.create({...});
} catch (err) {
if (err instanceof ValidationError) { /* invalid input */ }
if (err instanceof TransportError) { /* network/connection failure */ }
if (err instanceof BinderPayError && err.responseCode === '4002701') { /* field format */ }
}Testing
npm test # Jest
npm run build # tsup (CJS + ESM + d.ts)License
This project is licensed under the MIT License - see the LICENSE file for details.
