API Reference

Normal / Legacy API Authentication

When you're calling any of Durianpay's API, you have to pass Authorization parameter in the header. Value for Authorizationcan be received by going through these steps:

  1. Get your respective API Keys from Durianpay's dashboard, by going to Settings > API Keys

  2. Formatting the API Key to BASIC AUTH format
    The BASIC AUTH will have the following format: Username:Password.
    Replace Username with your API Key and leave value for password as Blank.
    The result for this combination will be: string {Your_API_Key}:

  3. Encode the previously generated combination to Base64 format

  4. Include the Base64 encoded string in the HTTP(S) header
    Prepend the authorization method (Basic) and a space () to the encoded string. The authorization header is as given below: Authorization: Basic [Base64({Your_API_Key}:)]

🚧

Important things to note:

  1. Before you encode the keys value, Please ensure the format of the API Key follows the aforementioned Basic Auth format, {Your_API_Key}: . Where there will be : after your API Key.
  2. The Authorization Header given in the example is for reference only. Please use your own Sandbox/Production Server Key to create your API key.
  3. The API keys for Sandbox and Production are different. When going live, please switch to the LIVE API Keys.

SNAP API Authentication:

When you're calling any of Durianpay's SNAP API, you have to provide several parameter in the header. Value for these header parameter can be received via dashboard under Menu Settings > API Key:

  • Client Key (Will be used as X-Client-Key and X-Partner-ID in request header)
  • Client Secret Key (Will be used for encrypting symmetric X-Signature)
  • Merchant ID (Will be used for several API request body such as Balance Inquiry)
  • Public Key given by Durianpay (Will be used to verify signature from us)

Additionally you need to generate Merchant's Private & Public Key Pair from your end

  • Merchant Private Key will be used for encrypting asymmetric X-Signature
  • Merchant Public Key will be used by Durianpay to verify signature of request transaction from the merchant

All credentials above will need to be setup for both live and sandbox environment. Live credentials will be given only after merchant 's legal document & functionality test is approved by Durianpay.

Generate Your Private/Public Key Pair

You can refer to following steps to generate private/public key pair from your system.

Generate Private Key

openssl genrsa -out rsa_private_key.pem 2048

Generate Public Key

openssl rsa -in rsa_private_key.pem -out rsa_public_key.pem -pubout

You need to generate different public-private key pair for sandbox mode and live mode

Configuration of Public Key

To configure your public key in Durianpay, you can set it up via our dashboard. It is supported separately for your sandbox public key and live public key.

  1. Please ensure the file of your public key to be in a .pem format
  2. Open Settings-API Key menu and you will see Upload Public Key button. Click the button to upload your public key
  3. Select the public key file you want to submit or drag the file to the upload box then click Upload button.
  4. Once public key is stored in Durianpay side you will see rsa_public_key.pem and delete button.

Request Header

Each SNAP Request requires specific header value. There are 2 kind of header for requests, to generate B2B Access Token, and Transaction (disbursement, payment, etc)

B2B Access Token Header

Generate Access Token (B2B) API

HeaderDescriptionFormat
X-TIMESTAMPRequest timestamp ISO 8601datetime
X-SIGNATURESecurity signatureBase64 encoded string
X-CLIENT-KEYMerchant identifier keyString
Content-TypeType of data being sentMIME type

Transaction Request Header

HeaderDescriptionFormat
X-TIMESTAMPRequest timestampISO 8601 datetime
X-SIGNATURESecurity signatureBase64 encoded string
X-PARTNER-IDPartner identifierAlphanumeric string
X-EXTERNAL-IDUnique messaging reference identifier generated by merchant and should be unique within the same dayRandom Alphanumeric string
CHANNEL-IDChannel identifierNumeric string
Content-TypeType of data being sentMIME type
AuthorizationAuthorization tokenJWT (Started with 'Bearer ')

Generate Signature

There are two use kind of signature to be generated, one is to obtain access token, and another is for transaction. Signature for B2B Access Token is encrypted in an asymmetrically way, while for transaction signature is encrypted in a symmetrically way.

Generate Signature To Obtain Access Token (B2B)

You can find the required structure of the string to be encrypted as below:

<X-CLIENT-KEY>|<X-TIMESTAMP> // (on ISO8601 format)
//Example: merchant_client_key|2024-05-13T14:53:06.991+07:00

which then signed using SHA256withRSA with Private/Public Key pair merchant has generated.

X-Signature: aw3o6HM68vJDLO4nxAPgK0it5nd6zik3bUgMzqLiTrIB7w1QbnCLDo/IMVjaYsbPk9s=

Script to verify signature:

/*
script to verify webhook signature
to verify the signature, fill the necessary parametes inside main() method
and run the script using the command

`go run main.go`

*/

package main

import (
	"bytes"
	"crypto"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"net/http"
	"strings"
)

func main() {
	// fill data here for the parameters
	requestBody := []byte(`{}`)
	relativeURL := ""
	timestamp := ""
	rsaPublicKey := ""
	xSignature := ""

	err := VerifyWebhookSignature(requestBody, relativeURL, timestamp, rsaPublicKey, xSignature)
	if err != nil {
		fmt.Println("signature verification failed")
		return
	}
	fmt.Println("signature verified")
}

func VerifyWebhookSignature(requestBody []byte, relativeURL string, timestamp string, rsaPublicKey string, xSignature string) (err error) {
	// body should be added to signature as Lowercase(HexEncode(SHA-256(minify(RequestBody))))
	var minifiedRequest bytes.Buffer
	err = json.Compact(&minifiedRequest, requestBody)
	if err != nil {
		fmt.Println("error minifying request body, error: ", err.Error())
		return
	}
	encodedRequest := generateHexEncodedSHA256(minifiedRequest.String())

	// Format -> SHA256withRSA (HTTPMethod + ":" + RelativeUrl + ":" + Lowercase(HexEncode(SHA-256(minify(RequestBody)))) + ":" + TimeStamp)
	verificationBody := strings.Join([]string{http.MethodPost, relativeURL, strings.ToLower(encodedRequest), timestamp}, ":")

	err = verifySignature([]byte(verificationBody), rsaPublicKey, xSignature)
	if err != nil {
		fmt.Println("error verifying signature, error: ", err.Error())
		return
	}
	return
}

func verifySignature(requestPayload []byte, rsaPublicKey string, signature string) (err error) {
	// base64 decode the signature
	decodedSignature, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		fmt.Println("error base64 decoding signature, error: ", err.Error())
		return
	}

	rsaKey, err := getPKIXPublicKey(rsaPublicKey)
	if err != nil {
		fmt.Println("error parsing rsa public key, error: ", err.Error())
		return
	}

	hashed := sha256.Sum256(requestPayload)

	err = rsa.VerifyPKCS1v15(rsaKey, crypto.SHA256, hashed[:], decodedSignature)
	if err != nil {
		fmt.Println("error verifying signature, error: ", err.Error())
		return
	}
	return
}

func generateHexEncodedSHA256(msg string) string {
	h := sha256.New()
	h.Write([]byte(msg))
	return hex.EncodeToString(h.Sum(nil))
}

// getPKIXPublicKey takes in an RSA public key string.
// decode the PEM string and constructs an RSA public key with PKIX format
func getPKIXPublicKey(publicKey string) (rsaPublicKey *rsa.PublicKey, err error) {
	block, _ := pem.Decode([]byte(publicKey))
	if block == nil {
		err = errors.New("no key found")
		fmt.Println("error verifying signature, error: ", err.Error())
		return
	}

	key, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		fmt.Println("error in parsing public key, error: ", err.Error())
		return
	}

	rsaPublicKey, ok := key.(*rsa.PublicKey)
	if !ok {
		err = errors.New("cannot type assert as rsa public key pointer")
		fmt.Println("error parsing public key, error: ", err.Error())
		return
	}
	return
}


Generate Signature For Transaction API

You can find the required structure of the string to be encrypted as below:

<HTTP METHOD> + ":" + <RELATIVE PATH URL> + ":" + <B2B ACCESS TOKEN> + ":" + LowerCase(HexEncode(SHA-256(Minify(<HTTP BODY>)))) + ":" + <X-TIMESTAMP>
  // Example: POST:/v1.0/balance-inquiry:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJEdXJpYW4gTW9uZXkiLC :f806c49e8cd175aa9dd5dd8e0a49648c885954d:2024-05-13T15:07:07+07:00

which then encrypted using HMAC-SHA512 with merchant's Secret Key that Durianpay give.

X-Signature: tnNHFw5ZCWjnHFd9UGAB5iDLuwo+89efml8F1CP8vaqut/PfNWac/XLiCkLt3zGHTppZKPxz/PPptqM2alsOFA==

Generate Access Token (B2B)

📘

B2B Access Token will be used for all transactional APIs. The token itself will have 900 seconds (15 minutes) valid time by default.

There is no refresh access token nor the access token will be auto refreshed, so you will need to generate another access token if it has passed its expiry time

curl --location 'https://api.durianpay.id/v1.0/access-token/b2b' \
--header 'X-TIMESTAMP: 2024-05-13T15:32:36.422+07:00' \
--header 'X-SIGNATURE: KLJ9KthaIiZSzPqHUMuJRsKOZtYCqqWd5DhfWqYC7OjZ2Qlu0PvQ2wRQF5vfDWsz/qFYEexOXb47+oQBjMhAz4XRNhXBBfwoWxrGTG8iU8EYSDVfChW0NqAWjWIQolhB4UcHn5SybPFRON3rvAOIQTzuEshUA1PNAln8jOKaRmY=' \
--header 'X-CLIENT-KEY: client_id' \
--header 'Content-Type: application/json' \
--data '{
    "grantType": "AUTHORIZATION_CODE"
}'
import requests

url = 'https://api.durianpay.id/v1.0/access-token/b2b'
headers = {
    'X-TIMESTAMP': '2024-05-13T15:32:36.422+07:00',
    'X-SIGNATURE': 'KLJ9KthaIiZSzPqHUMuJRsKOZtYCqqWd5DhfWqYC7OjZ2Qlu0PvQ2wRQF5vfDWsz/qFYEexOXb47+oQBjMhAz4XRNhXBBfwoWxrGTG8iU8EYSDVfChW0NqAWjWIQolhB4UcHn5SybPFRON3rvAOIQTzuEshUA1PNAln8jOKaRmY=',
    'X-CLIENT-KEY': 'client_id',
    'Content-Type': 'application/json'
}
data = {
    "grantType": "AUTHORIZATION_CODE"
}

response = requests.post(url, headers=headers, json=data)

print(response.text)
OkHttpClient client = new OkHttpClient();

String url = "https://api.durianpay.id/v1.0/access-token/b2b";
String timestamp = "2024-05-13T15:32:36.422+07:00";
String signature = "KLJ9KthaIiZSzPqHUMuJRsKOZtYCqqWd5DhfWqYC7OjZ2Qlu0PvQ2wRQF5vfDWsz/qFYEexOXb47+oQBjMhAz4XRNhXBBfwoWxrGTG8iU8EYSDVfChW0NqAWjWIQolhB4UcHn5SybPFRON3rvAOIQTzuEshUA1PNAln8jOKaRmY=";
String clientKey = "client_id";
String contentType = "application/json";

Gson gson = new Gson();
String jsonBody = gson.toJson(Map.of("grantType", "AUTHORIZATION_CODE"));

RequestBody requestBody = RequestBody.create(MediaType.parse(contentType), jsonBody);

Request request = new Request.Builder()
    .url(url)
    .addHeader("X-TIMESTAMP", timestamp)
    .addHeader("X-SIGNATURE", signature)
    .addHeader("X-CLIENT-KEY", clientKey)
    .addHeader("Content-Type", contentType)
    .post(requestBody)
    .build();
Response response = client.newCall(request).execute();
const request = require('request');

const url = 'https://api.durianpay.id/v1.0/access-token/b2b';
const headers = {
    'X-TIMESTAMP': '2024-05-13T15:32:36.422+07:00',
    'X-SIGNATURE': 'KLJ9KthaIiZSzPqHUMuJRsKOZtYCqqWd5DhfWqYC7OjZ2Qlu0PvQ2wRQF5vfDWsz/qFYEexOXb47+oQBjMhAz4XRNhXBBfwoWxrGTG8iU8EYSDVfChW0NqAWjWIQolhB4UcHn5SybPFRON3rvAOIQTzuEshUA1PNAln8jOKaRmY=',
    'X-CLIENT-KEY': 'client_id',
    'Content-Type': 'application/json'
};
const data = {
    "grantType": "AUTHORIZATION_CODE"
};

request.post({
    url: url,
    headers: headers,
    json: data
}, (error, response, body) => {
    if (error) {
        console.error('Request error:', error);
    } else {
        console.log('Response status code:', response.statusCode);
        console.log('Response body:', body);
    }
});
require 'net/http'
require 'uri'
require 'json'

url = URI.parse('https://api.durianpay.id/v1.0/access-token/b2b')
headers = {
  'X-TIMESTAMP' => '2024-05-13T15:32:36.422+07:00',
  'X-SIGNATURE' => 'KLJ9KthaIiZSzPqHUMuJRsKOZtYCqqWd5DhfWqYC7OjZ2Qlu0PvQ2wRQF5vfDWsz/qFYEexOXb47+oQBjMhAz4XRNhXBBfwoWxrGTG8iU8EYSDVfChW0NqAWjWIQolhB4UcHn5SybPFRON3rvAOIQTzuEshUA1PNAln8jOKaRmY=',
  'X-CLIENT-KEY' => 'client_id',
  'Content-Type' => 'application/json'
}
data = { 'grantType' => 'AUTHORIZATION_CODE' }

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url.request_uri, headers)
request.body = data.to_json

response = http.request(request)
puts "Response code: #{response.code}"
puts "Response body: #{response.body}"
{
    "responseCode": "2007300",
    "responseMessage": "Successful",
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJEdXJpYW4gTW9uZXkiLCJleHAiOjE3MTU1OTAwNTYsImlhdCI6MTcxNTU4OTE1NiwibWVyY2",
    "tokenType": "BearerToken",
    "expiresIn": "2024-05-13 08:47:36.452492159 +0000 UTC"
  }