Indonesia SNAP API & QRIS 2.0: Developer Guide to Open Banking Integration

Photo by Unsplash

Photo by Unsplash
Bank Indonesia's SNAP (Standar Nasional Open API Pembayaran) standard has fundamentally changed how fintech developers integrate with Indonesian banks and payment processors. Introduced via PBI No. 22/1/PBI/2022, 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 2.0's cross-border payment capabilities with Thailand, Malaysia, and Singapore, Indonesia's open banking ecosystem is now one of the most comprehensive in Southeast Asia.
SNAP is structured into two implementation phases. Phase 1, which all PJPs must fully support, covers domestic transfers (intra-bank and inter-bank), virtual account creation and inquiry, QRIS merchant registration and transaction processing, direct debit, and balance inquiry. Phase 2, currently being rolled out, adds open finance capabilities including lending data APIs, digital identity verification (using Dukcapil NIK data), and investment account APIs.
The most commonly integrated SNAP Phase 1 endpoints are: `/snap/v1.0/access-token/b2b` (OAuth 2.0 token), `/snap/v1.0/transfer-interbank` (credit transfer), `/snap/v1.0/create-va` (virtual account creation), `/snap/v1.0/inquiry-va` (VA inquiry), and `/snap/v1.0/debit` (direct debit). Each bank implementing SNAP must expose these endpoints with identical request/response schemas, differing only in the base URL and required partner credentials.
QRIS 2.0 extends the domestic QRIS standard to enable cross-border QR payments. Indonesian consumers can now pay at QRIS-enabled terminals in Thailand (PromptPay QR), Malaysia (DuitNow QR), Singapore (PayNow QR), and vice versa. For developers building merchant POS or e-wallet apps, the cross-border QRIS flow adds a currency conversion step — your application must call the QRIS foreign exchange rate API to display the IDR equivalent before the user confirms payment.
Always implement idempotency keys in your SNAP API calls using the `X-EXTERNAL-ID` header. SNAP servers use this field to detect and deduplicate retried requests — sending the same external ID twice for a transfer will return the original transaction result, not create a duplicate transfer.
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 step uses an asymmetric signature (RSA-SHA256 with your P12 client certificate) to prove identity, while the per-request HMAC-SHA512 signature uses the access token and request body hash to ensure message integrity.
The SNAP signature algorithm requires constructing a `StringToSign` from the HTTP method, endpoint path, access token (lowercased), SHA-256 hash of the request body (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. 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 BASE_URL = 'https://api.sandbox.bri.co.id'; // sandbox endpoint
/**
* Generate SNAP access token (OAuth 2.0 client credentials)
*/
async function getAccessToken() {
const timestamp = new Date().toISOString();
const stringToSign = `${CLIENT_ID}|${timestamp}`;
const signature = crypto
.createHmac('sha512', CLIENT_SECRET)
.update(stringToSign)
.digest('hex');
const resp = await axios.post(`${BASE_URL}/snap/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;
}
/**
* Transfer credit (SNAP Phase 1 — inter-bank transfer)
*/
async function transferCredit(token, payload) {
const timestamp = new Date().toISOString();
const requestId = crypto.randomUUID();
const bodyHash = crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
const stringToSign = `POST:/snap/v1.0/transfer-interbank:${token}:${bodyHash.toLowerCase()}:${timestamp}`;
const signature = crypto.createHmac('sha512', CLIENT_SECRET).update(stringToSign).digest('base64');
return axios.post(`${BASE_URL}/snap/v1.0/transfer-interbank`, payload, {
headers: {
Authorization: `Bearer ${token}`,
'X-TIMESTAMP': timestamp,
'X-SIGNATURE': signature,
'X-PARTNER-ID': CLIENT_ID,
'X-EXTERNAL-ID': requestId,
'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' },
beneficiaryBankCode: '014', // BCA
beneficiaryAccountNo: '1234567890',
sourceAccountNo: '9876543210',
transactionDate: new Date().toISOString()
});
console.log('Transfer status:', result.data.responseCode);
})();SNAP partners receive a P12 client certificate from each bank they integrate with. Store these certificates in your secrets manager (AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault) — never commit them to source code. Certificates typically expire annually; implement automated expiry monitoring with alerting at 60 and 30 days before expiration. When rotating keys, test the new certificate in the bank's sandbox environment at least 2 weeks before the old one expires.
For applications requiring dynamic QRIS generation (amount pre-filled in the QR code), use the SNAP `/snap/v1.0/generate-qr` endpoint. Dynamic QR codes expire after a configurable TTL (typically 5 minutes for consumer payments, up to 24 hours for bills). Always display the merchant name, amount, and a unique transaction reference alongside the QR code so users can verify the transaction before scanning.
SNAP defines a standardized error code taxonomy: `4001XX` for input validation errors (do not retry), `4091XX` for conflict/duplicate errors (return original result), `5001XX` for partner system errors (retry with exponential backoff), and `5041XX` for bank processing timeouts (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.
Before connecting to a bank's SNAP production environment, partners must complete a certification process in the sandbox, demonstrating correct implementation of all mandatory endpoints, proper error handling, idempotency behavior, and security controls. The certification process typically takes 2–4 weeks per bank. Use the official SNAP test case matrix published by Bank Indonesia to prepare your test scripts.
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 with an HTTP 200 response containing the SNAP acknowledgment payload within 30 seconds, otherwise the bank's system will retry delivery with increasing intervals up to 24 hours.
SNAP production environments enforce rate limits per partner credential, typically ranging from 50 to 500 transactions per second depending on your PJP tier. Implement a token bucket rate limiter in your integration layer to smooth out traffic spikes. Bank Indonesia's SNAP SLA requires PJPs to maintain 99.5% availability for payment APIs — 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 `4011XX` (authentication errors) often indicates an imminent certificate expiry, while increasing `5041XX` (timeout) rates from a specific bank are an early warning of their infrastructure issues before they formally announce maintenance.
Before going live with your SNAP integration, verify: production IP addresses are whitelisted with all integrated banks, P12 certificates are stored in a secrets manager with expiry monitoring, webhook endpoints are deployed with idempotency logic and 30-second response SLA, 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.
A robust SNAP test suite should cover: (1) successful happy-path flows for each endpoint; (2) replay attack prevention (duplicate X-EXTERNAL-ID must return idempotent result); (3) expired access token handling; (4) clock skew tolerance (X-TIMESTAMP within ±15 seconds of server time); (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.