Guide to Indonesian SNAP Protocol Signature Integration
This document is intended for merchants/partners integrating with the Indonesian SNAP (Standar Nasional Open API Pembayaran) standard. It describes the signing and verification logic for various interface types and provides reference implementation code based on the JDK native API (which can be adapted to any programming language).
1. Signature Mechanism Overview
SNAP uses different signature algorithms depending on the interface type. Each key is used exclusively for one purpose throughout the entire process and must not be mixed:
| Interface Type | Direction | Signature Algorithm | Key | StringToSign |
|---|---|---|---|---|
| Access Token (B2B) | Merchant → Platform | SHA256with RSA (Asymmetric) | Merchant RSA private key for signing / Platform uses merchant public key for verification | clientKey || timestamp |
| Business Transaction Interface | Merchant → Platform | HMAC-SHA512 (Symmetric) | clientSecret (shared between both parties) | HTTPMethod:EndpointUrl:AccessToken:SHA256(minify(body)):timestamp |
| Asynchronous Notification | Platform → Merchant | SHA256with RSA (Asymmetric) | Platform RSA private key for signing / Merchant uses platform public key for verification | HTTPMethod:EndpointUrl:SHA256(minify(body)):timestamp |
1.1 Credentials Required by the Merchant
| Credential | Purpose | Source |
|---|---|---|
| Merchant RSA key pair (2048-bit recommended) | Signing for Access Token interface (using private key) | Generated by the merchant; private key is kept securely, public key is uploaded to the platform |
| clientKey + clientSecret | Transaction interface signing (clientSecret used as HMAC key) | Assigned by the platform |
| Platform RSA public key | Verifying asynchronous notifications sent by the platform | Provided by the platform |
2. Core Rules (Must Read)
2.1 ⚠️ Pass Keys Directly; Do Not Decode Manually
Keys are used in their original string form in the signing process. The signature algorithm internally performs Base64.decode. Callers should not decode in advance, as this would result in double decoding and cause failure.
RSA private/public keys: Pass the Base64 string (the algorithm internally uses Base64.getDecoder().decode(...) to restore the bytes).
clientSecret: Pass the UTF-8 plaintext string (the algorithm internally uses secretKey.getBytes(UTF-8) to obtain the bytes).
In short: pass the private/public key as a Base64 string and clientSecret as the plaintext; pass them directly without any decoding.
2.2 StringToSign Concatenation Rules
Fields are joined with a colon
:(the Access Token interface uses a pipe|).All concatenated fields use raw values; no URL encoding or trimming.
HTTPMethod must be in uppercase (POST / GET).
EndpointUrl is the request path (without the host, without the query string), e.g.,
/v1.0/debit/host-to-host.
2.3 HTTP Body Minify Rules
In the StringToSign for transaction interfaces and asynchronous notifications, the body part is Lowercase(HexEncode(SHA-256(minify(body)))), i.e., the body is first minified before the SHA-256 is computed.
Definition of minify: Remove extraneous whitespace (spaces, newlines, tabs) from the JSON while preserving the original field order, preserving original field values, and not omitting null fields. Do not sort the fields — sorting alters the byte stream, causing SHA-256 mismatches between the two ends and resulting in verification failure.
2.4 Timestamp and Encoding
Timestamp format: ISO-8601 with timezone, e.g.,
2026-07-29T10:00:00+07:00(Western Indonesia Time WIB, UTC+7).Character encoding: All strings involved in signing use UTF-8 uniformly.
Digest output: SHA-256 output is a lowercase hexadecimal string.
3. Signature Algorithm Implementation (Reference Code)
The following provides implementation logic for each algorithm based on the JDK native API, which can be used as a reference. Required imports:
import com.alibaba.fastjson.JSON; // Only used for minify; can be replaced with any JSON library
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;3.1 RSA Private Key Signing / Public Key Verification (SHA256withRSA)
/** Sign with private key: base64PrivateKey is the Base64(PKCS8) string */
private static String signWithRsa(String content, String base64PrivateKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64PrivateKey); // internal decode
PrivateKey privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign()); // signature output in Base64
}
/** Verify with public key: base64PublicKey is the Base64(X509) string */
private static boolean verifyWithRsa(String content, String sign, String base64PublicKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64PublicKey);
PublicKey publicKey = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(keyBytes));
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(content.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.getDecoder().decode(sign));
}3.2 HMAC-SHA512 Signing / Verification (Symmetric, Key is UTF-8 Plaintext)
/** Sign: secretKey is UTF-8 plaintext (i.e., clientSecret) */
private static String signWithHmacSha512(String content, String secretKey) throws Exception {
Mac mac = Mac.getInstance("HmacSHA512");
mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA512"));
byte[] hash = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}
/** Verify: recompute with the same key and perform constant-time comparison to prevent timing side-channel attacks */
private static boolean verifyWithHmacSha512(String content, String sign, String secretKey) throws Exception {
String computed = signWithHmacSha512(content, secretKey);
return MessageDigest.isEqual(
computed.getBytes(StandardCharsets.UTF_8),
sign.getBytes(StandardCharsets.UTF_8));
}3.3 SHA-256 Lowercase Hexadecimal Digest
private static String sha256Hex(String content) throws Exception {
byte[] hash = MessageDigest.getInstance("SHA-256").digest(content.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte b : hash) {
sb.append(String.format("%02x", b)); // lowercase hex
}
return sb.toString();
}3.4 JSON Minify (Remove Whitespace, Preserve Order, Retain null; Do Not Sort)
private static String minify(String body) {
if (body == null || body.trim().isEmpty()) {
return "";
}
// OrderedField: preserve original field order
// WriteMapNullValue: retain fields with null values
// Note: Do NOT add SortField, as it reorders by field name and breaks the order
return JSON.toJSONString(
JSON.parse(body, Feature.OrderedField),
SerializerFeature.WriteMapNullValue,
SerializerFeature.DisableCircularReferenceDetect);
}The same principle applies when using other JSON libraries: parse into a structure that preserves insertion order (e.g., LinkedHashMap / Jackson's JsonNode) and then serialize compactly. The key points are: do not sort, do not alter values, and do not drop fields.
4. Access Token Interface (RSA Asymmetric)
Scenario: The merchant signs when obtaining a B2B Access Token; the platform verifies using the merchant's uploaded public key.
StringToSign: clientKey | timestamp
// —— Merchant signing ——
String stringToSign = clientKey + "|" + timestamp; // Example: "PMID0001|2026-07-29T10:00:00+07:00"
String signature = signWithRsa(stringToSign, merchantPrivateKey);
// Request headers
// X-CLIENT-KEY : <clientKey>
// X-TIMESTAMP : <timestamp>
// X-SIGNATURE : <signature>// —— Platform verification ——
String stringToSign = clientKey + "|" + timestamp;
boolean valid = verifyWithRsa(stringToSign, xSignature, merchantPublicKey);5. Business Transaction Interface (HMAC-SHA512 Symmetric)
Scenario: The merchant signs when calling business interfaces (collections, disbursements, queries, etc.); the platform verifies using the same clientSecret.
StringToSign: HTTPMethod : EndpointUrl : AccessToken : Lowercase(Hex(SHA-256(minify(body)))) : timestamp
The HMAC key is clientSecret; accessToken is just plaintext in the StringToSign, not the HMAC key.
// —— Merchant signing ——
String stringToSign = httpMethod + ":" + endpointUrl + ":" + accessToken
+ ":" + sha256Hex(minify(requestBody)) + ":" + timestamp;
String signature = signWithHmacSha512(stringToSign, clientSecret);
// Request headers
// Authorization : Bearer <accessToken>
// X-TIMESTAMP : <timestamp>
// X-SIGNATURE : <signature>/// —— Platform verification ——
String stringToSign = httpMethod + ":" + endpointUrl + ":" + accessToken
+ ":" + sha256Hex(minify(requestBody)) + ":" + timestamp;
boolean valid = verifyWithHmacSha512(stringToSign, xSignature, clientSecret);Example StringToSign concatenation result:
POST:/v1.0/debit/host-to-host:eyJhbGciOiJ...:a1b2c3...(lowercase hex SHA256 of body):2026-07-29T10:00:00+07:006. Asynchronous Notification (RSA Asymmetric)
Scenario: The platform asynchronously calls back to the merchant's notification URL; the platform signs with its own RSA private key; the merchant verifies using the platform's public key.
StringToSign (difference from transaction interface: AccessToken is not included): HTTPMethod : EndpointUrl : Lowercase(Hex(SHA-256(minify(body)))) : timestamp
// —— Platform signing ——
String stringToSign = httpMethod + ":" + notifyUrl + ":"
+ sha256Hex(minify(notifyBody)) + ":" + timestamp;
String signature = signWithRsa(stringToSign, platformPrivateKey);
// Notification request headers
// X-TIMESTAMP : <timestamp>
// X-SIGNATURE : <signature>// —— Merchant verification ——
String stringToSign = httpMethod + ":" + notifyUrl + ":"
+ sha256Hex(minify(notifyBody)) + ":" + timestamp;
boolean valid = verifyWithRsa(stringToSign, xSignature, platformPublicKey);⚠️ When the merchant receives a notification: first verify the signature using the raw received body string. Only after verification passes should the body be parsed for business logic, to avoid business-layer logging or deserialization altering the body and causing verification failure.
7. Frequently Asked Questions
Q1: Verification always fails?
Please check the following in order:
Whether the wrong key was used (private/public key/clientSecret for the wrong purpose, or manual decoding was performed). → See 2.1.
Whether endpointUrl is the pure path (without host / query).
Whether httpMethod is in uppercase.
Whether the body was modified by the framework (extra whitespace, fields reordered) → verify using the raw body first.
Whether the timestamp format/timezone matches the request header.
Q2: Does field order in the transaction interface body matter?
Yes. Minify only removes whitespace and does not change field order. If either side sorts or reformats the fields, the SHA-256 result will differ, leading to verification failure.
Q3: Does clientSecret need to be decrypted or decoded before use?
No. clientSecret is a printable string and is passed directly as the plaintext key for HMAC-SHA512.
Q4: What are the RSA key specification requirements?
RSA 2048-bit is recommended; the signature algorithm is SHA256withRSA; the private key is in PKCS8 format, the public key in X509 format, both transmitted in Base64 encoding.
