Webhooks are used to notify the merchant system in real time about transaction status changes (such as deposits credited, payouts completed, etc.). When an event occurs, NUSDpay sends an HTTP POST request to the endpoint URL configured by the merchant.
How It Works
Item Description Trigger condition Automatically triggered when transaction status changes (e.g. deposit confirmed, payout completed) Request method HTTP POST Timeout 2 seconds Retry policy Automatically retries if a 200/201 response is not received, up to 10 times Response requirement Respond with 200 or 201 to indicate the event was successfully received
After receiving a Webhook, first verify that the wallet_id belongs to your project. Events from other projects should be ignored.
Prepare an endpoint URL
Deploy an HTTP endpoint on your server to receive Webhook events.
Provide the URL to us
Send the endpoint URL to technical support to be configured.
Implement the handling logic
In your endpoint, implement: parse the request → verify the signature → process the business logic → respond with 200/201.
Verify the Signature
To prevent unauthorized access, you must verify the signature when you receive a Webhook event.
Steps
Read the timestamp and signature from the request headers:
bizTimestamp := r . Header . Get ( "biz-timestamp" )
signature := r . Header . Get ( "biz-resp-signature" )
Concatenate the message: raw request body + "|" + timestamp
Apply a double SHA256 hash to the message
Verify the signature using the Ed25519 public key
Full Verification Code
package webhook
import (
" crypto/sha256 "
" encoding/hex "
" errors "
" golang.org/x/crypto/ed25519 "
)
func VerifySignature ( rawBody string , bizTimestamp string , signatureHex string , pubKeyHex string ) ( bool , error ) {
if bizTimestamp == "" || signatureHex == "" {
return false , errors . New ( "missing signature or timestamp" )
}
msg := rawBody + "|" + bizTimestamp
h1 := sha256 . Sum256 ([] byte ( msg ))
h2 := sha256 . Sum256 ( h1 [:])
msgHash := h2 [:]
sig , err := hex . DecodeString ( signatureHex )
if err != nil {
return false , err
}
pubKeyBytes , err := hex . DecodeString ( pubKeyHex )
if err != nil {
return false , err
}
if len ( pubKeyBytes ) != ed25519 . PublicKeySize {
return false , errors . New ( "invalid pubkey length" )
}
if len ( sig ) != ed25519 . SignatureSize {
return false , errors . New ( "invalid signature length" )
}
ok := ed25519 . Verify ( ed25519 . PublicKey ( pubKeyBytes ), msgHash , sig )
return ok , nil
}
Please contact technical support to obtain the PUB_KEY .
Webhook Events
Event Types
Event Description wallets.transaction.createdTransaction has been created wallets.transaction.updatedTransaction status has been updated wallets.transaction.succeededTransaction completed successfully
NUSDpay does not guarantee that events arrive in the order they are produced. Your endpoint should process business logic based on event content, not arrival order.
Retry Mechanism
Timeout (no response within 2 seconds) or non-200/201 response: the system retries automatically
Up to 10 retries
After exceeding the retry limit, the event status becomes “delivery failed”
We recommend using request_id for idempotency to prevent retries from causing duplicate processing. We also recommend periodically calling the query endpoint as a reconciliation fallback.
Complete Implementation Examples
package main
import (
" encoding/json "
" io "
" log "
" net/http "
" time "
" crypto/sha256 "
" encoding/hex "
" golang.org/x/crypto/ed25519 "
)
const (
PORT = "8000"
PUB_KEY = "YOUR_PUB_KEY_HEX"
)
var walletID = "YOUR_WALLET_ID"
type WebhookPayload struct {
Data struct {
WalletID string `json:"wallet_id"`
} `json:"data"`
}
func webhookHandler ( w http . ResponseWriter , r * http . Request ) {
rawBody , err := io . ReadAll ( r . Body )
if err != nil {
http . Error ( w , "Read body failed" , http . StatusBadRequest )
return
}
defer r . Body . Close ()
// Verify the signature
signature := r . Header . Get ( "biz-resp-signature" )
bizTimestamp := r . Header . Get ( "biz-timestamp" )
message := string ( rawBody ) + "|" + bizTimestamp
h1 := sha256 . Sum256 ([] byte ( message ))
h2 := sha256 . Sum256 ( h1 [:])
sig , _ := hex . DecodeString ( signature )
pubKey , _ := hex . DecodeString ( PUB_KEY )
if ! ed25519 . Verify ( ed25519 . PublicKey ( pubKey ), h2 [:], sig ) {
http . Error ( w , "Invalid signature" , http . StatusUnauthorized )
return
}
// Parse and handle
var payload WebhookPayload
json . Unmarshal ( rawBody , & payload )
if payload . Data . WalletID == walletID {
// Process business logic
log . Println ( "Received event for wallet:" , walletID )
}
w . WriteHeader ( http . StatusCreated )
}
func main () {
http . HandleFunc ( "/api/webhook" , webhookHandler )
log . Printf ( "Server running on : %s \n " , PORT )
log . Fatal (( & http . Server {
Addr : ":" + PORT , Handler : nil ,
ReadTimeout : 5 * time . Second , WriteTimeout : 5 * time . Second ,
}). ListenAndServe ())
}
const express = require ( "express" );
const CryptoJS = require ( "crypto-js" );
const nacl = require ( "tweetnacl" );
const PUB_KEY = "YOUR_PUB_KEY_HEX" ;
const WALLET_ID = "YOUR_WALLET_ID" ;
const app = express ();
app . use ( express . json ({
verify : ( req , res , buf ) => { req . rawBody = buf . toString (); }
}));
app . post ( "/api/webhook" , ( req , res ) => {
// Verify the signature
const signature = req . headers [ 'biz-resp-signature' ];
const bizTimestamp = req . headers [ 'biz-timestamp' ];
const message = ` ${ req . rawBody } | ${ bizTimestamp } ` ;
const hash = CryptoJS . SHA256 ( CryptoJS . SHA256 ( message )). toString ( CryptoJS . enc . Hex );
try {
const sigBuf = Buffer . from ( signature , "hex" );
const msgBuf = Buffer . from ( hash , "hex" );
const vk = Buffer . from ( PUB_KEY , "hex" );
if ( ! nacl . sign . detached . verify ( msgBuf , sigBuf , vk )) {
return res . status ( 401 ). send ( "Invalid signature" );
}
} catch ( e ) {
return res . status ( 401 ). send ( "Verification failed" );
}
// Process business logic
const data = JSON . parse ( req . rawBody );
if ( data . data . wallet_id === WALLET_ID ) {
console . log ( "Received event for wallet:" , WALLET_ID );
}
res . sendStatus ( 201 );
});
app . listen ( 8000 , () => console . log ( "Server running on :8000" ));
Next Steps
Deposit Callback View the payload field reference for deposit events.
Withdrawal Callback View the payload field reference for withdrawal events.