Payment Webhook Response

To ensure the integrity and authenticity of webhook, you can use signature verification. The signature is a cryptographic hash generated using a secret key and the content of the webhook.

Steps for Signature Verification

  1. Extract Signature: Extract the signature from the webhook.
  2. Extract Data: Extract the relevant data fields from the webhook.
  3. Recreate Message: Recreate the message by concatenating the extracted data fields.
  4. Compute Signature: Compute the signature using the same algorithm and secret key used for generating the original signature.
  5. Compare Signatures: Compare the extracted signature with the computed signature.
  6. Compare Amount and Currency: Compare the received amount and currency with the details in your DB for the given merchant_order_ref
  7. Verification: If the two signatures match, the webhook is considered valid. Otherwise, it may have been tampered with.

Example Code

Payment Webhook

Code samples to generate and verify signature hash for the data received via Payment Status webhooks sent from PortOne servers.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"net/url"
	"strconv"
)

type WebhookResponse struct {
	Amount           float64
	ChannelKey       string
	ChannelOrderRef  string
	CountryCode      string
	Currency         string
	MerchantOrderRef string
	MethodName       string
	OrderRef         string
	SignatureHash    string
	Status           string
}

func VerifySignature(webhookResponse WebhookResponse, secretKey string) bool {
	// Create a url.Values map and add the necessary parameters
	params := make(url.Values)
	params.Add("amount", strconv.FormatFloat(webhookResponse.Amount, 'f', -1, 64))
	params.Add("channel_key", webhookResponse.ChannelKey)
	params.Add("channel_order_ref", webhookResponse.ChannelOrderRef)
	params.Add("country_code", webhookResponse.CountryCode)
	params.Add("currency", webhookResponse.Currency)
	params.Add("merchant_order_ref", webhookResponse.MerchantOrderRef)
	params.Add("method_name", webhookResponse.MethodName)
	params.Add("order_ref", webhookResponse.OrderRef)
	params.Add("status", webhookResponse.Status)

	// Encode the parameters
	data := params.Encode()

	// Create the HMAC hash using SHA-256
	secret := []byte(secretKey)
	message := []byte(data)
	hash := hmac.New(sha256.New, secret)
	hash.Write(message)

	// Convert the hash to a base64 string
	hashValue := base64.StdEncoding.EncodeToString(hash.Sum(nil))

	// Compare the computed hash with the signature received in the payment response
	if hashValue != webhookResponse.SignatureHash {
		fmt.Println("Hash verification failed, not from valid source")
		return false
	} else {
		fmt.Println("Hash verification succeeded")
		return true
	}
}

func main() {

	portOneSecret := "PORTONE_SECRET"

        // Verify the amount and currency received in the webhook matches with data in your system
	var amount float64
	amount = 100.25
        currency := "currency"
        // Define the webhook response struct below with the respective params received in webhook
	webhookResponse := WebhookResponse{
		Amount:           amount,
		ChannelKey:       "channel_key",
		ChannelOrderRef:  "channel_order_ref",
		CountryCode:      "country_code",
		Currency:         currency,
		MerchantOrderRef: "merchant_order_ref",
		MethodName:       "method_name",
		OrderRef:         "order_ref",
		SignatureHash:    "signature_hash",
		Status:           "status",
	}

	// Verify the signature
	isValid := VerifySignature(webhookResponse, portOneSecret)

	// Output the result of the verification
	if isValid {
		fmt.Println("Webhook response is valid.")
	} else {
		fmt.Println("Webhook response is invalid.")
	}
}
<?php
function VerifySignature($webhookResponseObj, $secretKey) {
    // Extract the received signature hash from the webhookResponseObj object
    $signature_hash = $webhookResponseObj->signature_hash;

    // Create an array with the necessary parameters
    $data = array(
        'amount' => $webhookResponseObj->amount,
        'channel_key' => $webhookResponseObj->channel_key,
        'channel_order_ref' => $webhookResponseObj->channel_order_ref,
        'country_code' => $webhookResponseObj->country_code,
        'currency' => $webhookResponseObj->currency,
        'merchant_order_ref' => $webhookResponseObj->merchant_order_ref,
        'method_name' => $webhookResponseObj->method_name,
        'order_ref' => $webhookResponseObj->order_ref,
        'status' => $webhookResponseObj->status,
    );

    // Sort the array by keys
    ksort($data);

    // Build the query string
    $message = http_build_query($data);
    // Generate the HMAC hash using SHA-256 and encode it in base64
    $hash_value = base64_encode(hash_hmac('sha256', $message, $secretKey, true));

    // Compare the generated hash with the received signature hash
    if ($hash_value !== $signature_hash) {
        echo "Hash verification failed, not from valid source\n";
        return false;
    } else {
        echo "Hash verification succeeded\n";
        return true;
    }
}

// Define the main function
function main() {
    $secret_key = "PORTONE_SECRET";
    // Verify the amount and currency received in the webhook matches with data in your system
    $amount = 100.25
    $currency = "currency"

    // Define the webhook response struct below with the respective params received in webhook
    $webhookResponseObj = (object) [
        'amount' => $amount,
        'channel_key' => 'channel_key',
        'channel_order_ref' => 'channel_order_ref',
        'country_code' => 'country_code',
        'currency' => $currency,
        'merchant_order_ref' => 'merchant_order_ref',
        'method_name' => 'method_name',
        'order_ref' => 'order_ref',
        'status' => 'status',
        'signature_hash' => 'signature_hash',
    ];

    // Verify the signature
    $isValid = VerifySignature($webhookResponseObj, $secret_key);

    // Print the result of the verification
    if ($isValid) {
        echo "Webhook response is valid.\n";
    } else {
        echo "Webhook response is invalid.\n";
    }
}

// Call the main function
main();

?>
const crypto = require('crypto');
const { URLSearchParams } = require('url');

function VerifySignature(webhookResponseObj, secretKey) {
    const params = new URLSearchParams();
    params.append('currency', webhookResponseObj.currency)
    params.append('amount', webhookResponseObj.amount)
    params.append('order_ref', webhookResponseObj.order_ref)
    params.append('merchant_order_ref', webhookResponseObj.merchant_order_ref)
    params.append('channel_order_ref', webhookResponseObj.channel_order_ref)
    params.append('country_code', webhookResponseObj.country_code)
    params.append('status', webhookResponseObj.status)
    params.append('channel_key', webhookResponseObj.channel_key)
    params.append('method_name', webhookResponseObj.method_name)

    params.sort();
    const message = params.toString()
    const hash_value =  crypto.createHmac('sha256', secretKey).update(message).digest('base64');

    if(hash_value !==  webhookResponseObj.signature_hash){
      console.log("Hash verification failed, not from valid source")
      return false;
    } else{
      console.log("Hash verification succeded")
      return true;
    }
}

// Main function to demonstrate calling VerifySignature
function main() {
    const secretKey = 'PORTONE_SECRET';

    // Verify the amount and currency received in the webhook matches with data in your system
    const amount = 100.25
    const currency = 'currency'

    // Example webhookResponseObj object
    const webhookResponseObj = {
        amount: amount,
        channel_key: 'channel_key',
        channel_order_ref: 'channel_order_ref',
        country_code: 'country_code',
        currency: currency,
        merchant_order_ref: 'merchant_order_ref',
        method_name: 'method_name',
        order_ref: 'order_ref',
        status: 'status',
        signature_hash: 'signature_hash'
    };

    // Verify the signature
    const isValid = VerifySignature(webhookResponseObj, secretKey);

    // Print the result of the verification
    if (isValid) {
        console.log("Webhook response is valid.");
    } else {
        console.log("Webhook response is invalid.");
    }
}

// Call the main function
main();
using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;

namespace ConsoleApp
{
    class WebhookResponse
    {
	public double amount;
        public string currency;
        public string order_ref;
        public string merchant_order_ref;
        public string country_code;
        public string channel_order_ref;
        public string status;
        public string channel_key;
        public string method_name;
        public string signature_hash;
    }

    class ApiSecurityExample
    {
        public static bool VerifySignature(WebhookResponse webhookResponse, string secret)
        {
            var map = new SortedDictionary<string, string>()
            {
	        { "amount", RemoveTrailingZeros(webhookResponse.amount) },
		{ "channel_key", webhookResponse.channel_key },
		{ "channel_order_ref", webhookResponse.channel_order_ref },
		{ "country_code", webhookResponse.country_code },
		{ "currency", webhookResponse.currency },
		{ "merchant_order_ref", webhookResponse.merchant_order_ref },
		{ "method_name", webhookResponse.method_name },
                { "order_ref", webhookResponse.order_ref },
                { "status", webhookResponse.status },
            };

            var stringBuilder = new StringBuilder();
            foreach (var key in map.Keys)
            {
                if (stringBuilder.Length > 0)
                {
                    stringBuilder.Append("&");
                }
                var value = map[key];
                try
                {
                    stringBuilder.Append((key != null ? Uri.EscapeDataString(key) : ""));
                    stringBuilder.Append("=");
                    stringBuilder.Append(value != null ? Uri.EscapeDataString(value) : "");
                }
                catch (ArgumentNullException e)
                {
                    throw new Exception("The key or value is null.", e);
                }
                catch (UriFormatException e)
                {
                    throw new Exception("Invalid format for key or value.", e);
                }
            }

            var message = stringBuilder.ToString();
            // Console.WriteLine("message: " + message);
            var encoding = new ASCIIEncoding();
            byte[] keyByte = encoding.GetBytes(secret);
            byte[] messageBytes = encoding.GetBytes(message);
            var hmacsha256 = new HMACSHA256(keyByte);
            byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
            string hash_value = Convert.ToBase64String(hashmessage);
			
            if (hash_value != webhookResponse.signature_hash) {
                Console.WriteLine("Hash verification failed, not from valid source");
                return false;
              } else {
                Console.WriteLine("Hash verification succeded");
                return true;
              }
        }
		
		private static string RemoveTrailingZeros(double amount)
        {
            return amount.ToString("0.###################");
        }

    }

class Program
    {
      static void Main(string[] args)
        {
            string secret = "PORTONE_SECRET";
            // Verify the amount and currency received in the webhook matches with data in your system
            double amount = 100.25;
            string currency = "currency";
            WebhookResponse webhookResponse = new WebhookResponse()
            {
		amount = amount,
		channel_key = "channel_key",
		channel_order_ref = "channel_order_ref",
		country_code = "country_code",
		currency = currency,
                merchant_order_ref = "merchant_order_ref",
		method_name = "method_name",
                order_ref = "order_ref",
                status = "status",
                signature_hash = "signature_hash"
            };

            bool isValid = ApiSecurityExample.VerifySignature(webhookResponse, secret);

            // Print the result
            if (isValid) {
                Console.WriteLine("Webhook response is valid.");
            } else {
                Console.WriteLine("Webhook response is invalid.");
            }
        }
    }
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;

public class Main {
    public static void main(String[] args) {
        String secret = "PORTONE_SECRET";

        // Create WebhookResponse object using setter methods
        WebhookResponse webhookResponse = new WebhookResponse();

        // Verify the amount and currency received in the webhook matches with data in your system
        Double amount = 100.25;
        String currency = "currency";
        webhookResponse.setAmount(amount);
        webhookResponse.setChannelKey("channel_key");
        webhookResponse.setChannelOrderRef("channel_order_ref");
        webhookResponse.setCountryCode("country_code");
        webhookResponse.setCurrency(currency);
        webhookResponse.setMerchantOrderRef("merchant_order_ref");
        webhookResponse.setMethodName("method_name");
        webhookResponse.setOrderRef("order_ref");
        webhookResponse.setSignature("signature");
        webhookResponse.setStatus("status");

        boolean isValid = verifySignature(webhookResponse, secret);

        // Print the result
        if (isValid) {
            System.out.println("Webhook response is valid.");
        } else {
            System.out.println("Webhook response is invalid.");
        }
    }

    public static boolean verifySignature(WebhookResponse webhookResponse, String secretKey) {
        try {
            Map<String, String> params = new TreeMap<>();
            params.put("amount", webhookResponse.getAmount());
            params.put("channel_key", webhookResponse.getChannelKey());
            params.put("channel_order_ref", webhookResponse.getChannelOrderRef());
            params.put("country_code", webhookResponse.getCountryCode());
            params.put("currency", webhookResponse.getCurrency());
            params.put("merchant_order_ref", webhookResponse.getMerchantOrderRef());
            params.put("method_name", webhookResponse.getMethodName());
            params.put("order_ref", webhookResponse.getOrderRef());
            params.put("status", webhookResponse.getStatus());

            // Encode the parameters
            String data = encodeParams(params);

            byte[] secret = secretKey.getBytes();
            byte[] message = data.getBytes();

            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secret, "HmacSHA256");
            sha256_HMAC.init(secretKeySpec);

            byte[] hash = sha256_HMAC.doFinal(message);
            String computedSignature = Base64.getEncoder().encodeToString(hash);

            if (!computedSignature.equals(webhookResponse.getSignature())) {
                System.out.println("Hash verification failed, not from a valid source");
                return false;
            } else {
                System.out.println("Hash verification succeeded");
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    
    public static String encodeParams(Map<String, String> params) {
        StringBuilder encodedParams = new StringBuilder();

        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (encodedParams.length() > 0) {
                encodedParams.append("&");
            }
            encodedParams.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8))
                         .append("=")
                         .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
        }

        return encodedParams.toString();
    }
}

class WebhookResponse {
    private Double amount;
    private String channelKey;
    private String channelOrderRef;
    private String countryCode;
    private String currency;
    private String merchantOrderRef;
    private String methodName;
    private String orderRef;
    private String signature;
    private String status;

    // Default constructor
    public WebhookResponse() {
    }

    // Setter methods
    public void setOrderRef(String orderRef) {
        this.orderRef = orderRef;
    }

    public void setChannelOrderRef(String channelOrderRef) {
        this.channelOrderRef = channelOrderRef;
    }

    public void setMerchantOrderRef(String merchantOrderRef) {
        this.merchantOrderRef = merchantOrderRef;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public void setSignature(String signature) {
        this.signature = signature;
    }

    public void setAmount(Double amount) {
        this.amount = amount;
    }

    public void setChannelKey(String channelKey) {
        this.channelKey = channelKey;
    }

    public void setCountryCode(String countryCode) {
        this.countryCode = countryCode;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    // Getter methods
    public String getCurrency() {
        return currency;
    }

    public String getAmount() {
        DecimalFormat df = new DecimalFormat("0.##");
        return df.format(amount);
    }

    public String getOrderRef() {
        return orderRef;
    }

    public String getMerchantOrderRef() {
        return merchantOrderRef;
    }

    public String getCountryCode() {
        return countryCode;
    }

    public String getChannelOrderRef() {
        return channelOrderRef;
    }

    public String getStatus() {
        return status;
    }

    public String getChannelKey() {
        return channelKey;
    }

    public String getMethodName() {
        return methodName;
    }

    public String getSignature() {
        return signature;
    }
}
#!/usr/bin/python
# -*- coding: utf-8 -*-
import urllib.parse
import hashlib
import hmac
import base64

class WebhookResponseObj:
    def __init__(self, currency, amount, order_ref, merchant_order_ref, channel_order_ref, country_code, status, channel_key, method_name, signature_hash):
        # Instance Variables
        self.amount = amount
        self.channel_key = channel_key
        self.channel_order_ref = channel_order_ref
        self.country_code = country_code
        self.currency = currency
        self.merchant_order_ref = merchant_order_ref
        self.method_name = method_name
        self.order_ref = order_ref
        self.signature_hash = signature_hash
        self.status = status

def VerifySignature(webhookResponseObj, secretKey):
    f = {
        'amount': f"{webhookResponseObj.amount:.2f}".rstrip('0').rstrip('.'),
        'channel_key': webhookResponseObj.channel_key,
        'channel_order_ref': webhookResponseObj.channel_order_ref,
        'country_code': webhookResponseObj.country_code,
        'currency': webhookResponseObj.currency,
        'merchant_order_ref': webhookResponseObj.merchant_order_ref,
        'method_name': webhookResponseObj.method_name,
        'order_ref': webhookResponseObj.order_ref,
        'status': webhookResponseObj.status,
    }
    
    f = dict(sorted(f.items()))
    message1 = urllib.parse.urlencode(f)
    message = message1.encode('utf-8')
    secret = secretKey.encode('utf-8')
    signature = base64.b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest()).decode('utf-8')

    if signature != webhookResponseObj.signature_hash:
        print("Hash verification failed, not from valid source\n")
        return False
    else:
        print("Hash verification succeeded\n")
        return True

# Define constants
secret = 'PORTONE_SECRET'
# Verify the amount and currency received in the webhook matches with data in your system
amount = 100.25
currency = 'currency'

# Create an instance of WebhookResponseObj
webhookResponseObj = WebhookResponseObj(
    amount=amount,
    channel_key='channel_key',
    channel_order_ref='channel_order_ref',
    country_code='country_code',
    currency=currency,
    merchant_order_ref='merchant_order_ref',
    method_name='method_name',
    order_ref='order_ref',
    signature_hash='signature_hash',
    status='status'
)

# Call VerifySignature
isValid = VerifySignature(webhookResponseObj, secret)
if isValid:
    print("Webhook response is valid.\n")
else:
    print("Webhook response is invalid.\n")

Parameter list to generate signature

amount
float64

The amount of the transaction


channel_key
string

The key of the payment channel


channel_order_ref
string

Payment Channel's Order Reference


country_code
string

The country code of the transaction


currency
string

The currency of the transaction


merchant_order_ref
string

Order Reference sent by merchant to initiate transaction


method_name
string

The name of the payment method used for the transaction


order_ref
string

PortOne's Order Reference, used further for refund and other disputes


status
string

The current status of the transaction


signature_hash
string

Calculate hash as mentioned earlier, verify if it is equal to signature_hash received in the payment webhook response


📘

Note

Refer to Payment Webhook Parameters for the complete list of payment webhook parameters.