Test and Validate Webhooks

It is a best practice to test and validate the webhooks before their usage.

Test Webhooks

You can create an endpoint url and test webhooks using tools like webhook.site or create a tunnel to your localhost using tools such as ngrok.

Validate Webhooks

Here are the steps to verify webhooks:

  1. Get the payload from the webhook endpoint.
  2. Generate the signature.
  3. Verify the signature.

You can look at the sample code in the below repository in different languages.

📘

The payload mentioned here refers to the raw JSON body, not the parsed body.

Webhook Signature

Use the signature to verify if the request has not been tampered with. To verify the signature at your end, you need your Cashfree Payment Gateway secret key along with the payload.

  • The timestamp is present in the header x-webhook-timestamp.
  • The actual signature is present in the header x-webhook-signature.

The signature creation logic is as below:

timestamp := 1617695238078; 
signedPayload := $timestamp.$payload;
expectedSignature := Base64Encode(HMACSHA256($signedPayload, $merchantSecretKey));

📘

Note

Sometimes you may receive the same webhook more than once. It is recommended to ensure that your implementation of the webhook is idempotent.

Sample Code

Fetch The Raw JSON and Headers

//Set up your server like this 
var express = require('express')
var bodyParser = require('body-parser');
var crypto = require('crypto');
var app = express()

//This part is to get the rawBody
app.use(
    express.json({
        limit: '1mb',
        verify: (req, res, buf) => {
        req.rawBody = buf.toString();
        },
    })
);
app.use(bodyParser.json());
//This is your endpoint
app.post('/webhook', function(req, res) {
    console.log(req.rawBody);
    const ts = req.headers["x-webhook-timestamp"]
    const signature = req.headers["x-webhook-signature"]  
    const currTs = Math.floor(new Date().getTime() / 1000)
  	if(currTs - ts > 30000){
    	res.send("Failed")
  	}  
    const genSignature = verify(ts, req.rawBody)
    if(signature === genSignature){
        res.send('OK')
    } else {
        res.send("failed")
    } 
})
...
func main() {
	e := echo.New()

	e.POST("/webhook", webhook)
	fmt.Println("Start server ....")
	e.Logger.Fatal(e.Start(":8080"))

}

func webhook(c echo.Context) error {
	signature := c.Request().Header.Get("x-webhook-signature")
	tsStr := c.Request().Header.Get("x-webhook-timestamp")
	timestamp, err := strconv.ParseInt(tsStr, 0, 64)
	if err != nil {
		fmt.Println("failed in getting proper timestamp")
		return c.String(http.StatusBadRequest, "Bad request")
	}
	slurp1, err := io.ReadAll(c.Request().Body)
	if err != nil {
		return c.String(http.StatusBadRequest, "Bad request")
	}
	req := string(slurp1)
	fmt.Println("Raw request body:", req)
	genSignature, err := VerifySignature(signature, timestamp, req)
	if err != nil {
		return c.String(http.StatusOK, "Failure in verifying signature")
	}
	fmt.Println("generated signature: ", genSignature)
	fmt.Println("expected signature: ", signature)
	matched := signature == genSignature
	fmt.Println("match? ", matched)
	return c.String(http.StatusOK, "Request completed")
}
<?php

$inputJSON = file_get_contents('php://input');

$expectedSig = getallheaders()['x-webhook-signature'];
$ts = getallheaders()['x-webhook-timestamp'];

if(!isset($expectedSig) || !isset($ts)){
    echo "Bad Request";
    die();
}
//defined below
$computeSig = computeSignature($ts, $inputJSON);
$matches = $expectedSig == $computeSig;
?>

Compute Signature and Verify

function verify(ts, rawBody){
    const body = ts + rawBody
    const secretKey = "<your secret key>";
    let genSignature = crypto.createHmac('sha256',secretKey).update(body).digest("base64");
    return genSignature
}
func VerifySignature(expectedSig string, ts int64, body string) (string, error) {
	t := time.Now()
	currentTS := t.Unix()
	if currentTS-ts > 1000*300 {
		return "", errors.New("webhook delivered too late")
	}
	signStr := strconv.FormatInt(ts, 10) + body
	fmt.Println("signing String: ", signStr)
	key := ""
	h := hmac.New(sha256.New, []byte(key))
	h.Write([]byte(signStr))
	b := h.Sum(nil)
	return base64.StdEncoding.EncodeToString(b), nil
}
function computeSignature($ts, $rawBody){
    $signStr = $ts . $rawBody;
    $key = "";
    $computeSig = base64_encode(hash_hmac('sha256', $signStr, $key, true));
    return $computeSig;
}
public String generateSignature() {
 	String payload = "json payload in string";
  String timestamp = "x-webhook-timestamp";
  String data = timestamp+payload;

  String secretKey = "SECRET-KEY"; // Get secret key from Cashfree Merchant Dashboard;
  Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
  SecretKeySpec secret_key_spec = new SecretKeySpec(secretKey.getBytes(),"HmacSHA256");
  sha256_HMAC.init(secret_key_spec);
  String computed_signature = Base64.getEncoder().encodeToString(sha256_HMAC.doFinal(data.getBytes()));
  return computed_signature; // compare with "x-webhook-signature"
}
import base64
import hashlib
import hmac

def generateSignature():    
    timestamp = "timestamp data"
    payload = 'json data in string'
    signatureData = timestamp+payload
    message = bytes(signatureData, 'utf-8')
    secretkey=bytes("Secret_Key",'utf-8') #Get Secret_Key from Cashfree Merchant Dashboard.
    signature = base64.b64encode(hmac.new(secretkey, message, digestmod=hashlib.sha256).digest())
    computed_signature = str(signature, encoding='utf8')
    return computed_signature #compare with "x-webhook-signature"