# SNAP Overview

# Overview

  • PayCools provides several ways for merchants or third parties to integrate with PayCools's system. One of the ways is by integrating with PayCools Standard Nasional Open API Pembayaran (SNAP) API. SNAP is the standardized Open API initiated by Bank Indonesia, focused on building a healthy, competitive, and innovative financial industry. The standardization includes aspects such as communication, data format, security, and more.

# Advantage

  • Following are the advantage of PayCools SNAP API:
  1. Integration and communication are more efficient using API which have been standardize.
  2. The process and data is more secure refer to the baseline of SNAP security.
  3. Expanding the digital payment that can be used for all of third parties's ecosystem.

# Documentation

  • The following are the documentation which appears for PayCools SNAP API:
No Name Remakrs
1 API Application Programming Interface, allows merchant or third party to connect and integrate with PayCools. With the API merchant or third party enables to provides PayCools as one of their payment method
2 Data Model A extended information either from Request or Response Parameter
3 Enum A data type that represent a constant value

# Quick Start

  • The following are the list of brief step that you should take:
  1. Select the service you would like to integrate. See SNAP Service for a list of services provided by PayCools.
  2. Grab client_id and client_secret from onboarding process. These values as an important parameter which used to integrate.
  3. Do the signature by following the step that have been provided on each service. There are 2 signature provided, namely: Symmetric and Asymmetric.
  4. Follow the instruction to hit the API which related to the each service.
  5. Note:
    • If you use the symmetric approach for signature, you have to do the Authorization Token Request first, obtain the token that can be used in the HTTP header Authorization.
    • Some API need access customer token or it's called access token, thus you have to do the Account Binding & Unbinding first to obtain the access token that used in the HTTP header Authorization-Customer.
  6. Check periodically the latest document to see the updated of PayCools SNAP API.

Signature Generation and Validation

  • The below will explain about signature generation & validation.

# Digital Signature Validation

  • Apply Token B2B Signature
  1. Compose the string to sign:
    <X-CLIENT-KEY> + “|” + <X-TIMESTAMP>
  2. Generate RSA-2048 public and private keys, and use the private key to sign, and fill the public key into the merchant background.
  3. Take the signature from HTTP header “ X-SIGNATURE“.
  4. Compare the value between X-SIGNATURE and the generated signature, if those value are the same, then consume the message.
  • Transaction Signature
  1. Compose the string to sign:
    <HTTP METHOD> + ":" + <RELATIVE PATH URL> + “:“ + <B2B ACCESS TOKEN> + “:“ + LowerCase(HexEncode(SHA-256(Minify(<HTTP BODY>)))) + “:“ + <X-TIMESTAMP>
    Note: <HTTP BODY> must be sorted in ASCII order and converted to a json string.
  2. Generate RSA-2048 public and private keys, and use the private key to sign, and fill the public key into the merchant background.
  3. Take the signature from HTTP header “ X-SIGNATURE“.
  4. Compare the value between X-SIGNATURE and the generated signature, if those value are the same, then consume the message.

# Appendix: RSA Usage Example (Java Language)

  • Generate RSA key pair:
import java.security.*;
import java.util.Base64;

public class RsaGenerateKeyPair {
    public static void main(String[] args) throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.genKeyPair();

        PublicKey publicKey = keyPair.getPublic();
        byte[] publicKeyBytes = publicKey.getEncoded();
        String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyBytes);
        System.out.println(publicKeyBase64);

        PrivateKey privateKey = keyPair.getPrivate();
        byte[] privateKeyBytes = privateKey.getEncoded();
        String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKeyBytes);
        System.out.println(privateKeyBase64);
    }
}

RSA Public Key:

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAna4Dyz8nGJiAlc9jTGyRa+TtlZXYABTc+Xfb3T4NdDbnUO8vtNLHugwmqARp8kzEzsMRbmvKro4EpaXqANn7SAGo+YI6sVUDmX7ESk3P6j51PtTvWR6dikJN6qwtmV64ojEbxDnIBL3VKuctefL8uPcI7MZBUPBXg9l8CZmnn2cKqWjZ8MuEQr4G45IqmJ0tRsRmW9ofNnvI1MLPt7c/Z/D1E6HKVwjPcMZKMuF0HpIDqdQaPX83dlSzv9FF9jFR8HWfWW8Oz3jz+GtSLSdh2ERcyO56WHpWl1POV4o9jF+4R/oBgcH+0zA1Z2aFfQf/n9miMhacrioStBaHkh1f/QIDAQAB

RSA Private Key:

MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCdrgPLPycYmICVz2NMbJFr5O2VldgAFNz5d9vdPg10NudQ7y+00se6DCaoBGnyTMTOwxFua8qujgSlpeoA2ftIAaj5gjqxVQOZfsRKTc/qPnU+1O9ZHp2KQk3qrC2ZXriiMRvEOcgEvdUq5y158vy49wjsxkFQ8FeD2XwJmaefZwqpaNnwy4RCvgbjkiqYnS1GxGZb2h82e8jUws+3tz9n8PUTocpXCM9wxkoy4XQekgOp1Bo9fzd2VLO/0UX2MVHwdZ9Zbw7PePP4a1ItJ2HYRFzI7npYelaXU85Xij2MX7hH+gGBwf7TMDVnZoV9B/+f2aIyFpyuKhK0FoeSHV/9AgMBAAECggEAYyqq5iucqgJXdGCO4eSx/LpolZg81ahJZXf1RgqdqYZSKnuTdFTQGflEYo0MGMAhUqwqDVkrimZ1E7zqE4kEWT/6BpnZ0edWsTWhu91+MqL/V/nRYio4CFk06a9JqliBJDhgbyOr4ReGtknYNwcT3Dw5V7hEIeRWFe007lC9tCi7mlpzBNwEIf4itmnncuA70GlxcoMkoGzfYg79eUCfXorbfJcaamR2wXLSU6KoJ422UR3L0rgzmgXzVQw9rrlQ3h6viDykKfaPi/43MN2qb6Zu5isbJIzyz0kHrcE6KJMgJhBDkLIo0f0qE/rEl1Xp/qDwr4+3WBfCHeuTFsud/QKBgQDXmA3f0/ONPMgEGdJlwG20W+7jXHabnRPuUJyDQKbtP+vuaKrpzN+jC1rlxBfAJj2iAVXXXM/RFWWapBd16TqGI4P3RW8eocaxhyl8rWSvCOy/OueNI+fM8gX/IjsJc7VMmCEWHuLvXoM2ixXPWP3v0DEPPPDrCd5dnjR6+5oGgwKBgQC7O03ps4KzMUzEtJcrFFKV0C/m1X905OqQ3cKQnGqRzLp/7d9DQsv+oKzjlpz1xktdJmig7ABiL0+FqJHdcrNiVabI5c6oS2SZkToQFlKv2GYT2KikJ0L43xLfiDvB3tues//9OXuU0WzXZqq7CNAvcmAdPjlFi9RxHsRGABo3fwKBgEi2EJ/XpQGSaUbwyoPktVsp0lS9/4aWIH20lES0DlhfwZuDk3kMzrP3hW2OiBAXFZxI5QGgXLqAg+b2xq7OvR02ZzCDK2niV9fR5Q0Wkaly0h3gqO1yGaCGU71rdwvGCXROroH+Yr0mXAyONgnbUrGJvrIL9JjgmC1syPhdWOIvAoGBAJHJbbNpWX3aB2KrE4IxwtRwVLwyxZnpnVPLuPINOVXpydZPDCc9XcYYqkZUQkeFba1MeO/Ek8/f8tWqGloKM+9/reyENFQK0Hxa/pEEMMJHh8QwUa/v+k/6sqFnXNBqjSuYEN3F4ppQL6XRhWM5S5GGR5y9lK64YGTshfvTnJZVAoGBAJ3TmJcRJWfi7CA985VAnE+IQoQfKKz9NTT7hGBwWTVd7iUc0QCpgHNIixZnfVcjKxz7Hhq6Vy+cEbDBtwbSuDfuVf1spiiqOuYVIjFqq5AsuvpX1CJmm7V+LRtJO/NXmXQP5YfojzET9NqTZvGEVXuzPA0qp8JC7HKrCYykscqE
  • Signature And Validation Example:
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;

import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;


@Slf4j
public class SnapSignUtil {
    
    public static boolean verifySign(String httpMethod, String url, String accessToken, String body, String timestamp, String sign, String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
        log.info("snap verifySign => httpMethod:{}, url:{}, accessToken:{}, body:{}, timestamp:{}, sign:{}", httpMethod, url, accessToken, body, timestamp, sign);
        String bodySha = handleBody(minifyJsonString(body));
        String payload;
        if (StringUtils.isNotBlank(accessToken)) {
            payload = StrUtil.format("{}:{}:{}:{}:{}", httpMethod, url, accessToken, bodySha, timestamp);
        } else {
            payload = StrUtil.format("{}:{}:{}:{}", httpMethod, url, bodySha, timestamp);
        }
        log.info("snap verifySign => payload:{}", payload);

        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
        signature.update(payload.getBytes());
        return signature.verify(Base64.getDecoder().decode(sign));
    }


    public static String generateSign(HttpMethod httpMethod, String url, String accessToken, String body, String timestamp, String privateKey) {
        log.info("snap generateSign => httpMethod:{}, url:{}, accessToken:{}, body:{}, timestamp:{}", httpMethod, url, accessToken, body, timestamp);
        String bodySha = handleBody(minifyJsonString(body));
        String payload;
        if (StringUtils.isNotBlank(accessToken)) {
            payload = StrUtil.format("{}:{}:{}:{}:{}", httpMethod.toString(), url, accessToken, bodySha, timestamp);
        } else {
            payload = StrUtil.format("{}:{}:{}:{}", httpMethod.toString(), url, bodySha, timestamp);
        }

        log.info("snap sign payload => payload:{}", payload);
        try {
            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))));
            signature.update(payload.getBytes(StandardCharsets.UTF_8));
            byte[] signWithoutHex = signature.sign();
            String sign = Base64.getEncoder().encodeToString(signWithoutHex);
            return sign;
        } catch (Exception e) {
            log.error("[Generate Sign] Generate error.", e);
        }
        return null;
    }

    public static String generateAccessTokenSign(String partnerId, String timestamp, String privateKey) {
        String payload = StrUtil.format("{}|{}", partnerId, timestamp);
        try {
            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))));
            signature.update(payload.getBytes(StandardCharsets.UTF_8));
            byte[] signWithoutHex = signature.sign();
            return Base64.getEncoder().encodeToString(signWithoutHex);
        } catch (Exception e) {
            log.error("[Generate Access Token Sign] Generate error.", e);
        }
        return null;
    }

    public static Boolean verifyAccessTokenSign(String partnerId, String timestamp, String sign, String publicKey) throws NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException {
        String payload = StrUtil.format("{}|{}", partnerId, timestamp);
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
        signature.update(payload.getBytes());
        return signature.verify(Base64.getDecoder().decode(sign));
    }

    public static String buildTimeStamp() {
        return ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC+7"))
                .toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss+07:00"));
    }

    private static String handleBody(String body) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            byte[] bytes = messageDigest.digest(body.getBytes());
            String result = new String(Hex.encodeHex(bytes));
            return result.toLowerCase();
        } catch (NoSuchAlgorithmException e) {
            log.error("[Generate Sign] Handle body error.", e);
        }
        return null;
    }

    /**
     * minify json string
     * @param json
     * @return
     */
    public static String minifyJsonString(String json) {
        try {
            // Remove whitespaces and newlines
            StringBuilder result = new StringBuilder();
            boolean inString = false;
            for (int i = 0; i < json.length(); i++) {
                char c = json.charAt(i);
                if (c == '"') {
                    inString = !inString;
                }
                if (!Character.isWhitespace(c) || inString) {
                    result.append(c);
                }
            }
            return result.toString();
        } catch (Exception e) {
            throw new RuntimeException("Parse JSON failed. Value: " + json, e);
        }
    }

    public static final String PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCdrgPLPycYmICVz2NMbJFr5O2VldgAFNz5d9vdPg10NudQ7y+00se6DCaoBGnyTMTOwxFua8qujgSlpeoA2ftIAaj5gjqxVQOZfsRKTc/qPnU+1O9ZHp2KQk3qrC2ZXriiMRvEOcgEvdUq5y158vy49wjsxkFQ8FeD2XwJmaefZwqpaNnwy4RCvgbjkiqYnS1GxGZb2h82e8jUws+3tz9n8PUTocpXCM9wxkoy4XQekgOp1Bo9fzd2VLO/0UX2MVHwdZ9Zbw7PePP4a1ItJ2HYRFzI7npYelaXU85Xij2MX7hH+gGBwf7TMDVnZoV9B/+f2aIyFpyuKhK0FoeSHV/9AgMBAAECggEAYyqq5iucqgJXdGCO4eSx/LpolZg81ahJZXf1RgqdqYZSKnuTdFTQGflEYo0MGMAhUqwqDVkrimZ1E7zqE4kEWT/6BpnZ0edWsTWhu91+MqL/V/nRYio4CFk06a9JqliBJDhgbyOr4ReGtknYNwcT3Dw5V7hEIeRWFe007lC9tCi7mlpzBNwEIf4itmnncuA70GlxcoMkoGzfYg79eUCfXorbfJcaamR2wXLSU6KoJ422UR3L0rgzmgXzVQw9rrlQ3h6viDykKfaPi/43MN2qb6Zu5isbJIzyz0kHrcE6KJMgJhBDkLIo0f0qE/rEl1Xp/qDwr4+3WBfCHeuTFsud/QKBgQDXmA3f0/ONPMgEGdJlwG20W+7jXHabnRPuUJyDQKbtP+vuaKrpzN+jC1rlxBfAJj2iAVXXXM/RFWWapBd16TqGI4P3RW8eocaxhyl8rWSvCOy/OueNI+fM8gX/IjsJc7VMmCEWHuLvXoM2ixXPWP3v0DEPPPDrCd5dnjR6+5oGgwKBgQC7O03ps4KzMUzEtJcrFFKV0C/m1X905OqQ3cKQnGqRzLp/7d9DQsv+oKzjlpz1xktdJmig7ABiL0+FqJHdcrNiVabI5c6oS2SZkToQFlKv2GYT2KikJ0L43xLfiDvB3tues//9OXuU0WzXZqq7CNAvcmAdPjlFi9RxHsRGABo3fwKBgEi2EJ/XpQGSaUbwyoPktVsp0lS9/4aWIH20lES0DlhfwZuDk3kMzrP3hW2OiBAXFZxI5QGgXLqAg+b2xq7OvR02ZzCDK2niV9fR5Q0Wkaly0h3gqO1yGaCGU71rdwvGCXROroH+Yr0mXAyONgnbUrGJvrIL9JjgmC1syPhdWOIvAoGBAJHJbbNpWX3aB2KrE4IxwtRwVLwyxZnpnVPLuPINOVXpydZPDCc9XcYYqkZUQkeFba1MeO/Ek8/f8tWqGloKM+9/reyENFQK0Hxa/pEEMMJHh8QwUa/v+k/6sqFnXNBqjSuYEN3F4ppQL6XRhWM5S5GGR5y9lK64YGTshfvTnJZVAoGBAJ3TmJcRJWfi7CA985VAnE+IQoQfKKz9NTT7hGBwWTVd7iUc0QCpgHNIixZnfVcjKxz7Hhq6Vy+cEbDBtwbSuDfuVf1spiiqOuYVIjFqq5AsuvpX1CJmm7V+LRtJO/NXmXQP5YfojzET9NqTZvGEVXuzPA0qp8JC7HKrCYykscqE";
    public static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAna4Dyz8nGJiAlc9jTGyRa+TtlZXYABTc+Xfb3T4NdDbnUO8vtNLHugwmqARp8kzEzsMRbmvKro4EpaXqANn7SAGo+YI6sVUDmX7ESk3P6j51PtTvWR6dikJN6qwtmV64ojEbxDnIBL3VKuctefL8uPcI7MZBUPBXg9l8CZmnn2cKqWjZ8MuEQr4G45IqmJ0tRsRmW9ofNnvI1MLPt7c/Z/D1E6HKVwjPcMZKMuF0HpIDqdQaPX83dlSzv9FF9jFR8HWfWW8Oz3jz+GtSLSdh2ERcyO56WHpWl1POV4o9jF+4R/oBgcH+0zA1Z2aFfQf/n9miMhacrioStBaHkh1f/QIDAQAB";

    public static void main(String[] args) throws NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException {
        String appId = "869dd85c7d174f3a8e5d463796c85fe9";
        String timestamp = buildTimeStamp();
        String s = generateAccessTokenSign(appId, timestamp, PRIVATE_KEY);
        System.out.println(s);
        System.out.println(verifyAccessTokenSign(appId, timestamp, s, PUBLIC_KEY));

        String httpMethod = "POST";
        String url = "/v2.1/qr/qr-mpm-generate";
        String accessToken = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6Ijg2OWRkODVjN2QxNzRmM2E4ZTVkNDYzNzk2Yzg1ZmU5IiwiY2FjaGVLZXkiOiJUT0tFTl9BUElfODY5ZGQ4NWM3ZDE3NGYzYThlNWQ0NjM3OTZjODVmZTlfZTdmNmVlYzEtYzg5ZC00ZGIxLWFhZTktZTM1MjNiYWQwNjM1IiwiY3JlYXRlVGltZSI6MTc0MjU0NDg3NjE5OH0.vsM-hRP2odu6ykGifcYKBgNBwIzYQibPmpuenyT8c4U";
        String body = "{\"additionalInfo\":{\"channelCode\":\"QRIS_DYNAMIC_QR\",\"customerMobile\":\"08123456789\",\"customerName\":\"test123\",\"feeSplitType\":2,\"items\":[{\"branchNo\":\"123456\",\"codAmount\":{\"currency\":\"IDR\",\"value\":\"30000.00\"},\"courierNo\":\"123456\",\"frtAmount\":{\"currency\":\"IDR\",\"value\":\"30000.00\"},\"regionNo\":\"12356\",\"subOrderId\":\"123456\"},{\"branchNo\":\"123456\",\"codAmount\":{\"currency\":\"IDR\",\"value\":\"30000.00\"},\"courierNo\":\"123456\",\"frtAmount\":{\"currency\":\"IDR\",\"value\":\"40000.00\"},\"regionNo\":\"12356\",\"subOrderId\":\"123456\"}],\"notifyUrl\":\"www.test.com\"},\"codAmount\":{\"currency\":\"IDR\",\"value\":\"60000.00\"},\"frtAmount\":{\"currency\":\"IDR\",\"value\":\"70000.00\"},\"merchantId\":\"869dd85c7d174f3a8e5d463796c85fe9\",\"partnerReferenceNo\":\"test7099074702266642\",\"validityPeriod\":\"300\"}";

        String sign = generateSign(HttpMethod.POST, url, accessToken, body, timestamp, PRIVATE_KEY);
        System.out.println(sign);
        System.out.println(verifySign(httpMethod, url, accessToken, body, timestamp, sign, PUBLIC_KEY));
    }
}