Key Takeaway
Bank Indonesia's SNAP standard, set by PADG 23/15/PADG/2021 on 16 August 2021, mandates uniform request schemas, authentication flows and error codes across Indonesian banks and payment processors. For developers that replaces per-bank integration guesswork with one contract, published on ASPI's SNAP Developer Site, including the MPM services that generate and pay dynamic QRIS codes.
Bank Indonesia's SNAP (Standar Nasional Open API Pembayaran) standard has fundamentally changed how fintech developers integrate with Indonesian banks and payment processors. Set by PADG 23/15/PADG/2021, issued on 16 August 2021, SNAP mandates uniform request schemas, authentication flows, and error codes across all licensed Payment Service Providers (PJP), replacing dozens of proprietary bank APIs with a single standard. Combined with QRIS Cross-Border (QRIS Antarnegara), which Bank Indonesia lists as in use in Thailand, Malaysia, Singapore, Japan, South Korea and China, Indonesia's open banking ecosystem is now one of the most comprehensive in Southeast Asia.
Understanding SNAP: Scope and API Categories
PADG 23/15/PADG/2021 defines SNAP's scope as interconnection and interoperability, information-system security, governance and risk management for payment APIs, and applies its technical specifications to five API categories: registration, balance information, transaction history, credit transfer and debit transfer, plus any further category Bank Indonesia sets. In the specification ASPI publishes, credit transfer covers intrabank and interbank transfers, virtual accounts and QRIS merchant-presented (MPM) payments, while debit transfer covers direct debit and customer-presented (CPM) QR payments. ASPI has managed the standard and its Developer Site since 1 September 2023.
SNAP Payment APIs in Detail
The most commonly integrated SNAP endpoints, as ASPI's specification names them, are: `/v1.0/access-token/b2b` (B2B access token), `/v1.0/transfer-interbank` (interbank credit transfer), `/v1.0/transfer-va/create-va` (virtual account creation), `/v1.0/transfer-va/inquiry-va` (VA inquiry), and `/v1.0/debit/payment-host-to-host` (direct debit). Each bank implementing SNAP exposes these services with the same request/response schemas, differing only in the base URL, the path prefix in front of the version, and the required partner credentials.
QRIS Cross-Border (QRIS Antarnegara) Integration
QRIS Cross-Border, QRIS Antarnegara in Indonesian, is Bank Indonesia's name for cross-border QRIS payments. Indonesian travellers scan partner-country QR codes at participating merchants abroad and, where the linkage runs both ways, foreign visitors pay Indonesian QRIS merchants from their home apps. Bank Indonesia lists it in use in Thailand (PromptPay), Malaysia (DuitNow QR), Singapore (NETS QR), Japan (JPQR Global), South Korea and China (UnionPay), with the same Rp10,000,000 per-transaction limit as domestic QRIS. Currency conversion happens inside the participating payment apps: the payer is debited in their own currency and an Indonesian merchant is notified in rupiah, so a merchant integration needs no exchange-rate step of its own.
Generate a fresh numeric `X-EXTERNAL-ID` for every SNAP call. ASPI's specification requires it to be unique within the same day and answers a reused value with HTTP 409 Conflict (case code 00), so it is a replay guard, not a retry key. Idempotency for the transfer itself rides on `partnerReferenceNo`: a provider that has already processed that reference answers with case code 01, Duplicate partnerReferenceNo, instead of creating a second transfer.
Authentication: OAuth 2.0 with Asymmetric Key Signing
SNAP authentication uses a two-layer security model: a client credential OAuth 2.0 flow for access token acquisition, and HMAC-SHA512 request signing for every subsequent API call. The access token request carries an asymmetric SHA256withRSA signature over your client ID and the X-TIMESTAMP value, made with your private key, to prove identity; the token it returns expires after 900 seconds. The per-request HMAC-SHA512 signature then uses the access token and request body hash to ensure message integrity.
Implementing HMAC-SHA512 Request Signing in Node.js
The SNAP signature algorithm requires constructing a `StringToSign` from the HTTP method, the endpoint's relative path, the access token, the SHA-256 hash of the minified request body (hex-encoded and lowercased), and the X-TIMESTAMP header value — joined with colons. This string is then signed with HMAC-SHA512 using the client secret as the key. The resulting base64-encoded signature is sent in the `X-SIGNATURE` header, and X-TIMESTAMP must follow the specification's yyyy-MM-ddTHH:mm:ssTZD format, such as 2026-10-12T14:30:00+07:00. The code block below shows a complete implementation.
// snap-api-client.js — Bank Indonesia SNAP API request with HMAC-SHA512
const crypto = require('crypto');
const axios = require('axios');
const CLIENT_ID = process.env.SNAP_CLIENT_ID;
const CLIENT_SECRET = process.env.SNAP_CLIENT_SECRET;
const PRIVATE_KEY = process.env.SNAP_PRIVATE_KEY; // PEM; the bank verifies with your public key
const BASE_URL = process.env.SNAP_BASE_URL; // each bank publishes its own sandbox host
const PREFIX = process.env.SNAP_PATH_PREFIX || ''; // bank-specific prefix in front of /v1.0
/**
* X-TIMESTAMP in the spec's yyyy-MM-ddTHH:mm:ssTZD form, here in WIB (+07:00)
*/
function snapTimestamp(date = new Date()) {
const wib = new Date(date.getTime() + 7 * 60 * 60 * 1000);
return wib.toISOString().slice(0, 19) + '+07:00';
}
/**
* X-EXTERNAL-ID: a numeric string of at most 36 characters, unique within the day
*/
function externalId() {
return Date.now().toString() + crypto.randomInt(0, 1e9).toString().padStart(9, '0');
}
/**
* Get a B2B access token (OAuth 2.0 client credentials, valid for 900 seconds)
*/
async function getAccessToken() {
const timestamp = snapTimestamp();
const signature = crypto
.createSign('RSA-SHA256') // SHA256withRSA over client_ID|X-TIMESTAMP
.update(`${CLIENT_ID}|${timestamp}`)
.sign(PRIVATE_KEY, 'base64');
const resp = await axios.post(`${BASE_URL}${PREFIX}/v1.0/access-token/b2b`, {
grantType: 'client_credentials'
}, {
headers: {
'X-CLIENT-KEY': CLIENT_ID,
'X-TIMESTAMP': timestamp,
'X-SIGNATURE': signature,
'Content-Type': 'application/json'
}
});
return resp.data.accessToken;
}
/**
* Interbank credit transfer (SNAP service code 18)
*/
async function transferCredit(token, payload) {
const path = `${PREFIX}/v1.0/transfer-interbank`;
const timestamp = snapTimestamp();
const body = JSON.stringify(payload); // minified; sign exactly the bytes you send
const bodyHash = crypto.createHash('sha256').update(body).digest('hex').toLowerCase();
const stringToSign = `POST:${path}:${token}:${bodyHash}:${timestamp}`;
const signature = crypto.createHmac('sha512', CLIENT_SECRET).update(stringToSign).digest('base64');
return axios.post(`${BASE_URL}${path}`, body, {
headers: {
Authorization: `Bearer ${token}`,
'X-TIMESTAMP': timestamp,
'X-SIGNATURE': signature,
'X-PARTNER-ID': CLIENT_ID,
'X-EXTERNAL-ID': externalId(),
'CHANNEL-ID': '95221',
'Content-Type': 'application/json'
}
});
}
// Usage
(async () => {
const token = await getAccessToken();
const result = await transferCredit(token, {
partnerReferenceNo: 'TXN-2026-001',
amount: { value: '100000.00', currency: 'IDR' },
beneficiaryAccountName: 'Budi Santoso',
beneficiaryBankCode: '014', // BCA
beneficiaryAccountNo: '1234567890',
sourceAccountNo: '9876543210',
transactionDate: snapTimestamp()
});
console.log('Transfer status:', result.data.responseCode); // 2001800 = success
})();Private Key Management and Key Rotation
SNAP's asymmetric signature uses an RSA key pair: you sign with your private key and each bank you integrate with verifies the signature with your public key. Store the private key and client secrets in your secrets manager (AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault) — never commit them to source code. Where a key or certificate carries an expiry date, implement automated expiry monitoring with alerting at 60 and 30 days before expiration. When rotating keys, test the new key in the bank's sandbox environment at least 2 weeks before the old one is retired.
QRIS Merchant Integration and Dynamic QR Generation
For applications requiring dynamic QRIS generation (amount pre-filled in the QR code), use the SNAP Generate QR MPM service, `/v1.0/qr/qr-mpm-generate`. Its response returns the QR string in `qrContent` (or a `qrUrl` or `qrImage`), and the request's optional `validityPeriod` field sets when the code stops being valid; how long a code may live is up to each provider. Always display the merchant name, amount, and a unique transaction reference alongside the QR code so users can verify the transaction before scanning.
Handling SNAP Error Codes and Retry Logic
Every SNAP response code has seven digits: the HTTP status, a two-digit service code and a two-digit case code. Interbank transfer is service code 18, so `2001800` is success, `4001801` an invalid field format (do not retry), `4091800` an X-EXTERNAL-ID already used that day, `4091801` a partnerReferenceNo that was already processed (look up the original result), `5001801` an internal server error (retry with exponential backoff), and `5041800` a timeout from the issuer (query transaction status before retrying). Always implement a transaction status inquiry call before initiating a retry to avoid duplicate transfers, which are extremely difficult to reverse in the Indonesian interbank system.
SNAP sandbox and production environments are not just different base URLs — they also enforce different IP whitelisting rules. Your production server's egress IPs must be registered with each bank's API gateway team before going live. Sandbox often uses a shared allowlist, but production strictly rejects unapproved IPs with a 403 response. Failing to register IPs is the most common cause of production launch delays for SNAP integrations.
Testing and Certification Process
Before connecting to a bank's SNAP production environment, partners complete a testing process in that bank's sandbox, demonstrating correct implementation of the endpoints they use, proper error handling, idempotency behavior, and security controls. How long it takes varies by bank. To check an implementation against the specification itself, the SNAP Developer Site that ASPI has run since 1 September 2023 includes an online testing application for registered users.
Webhook Notification Handling (H2H Integration)
Most SNAP banks support Host-to-Host (H2H) webhook notifications for asynchronous transaction completion events. Configure your webhook endpoint to be idempotent — the same notification may be delivered multiple times. Acknowledge each notification promptly with an HTTP 200 and the response body the bank's specification defines; the acknowledgement timeout and retry schedule vary by bank, so take them from each bank's documentation rather than assuming one.
Rate Limiting and Circuit Breakers
SNAP's response-code table defines HTTP 429 Too Many Requests (case code 00) for a partner that exceeds its maximum transaction limit, but the limits themselves are set by each provider, so ask for yours during onboarding. Implement a token bucket rate limiter in your integration layer to smooth out traffic spikes, and build circuit breakers to automatically fall back to alternative payment channels when a SNAP provider is degraded.
Integrate SNAP API metrics — including signature verification failures, timeout rates, and error code distributions — into your observability platform. A sudden spike in codes starting with 401 (authorization errors such as Invalid Token) often points to a token-refresh or key-rotation problem, while rising codes starting with 504 (timeouts from the issuer) from a specific bank are an early warning of their infrastructure issues before they formally announce maintenance.
Production Go-Live Checklist
Before going live with your SNAP integration, verify: production IP addresses are whitelisted with all integrated banks, private keys and client secrets are stored in a secrets manager with expiry monitoring, webhook endpoints are deployed with idempotency logic and acknowledge within each bank's timeout, rate limiters and circuit breakers are configured and tested, transaction status inquiry fallback logic is implemented for all transfer endpoints, and your monitoring dashboards are live with alerts for error rate and latency thresholds.
Building a SNAP Integration Test Suite
A robust SNAP test suite should cover: (1) successful happy-path flows for each endpoint; (2) replay protection (a reused X-EXTERNAL-ID on the same day must be rejected with a 409 Conflict, and a repeated partnerReferenceNo must not create a second transaction); (3) expired access token handling; (4) X-TIMESTAMP formatting and the clock-skew tolerance your bank enforces; (5) invalid signature rejection; and (6) all defined SNAP error codes for your integrated endpoints. Mock the SNAP bank endpoints locally using tools like WireMock or Mockoon to enable fast CI pipeline execution without sandbox rate limits.
Key terms in this article include SNAP, QRIS, HMAC-SHA512, and PJP.
Related Articles
- DevSecOps: Building a Secure CI/CD Pipeline
- Microservices vs. Monolith: Choosing the Right Architecture
Sources
- Bank Indonesia — PADG 23/15/PADG/2021 tentang Standar Nasional Open API Pembayaran
- Bank Indonesia — Mulai 1 September 2023 Pengelolaan SNAP Dilakukan oleh ASPI
- ASPI SNAP Developer Site — Security standard: access token and signatures
- ASPI SNAP Developer Site — Virtual Account specification and response codes
- ASPI SNAP Developer Site — Interbank transfer specification
- ASPI SNAP Developer Site — QRIS MPM (Generate QR) specification
- Bank Indonesia — QRIS Antarnegara (QRIS Cross-Border)

