Skip to main content
BlogBackend

Indonesia SNAP API & QRIS Cross-Border: Developer Guide to Open Banking Integration

Bank Indonesia's SNAP (Standar Nasional Open API Pembayaran) is now mandatory for all licensed payment service providers. This guide covers OAuth 2.0 authentication, SHA256withRSA and HMAC-SHA512 request signing, SNAP's seven-digit response codes, QRIS Cross-Border payments, and practical Node.js implementation patterns for Indonesian fintech developers.

Matthews Wong

Software Engineer

December 202511 min read
Indonesia SNAP API & QRIS Cross-Border: Developer Guide to Open Banking Integration

Photo by Unsplash

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.

Sandbox vs. Production Endpoint and IP Whitelisting

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.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is SNAP and why is it mandatory for payment service providers in Indonesia?

SNAP (Standar Nasional Open API Pembayaran) is Bank Indonesia's open banking standard, set by PADG 23/15/PADG/2021 on 16 August 2021. It mandates uniform request schemas, authentication flows, and error codes across licensed Payment Service Providers (PJP), replacing dozens of proprietary bank APIs with a single standard. Its specifications cover five API categories: registration, balance information, transaction history, credit transfer and debit transfer.

How does SNAP's two-layer authentication model work?

SNAP uses an OAuth 2.0 client credential flow to acquire an access token, then HMAC-SHA512 request signing for every subsequent API call. The token request is signed with SHA256withRSA over your client ID and X-TIMESTAMP using your private key, and the token lasts 900 seconds. Each later call signs the HTTP method, relative path, access token, the lowercase hex SHA-256 of the minified body and X-TIMESTAMP with the client secret, and sends the result in the X-SIGNATURE header.

What is the correct way to handle retries and avoid duplicate transfers in SNAP?

Send a fresh numeric X-EXTERNAL-ID on every call, because ASPI's specification rejects a value reused on the same day with a 409 Conflict. Make the transfer itself idempotent with partnerReferenceNo, and call the transaction status inquiry before any retry, since duplicate interbank transfers are hard to reverse. Response codes are seven digits, HTTP status plus service code plus case code: a 400 with case code 01 is an invalid field and should not be retried, while a 504 timeout needs a status check first.

What does QRIS Cross-Border add for developers building cross-border payment features?

QRIS Cross-Border, called QRIS Antarnegara in Indonesian, is Bank Indonesia's cross-border QRIS, in use in Thailand, Malaysia, Singapore, Japan, South Korea and China with the same Rp10,000,000 per-transaction limit as domestic QRIS. Currency conversion happens inside the participating payment apps, so an Indonesian merchant still receives rupiah. Dynamic QR codes are generated through SNAP's Generate QR MPM service, /v1.0/qr/qr-mpm-generate, whose optional validityPeriod field sets when the code expires.

What are the most common causes of production launch delays in a SNAP integration?

Unregistered production IP addresses are a frequent blocker: SNAP's security standard calls for IP whitelisting, and production gateways reject egress IPs the bank has not approved. Each bank's own sandbox testing, covering endpoint behaviour, error handling, idempotency and security controls, also has to finish first, and its length varies by bank. Key management is the third risk: keep your private key in a secrets manager and test any rotated key in the sandbox before switching.