OPay Callback Notification Signature
Whenever the status of any of your transaction has been updated, e.g. a reference code payment has been paid, OPay
will keep you informed at your designated callBackUrl
. Since anyone can get hold of your
endpoint and attempt to send you phony event objects for malicious purposes
(e.g. to see if they can mark their subscription to your product as renewed just in case
you aren't running any verifications on the transaction reference),
it is important to verify that callbacks originate from OPay.
You can do any or both of the below to verify callbacks from OPay:
- Watch the IPs and accept callbacks only from our IPs.
- Validate the Signature as described in the section that follows.
In this page, you will learn:
- How to calculate the signature of the received callback.
- How to validate OPay callback signature.
Callback Signature Calculator
HMac-SHA3-512 signature of the callback payload
Valid callbacks are raised with sha512
value, which is essentially a HMAC-SHA3-512 signature of the callback payload signed using your Private Key.
The following section helps you generate a signature from the HMAC computation of the private key and payload.
Callback Signature Validation
- HMac-SHA3-512 signature of the callback payload
Valid callbacks are raised with Hmac sha3-512 value, which is essentially a HMac-SHA3-512 signature of the callback payload. Yes, signed using your Private Key.
Example signature payload params:
There are two values for Refunded, when payload.refunded is true, it is t, otherwise it is f.
{
"payload":{
"amount":"49160",
"channel":"Web",
"country":"NG",
"currency":"NGN",
"displayedFailure":"",
"fee":"737",
"feeCurrency":"NGN",
"instrumentType":"BankCard",
"reference":"10023",
"refunded":false,
"status":"SUCCESS",
"timestamp":"2022-05-07T06:20:46Z",
"token":"220507145660712931829",
"transactionId":"220507145660712931829",
"updated_at":"2022-05-07T07:20:46Z"
},
"sha512":"9f605d69f04e94172875dc156537071cead060bbcaeaca94a7b8805af9f89611e2fdf6836713c9c90b028ca7e4470b1356e996975f2abc862315aaa9b7f2ae2d",
"type":"transaction-status"
}
-Code Example:
class NiCallBackAutoController
{
private $secretkey;
private $merchantId;
public function __construct() {
$this->merchantId = '256621051120756';
$this->secretkey = 'OPAYPRV**************************98453';
}
public function test(Request $request)
{
$input = $request->all();
if(empty($input) || !isset($input['payload'])){
return 'Parameter error';
}
if (!isset($input['payload']['reference']) && !isset($input['payload']['transactionId']) && !isset($input['payload']['status']) && !isset($input['payload']['amount']) && !isset($input['payload']['currency']) && !isset($input['payload']['refunded']) && !isset($input['payload']['timestamp']) && !isset($input['payload']['token']) && !isset($input['payload']['transactionId']) && !isset($input['sha512'])) {
return 'Parameter error';
}
$reference = $input['payload']['reference'];
$status = $input['payload']['status'];
$amount = $input['payload']['amount'];
$currency = $input['payload']['currency'];
$refunded = $input['payload']['refunded'];
$timestamp = $input['payload']['timestamp'];
$token = $input['payload']['token'];
$transactionId = $input['payload']['transactionId'];
$authJson = sprintf("{Amount:\"%s\",Currency:\"%s\",Reference:\"%s\",Refunded:%s,Status:\"%s\",Timestamp:\"%s\",Token:\"%s\",TransactionID:\"%s\"}", $amount, $currency, $reference, $refunded?"t":"f", $status, $timestamp, $token, $transactionId);
$result = $this->auth($authJson);
return $result;
}
public function auth ( $authJson ) {
$secretKey = $this->secretkey;
$auth = hash_hmac('sha3-512', $authJson, $secretKey);
return $auth;
}
}
package com.opay.sdk.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.util.Map;
public class SignatureUtils {
static{
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null){
Security.addProvider(new BouncyCastleProvider());
}
}
public static void main(String[] args) throws Exception{
String requestBody = "{\"payload\":{\"amount\":\"49160\",\"channel\":\"Web\",\"country\":\"NG\",\"currency\":\"NGN\",\"displayedFailure\":\"\",\"fee\":\"737\",\"feeCurrency\":\"NGN\",\"instrumentType\":\"BankCard\",\"reference\":\"10023\",\"refunded\":false,\"status\":\"SUCCESS\",\"timestamp\":\"2022-05-07T06:20:46Z\",\"token\":\"220507145660712931829\",\"transactionId\":\"220507145660712931829\",\"updated_at\":\"2022-05-07T07:20:46Z\"},\"sha512\":\"9f605d69f04e94172875dc156537071cead060bbcaeaca94a7b8805af9f89611e2fdf6836713c9c90b028ca7e4470b1356e996975f2abc862315aaa9b7f2ae2d\",\"type\":\"transaction-status\"}";
ObjectMapper objectMapper = new ObjectMapper();
PaymentStatusCallBackRequest callBack = objectMapper.readValue(requestBody,PaymentStatusCallBackRequest.class);
System.out.println(verifyPaymentNotifySignature(callBack,"OPAYPRV16498196872570.13953388019021462"));
}
public static boolean verifyPaymentNotifySignature(PaymentStatusCallBackRequest callback, String privateKey){
String format = "{Amount:\"%s\",Currency:\"%s\",Reference:\"%s\",Refunded:%s,Status:\"%s\",Timestamp:\"%s\",Token:\"%s\",TransactionID:\"%s\"}";
String signContent = String.format(format,
callback.payload.amount,
callback.payload.currency,
callback.payload.reference,
callback.payload.refunded ? "t" : "f",
callback.payload.status,
callback.payload.timestamp,
callback.payload.token == null ? "" : callback.payload.token,
callback.payload.transactionId
);
String sign = buildHmacSha3_512(signContent, privateKey);
return callback.sha512.equalsIgnoreCase(sign);
}
public static String buildHmacSha3_512(String value, String secret){
byte[] keyBytes = secret.getBytes();
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HMac-SHA3-512");
Mac mac = null;
try {
mac = Mac.getInstance("HMac-SHA3-512", "BC");
mac.init(signingKey);
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException e) {
throw new RuntimeException(e);
}
byte[] rawHmac = mac.doFinal(value.getBytes());
return byte2hex(rawHmac);
}
public static String byte2hex(byte[] b) {
String hs = "";
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1) {
hs = hs + "0" + stmp;
} else {
hs = hs + stmp ;
}
}
return hs.toLowerCase();
}
public static class PaymentStatusCallBackRequest {
private Payload payload;
private String sha512;
private String type;
public Payload getPayload() {
return payload;
}
public void setPayload(Payload payload) {
this.payload = payload;
}
public String getSha512() {
return sha512;
}
public void setSha512(String sha512) {
this.sha512 = sha512;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
public static class Payload{
private String amount;
private String channel;
private String country;
private String currency;
private String displayedFailure;
private String fee;
private String feeCurrency;
private String instrumentType;
private String reference;
private boolean refunded;
private String status;
private String timestamp;
private String token;
private String transactionId;
private String updated_at;
private Map