API Reference

Transfer Bank Notify

Webhook: transfer-bank.notify

This is a server-to-server callback (webhook) sent by Durianpay to your system. You do not need to poll or request status from our API. Instead, you just need to configure your webhook listener endpoint, and Durianpay will push transaction status updates directly to your server via POST request. It can be configured via menu Settings - Webhook


Webhook Specifications

  • Method: POST
  • Content-Type: application/json
  • Retries: 2, 5, 10, 90, 210 minutes after the initial attempt if no 200 OK is received
  • Configuration: Set your webhook URL in Dashboard > Settings > Webhook

Subscribed Event

Event NameDescription
transfer-bank.notifySent when a disbursement reaches final status

Field Definition

Field

Type

Description

originalPartnerReferenceNo

string

Reference merchant send when submitting bank / e-wallet transfer request

originalReferenceNo

string

Durianpay's reference returned when submitting bank/ e-wallet transfer request (dis_item_xxx)

responseCode

string

HTTP status code (4 digit) + Service code (2 digit)+ Case code (2 digit)

responseMessage

string

Response message information

amount.value

string

String representing value of the transaction : "20000.00".

amount.currency

string

Currency of the transaction: "IDR"

beneficiaryAccountNo

string

The account number of the recipient.

beneficiaryBankCode

string

The bank code of the recipient account

sourceAccountNo

string

Identifier of source account. its value is the same as merchant id

additionalInfo.latestTransactionStatus

string

Current transaction status 00 - done 06 - failed

additionalInfo.transactionStatusDesc

string

Description current transaction status

additionalInfo.failureReason

string

Error information when the transaction status is failed. Will only be shown for failed transaction


Webhook Payload Example

{
  "originalReferenceNo": "dis_item_123",
  "originalPartnerReferenceNo": "123456789",
  "responseCode": "2000000",
  "responseMessage": "Request has been processed successfully",
  "amount": {
    "value": "12345678.00",
    "currency": "IDR"
  },
  "beneficiaryAccountNo": "1234567890",
  "beneficiaryBankCode": "002",
  "sourceAccountNo": "mer_123",
  "additionalInfo": {
    "latestTransactionStatus": "00",
    "transactionStatusDesc": "done"
  }
}
{
  "additionalInfo": {
   "failureReason": "Unknown disburse error, please ask customer support for further information",
   "latestTransactionStatus": "06",
   "transactionStatusDesc": "failed"
 },
  "amount": {
   "currency": "IDR",
   "value": "10000.00"
 },
  "beneficiaryAccountNo": "3370018285",
  "beneficiaryBankCode": "014",
  "originalPartnerReferenceNo": "1000-1000-1000-1655511",
  "originalReferenceNo": "dis_item_2OgsLYYZji1085",
  "responseCode": "2000000",
  "responseMessage": "Request has been processed successfully",
  "sourceAccountNo": "mer_MsCtIPhqRc8045"
}

📘 Status Handling

Inspect the additionalInfo object in the webhook payload:

"additionalInfo": {
  "latestTransactionStatus": "00",
  "transactionStatusDesc": "done"
}
latestTransactionStatustransactionStatusDescMeaning
00doneTransaction successful
06failedTransaction failed

Action Required:
Always respond with HTTP 200 OK to acknowledge receipt of the webhook. If not acknowledged, Durianpay will retry based on this schedule: 2, 5, 10, 90, 210 minutes.


🔐 Digital Signature Verification

Durianpay signs every webhook to ensure integrity and authenticity. You must validate the signature before processing.

Webhook Headers:

HeaderDescription
X-SIGNATUREBase64-encoded RSA-2048 signature (SHA-256)
X-TIMESTAMPTimestamp used to sign the payload

Signature Format:

POST:<relative path>:<lowercase hex sha256 of minified JSON body>:<X-TIMESTAMP>

Example:

POST:/callback/v1.0/transfer/notify:5d2c90ddfdd406117ced5c2b502c05b601d435c7e5440f82e58733fdd5f15b7d:2024-11-07T16:04:55.667+07:00

Use Durianpay's public key (Live/Sandbox-specific) to verify the signature.

🧪 Go Code: Signature Verification

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() {
	requestBody := []byte(`{}`)
	relativeURL := "" // e.g., "/callback/v1.0/transfer/notify"
	timestamp := ""   // from X-TIMESTAMP header
	rsaPublicKey := "" // PEM-formatted RSA public key
	xSignature := ""   // from X-SIGNATURE header

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

func VerifyWebhookSignature(requestBody []byte, relativeURL, timestamp, rsaPublicKey, xSignature string) error {
	var minifiedRequest bytes.Buffer
	if err := json.Compact(&minifiedRequest, requestBody); err != nil {
		return err
	}

	hashedPayload := generateHexEncodedSHA256(minifiedRequest.String())
	stringToSign := fmt.Sprintf("%s:%s:%s:%s", http.MethodPost, relativeURL, strings.ToLower(hashedPayload), timestamp)

	return verifySignature([]byte(stringToSign), rsaPublicKey, xSignature)
}

func verifySignature(payload []byte, publicKey string, signature string) error {
	sigBytes, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		return err
	}

	rsaKey, err := parseRSAPublicKey(publicKey)
	if err != nil {
		return err
	}

	hashed := sha256.Sum256(payload)
	return rsa.VerifyPKCS1v15(rsaKey, crypto.SHA256, hashed[:], sigBytes)
}

func generateHexEncodedSHA256(data string) string {
	hash := sha256.Sum256([]byte(data))
	return hex.EncodeToString(hash[:])
}

func parseRSAPublicKey(pemKey string) (*rsa.PublicKey, error) {
	block, _ := pem.Decode([]byte(pemKey))
	if block == nil {
		return nil, errors.New("no key found in PEM")
	}

	pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}

	rsaKey, ok := pubKey.(*rsa.PublicKey)
	if !ok {
		return nil, errors.New("not a valid RSA public key")
	}

	return rsaKey, nil
}