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

Verify Signature using SDK

var express = require('express')
import { Cashfree } from "cashfree-pg"; 
var app = express()

Cashfree.XClientId = "<x-client-id>";
Cashfree.XClientSecret = "<x-client-secret>";
Cashfree.XEnvironment = Cashfree.Environment.SANDBOX;

app.post('/webhook', function (req, res) {
    try {
        Cashfree.PGVerifyWebhookSignature(req.headers["x-webhook-signature"], req.rawBody, req.headers["x-webhook-timestamp"]))
    } catch (err) {
        console.log(err.message)
    }
})
import (
  cashfree "github.com/cashfree/cashfree-pg/v4"
)

// Route
e.POST("/webhook", _this.Webhook)

// Controller
func (_this *WebhookRoute) Webhook(c echo.Context) error {
  	clientId := "<x-client-id>"
		clientSecret := "<x-client-secret>"
		cashfree.XClientId = &clientId
		cashfree.XClientSecret = &clientSecret
		cashfree.XEnvironment = cashfree.SANDBOX
  
    signature := c.Request().Header.Get("x-webhook-signature")
    timestamp := c.Request().Header.Get("x-webhook-timestamp")
    
    body, _ := ioutil.ReadAll(c.Request().Body)
    rawBody := string(body)
    webhookEvent, err := cashfree.PGVerifyWebhookSignature(signature, rawBody, timestamp)
    if err != nil {
		fmt.Println(err.Error())
	} else {
		fmt.Println(webhookEvent.Object)
	}
}
<?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();
}

\Cashfree\Cashfree::$XClientId = "<x-client-id>";
\Cashfree\Cashfree::$XClientSecret = "<x-client-secret>";
$cashfree = new \Cashfree\Cashfree();

try {
 $response =  cashfree->PGVerifyWebhookSignature($expectedSig, $inputJSON, $ts);
} catch(Exception $e) {
  // Error if signature verification fails
}
?>
from cashfree_pg.api_client import Cashfree

@app.route('/webhook', methods = ['POST'])
def disp():
		# Get the raw body from the request
    raw_body = request.data
  
    # Decode the raw body bytes into a string
    decoded_body = raw_body.decode('utf-8')

    #verify_signature
    timestamp = request.headers['x-webhook-timestamp']
    signature = request.headers['x-webhook-signature'
		
		cashfree = Cashfree()
		cashfree.XClientId = "<app_id>"
		cashfree.XClientSecret = "<secret_key>"
		try:
			cashfreeWebhookResponse = cashfree.PGVerifyWebhookSignature(signature, decoded_body, timestamp)
		except:
			# If Signature mis-match
import com.cashfree.*;

@PostMapping("/my-endpoint")
public String handlePost(HttpServletRequest request) throws IOException {      
    Cashfree.XClientId = "<x-client-id>";
		Cashfree.XClientSecret = "<x-client-secret>";
		Cashfree.XEnvironment = Cashfree.SANDBOX;
  
  	StringBuilder stringBuilder = new StringBuilder();
    BufferedReader bufferedReader = null;

    try {           
      bufferedReader = request.getReader();
      String line;
      while ((line = bufferedReader.readLine()) != null) {
              stringBuilder.append(line).append('\n');
      }
        
        
      String rawBody = stringBuilder.toString();
      String signature = request.getHeader("x-webhook-signature");
      String timestamp = request.getHeader("x-webhook-timestamp");
        
      Cashfree cashfree = new Cashfree();
      PGWebhookEvent webhook = cashfree.PGVerifyWebhookSignature(signature, rawBody, timestamp);
          
    } catch (Exception e) {
            // Error if verification fails
    } finally {
         if (bufferedReader != null) {
            bufferedReader.close();
		}
	}
      
}
using cashfree_pg.Client;
using cashfree_pg.Model;


		[Route("api/[controller]")]
    [ApiController]
    public class YourController : ControllerBase
    {
        [HttpPost]
        public async Task<IActionResult> Post()
        {
            // Read the raw body of the POST request
            using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
            {
                string requestBody = await reader.ReadToEndAsync();
                var headers = Request.Headers;
                var signature = headers["x-webhook-signature"];
                var timestamp = headers["x-webhook-timestamp"];
                
                Cashfree.XClientId = "<x-client-id>";
                Cashfree.XClientSecret = "<x-client-secret>";
                Cashfree.XEnvironment = Cashfree.SANDBOX;
								var cashfree = new Cashfree();
                
                try {
                var response = cashfree.PGVerifyWebhookSignature(signature, requestBody, timestamp);
                } catch(Exception e) {
                // Error if signature mis matches
                }
            }
        }
    }

Compute Signature and Verify manually

function verify(ts, rawBody){
    const body = req.headers["x-webhook-timestamp"] + req.rawBody;
    const secretKey = "<your secret key>";
    let genSignature = crypto.createHmac('sha256',secretKey).update(body).digest("base64");
    return genSignature
}
func VerifySignature(expectedSig string, ts string, 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
}

timestamp := c.Request().Header.Get("x-webhook-timestamp")
body, _ := ioutil.ReadAll(c.Request().Body)
rawBody := string(body)
signature := c.Request().Header.Get("x-webhook-signature")

VerifySignature(signature, timestamp, rawBody)
function computeSignature($ts, $rawBody){
    $rawBody = file_get_contents('php://input');
		$ts = getallheaders()['x-webhook-timestamp'];
  
    $signStr = $ts . $rawBody;
    $key = "";
    $computeSig = base64_encode(hash_hmac('sha256', $signStr, $key, true));
    return $computeSig;
}
public String generateSignature() {

	bufferedReader = request.getReader();
  String line;
  while ((line = bufferedReader.readLine()) != null) {
     stringBuilder.append(line).append('\n');
  }      
  String payload = stringBuilder.toString();
  String timestamp = request.getHeader("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():    
    # Get the raw body from the request
    raw_body = request.data
  
    # Decode the raw body bytes into a string
    payload = raw_body.decode('utf-8')

    #verify_signature
    timestamp = request.headers['x-webhook-timestamp']
    
    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"