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
- Extract Signature: Extract the signature from the webhook.
- Extract Data: Extract the relevant data fields from the webhook.
- Recreate Message: Recreate the message by concatenating the extracted data fields.
- Compute Signature: Compute the signature using the same algorithm and secret key used for generating the original signature.
- Compare Signatures: Compare the extracted signature with the computed signature.
- Compare Amount and Currency: Compare the received amount and currency with the details in your DB for the given merchant_order_ref
- 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
amount
float64
The amount of the transaction
channel_key
channel_key
string
The key of the payment channel
channel_order_ref
channel_order_ref
string
Payment Channel's Order Reference
country_code
country_code
string
The country code of the transaction
currency
currency
string
The currency of the transaction
merchant_order_ref
merchant_order_ref
string
Order Reference sent by merchant to initiate transaction
method_name
method_name
string
The name of the payment method used for the transaction
order_ref
order_ref
string
PortOne's Order Reference, used further for refund and other disputes
status
status
string
The current status of the transaction
signature_hash
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.
Updated about 2 months ago