A webhook is a software architecture approach that allows applications and services to submit a web-based notification to other applications whenever a specific event occurs.
In Zenkipay world, webhooks are used to notify your application when a payment is made or when a payment is refunded.
How to configure a webhook?
To configure a webhook, you need to create a webhook endpoint in your application. This endpoint will receive the notification from Zenkipay.
Once you have created your webhook endpoint, you need to configure it in your Zenkipay account. To do so, got to configure your Zenkipay Webhook in Zenkipay Portal
What events can be notified?
Zenkipay will send a POST request to your webhook endpoint with a JSON payload containing the information about the event.
Zenkipay can notify you about the following events:
Date time when the webhook has been notified in UTC
long
No
eventDetailsVersion
Details event version used for this notificiation, start from v1
String
No
eventDetails
JSON Object with event details information
Object
No
orderId
Unique identifier for your Order in Zenkipay
String
No
merchantOrderId
Unique identifier for your Order in your system (if informed)
String
Yes
shopperCartId
Unique identifier for your Cart in your system (if informed)
String
Yes
transactionStatus
Transaction status in Zenkipay. For payment completed successfully = COMPLETED
String
No
orderStatus
Estatus for Order in Zenkipay
String
No
placedAt
Order placed at
long
No
totalAmount
FIAT total amount for this order
number
No
currency
Currency used for this order
String
No
merchantPayment
JSON Object for representation of payment to Merchant for this order in Crypto
Object
No
amount
Crypto amount for this order
number
No
currency
Crypto currency asset name
String
No
exchangeRate
Exchange rate data to calculate value in USD for this order
Object
No
from
Currency from order
String
No
to
Currency used to calculate Merchant payment, always USD
String
No
value
Exchange amount
number
No
timestamp
Date time for exchange request (UTC)
long
No
transactionHash
Unique identifier for this transaction in blockchain
String
No
cryptoLoveFiatAmount
Calculated amount for crypto love to Shopper
number
Yes
cryptoLovePercentage
Percentage configured in Zenkipay for Crypto love discounts
number
No
originalPurchaseData
String used to register oder in Zenkipay
String
Yes
merchantPluginId
Unique identifier for your plugin requests
String
Yes
How to decrypt my Zenkipay Order Payload Event data?
Using your RSA private key, you must decrypt the order event information as follows:
1
2
3
4
5
6
7
8
9
# This is just an example of pseudocode
# You should implement it according to
# your back end programming language.
privateKey = readPrivateKeyFile(PATH + PrivateKeyFile)
webhookPayloadString = webhookPayloadJsonString.read()
webhookPayloadEncryptedDataBytes = Base64.decode(webhookPayloadEncryptedData).readBytes(UTF-8)
decryptedMessageBytes = blockCipher(webhookPayloadEncryptedDataBytes) // Decrypt blocks according your RSA Key Size (4096/8)
decryptedMessageString = new String(decryptedMessageBytes, UTF-8)
/**
* Decrypt message with RSA private key
*
* @param string $encrypted_msg Base64 string holds the encrypted message.
*
* @return string decrypted message.
*/publicfunctiondecryptSecretMessage($encrypted_msg){$private_key_file=file_get_contents('/path/to/the/private-key.pem');$private_key=openssl_pkey_get_private($private_key_file);$encrypted_msg=base64_decode($encrypted_msg);// Decrypt the data in the small chunks
$a_key=openssl_pkey_get_details($private_key);$chunk_size=ceil($a_key['bits']/8);$offset=0;$decrypted='';while($offset<strlen($encrypted_msg)){$decrypted_chunk='';$chunk=substr($encrypted_msg,$offset,$chunk_size);if(openssl_private_decrypt($chunk,$decrypted_chunk,$private_key)){$decrypted.=$decrypted_chunk;}else{thrownewException('Problem decrypting the message');}$offset+=$chunk_size;}return$decrypted;}// Calling function
$decrypted_data=decryptSecretMessage($encrypted_msg);
importjavax.crypto.BadPaddingException;importjavax.crypto.Cipher;importjavax.crypto.IllegalBlockSizeException;importjava.nio.charset.StandardCharsets;importjava.security.*;importjava.security.interfaces.RSAPrivateKey;importjava.security.spec.InvalidKeySpecException;importjava.security.spec.PKCS8EncodedKeySpec;importjava.util.Base64;publicclassDecryptZenkiWebhookMessage{privatestaticfinalStringRSA_ALGORITHM="RSA";privatestaticfinalintRSA_KEY_SIZE=4096;publicstaticvoidmain(String[]args)throwsException{StringencryptedMessage="encrypted message";StringprivateKeyContent="<private key>";DecryptZenkiWebhookMessagedecryptZenkiWebhookMessage=newDecryptZenkiWebhookMessage();System.out.println(encryptedMessage);PrivateKeyprivateKey=decryptZenkiWebhookMessage.generateRSAPrivateKey(Base64.getDecoder().decode(privateKeyContent));StringdecryptedMessage=decryptZenkiWebhookMessage.decryptSecretMessage(encryptedMessage,privateKey);System.out.println(decryptedMessage);}publicStringdecryptSecretMessage(StringcipherMessage,PrivateKeyprivateKey)throwsException{CipherdecryptCipher=Cipher.getInstance(RSA_ALGORITHM);decryptCipher.init(Cipher.DECRYPT_MODE,privateKey);byte[]decryptedMessageBytes=blockCipher(Base64.getDecoder().decode(cipherMessage),decryptCipher);returnnewString(decryptedMessageBytes,StandardCharsets.UTF_8);}publicRSAPrivateKeygenerateRSAPrivateKey(byte[]byteArrayOfPrivateKey)throwsNoSuchAlgorithmException,InvalidKeySpecException{/* Generate private key from byte content */PKCS8EncodedKeySpeckeySpec=newPKCS8EncodedKeySpec(byteArrayOfPrivateKey,RSA_ALGORITHM);KeyFactorykf=KeyFactory.getInstance(RSA_ALGORITHM);return(RSAPrivateKey)kf.generatePrivate(keySpec);}privatebyte[]blockCipher(byte[]bytes,Ciphercipher)throwsIllegalBlockSizeException,BadPaddingException{byte[]scrambled;byte[]toReturn=newbyte[0];// for n-bit RSA key, the maximum length of data RSA can encrypt in bytes is n/8 - 11 and decrypt is n/8
intlength=(RSA_KEY_SIZE/8);byte[]buffer=newbyte[length];for(inti=0;i<bytes.length;i++){if((i>0)&&(i%length==0)){scrambled=cipher.doFinal(buffer);toReturn=append(toReturn,scrambled);intnewLength=length;if(i+length>bytes.length){newLength=bytes.length-i;}buffer=newbyte[newLength];}buffer[i%length]=bytes[i];}scrambled=cipher.doFinal(buffer);toReturn=append(toReturn,scrambled);returntoReturn;}privatestaticbyte[]append(byte[]prefix,byte[]suffix){byte[]toReturn=newbyte[prefix.length+suffix.length];System.arraycopy(prefix,0,toReturn,0,prefix.length);System.arraycopy(suffix,0,toReturn,prefix.length,suffix.length);returntoReturn;}}
Webhooks verification
As a security pattern, each webhook and its metadata are signed with a unique key for each endpoint. This signature can be used to verify that the webhook really comes from Zenkipay, and only process it if the origin is valid.
Each webhook notification includes three headers with additional information used for verification:
svix-id: Unique identifier of the webhook message. This identifier is unique for all messages.
svix-signature: Signature encoded in Base64.
svix-timestamp: Time stamp Epoch.
Verificación mediante las librerias oficiales de nuestro proveedor Svix
Se requiere instalar las librerias de Svix:
1
2
3
npm install svix
// Or
yarn add svix
1
composerrequiresvix/svix
1
2
3
4
5
6
7
8
9
// Gradle: Add this dependence
implementation"com.svix:svix:0.68.0"// Maven: Add this dependency in the file POM:
<dependency><groupId>com.svix</groupId><artifactId>svix</artifactId><version>0.68.0</version></dependency>
Next, verify the webhooks using the following code. The payload is the body(string) of the request, and the headers are the headers passed in the request.
💡 It is necessary to use the raw request body when verifying webhooks, as the cryptographic signature is sensitive to even the slightest changes. You should be careful with frameworks that parse the request as JSON because this will also break the signature verification.
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// Headers are sent for each notification.
const headers = {
"svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp": "1614265330",
"svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"algorithm":"RSA","encryptedData":"z7YjgSyx0VzXlDGNC4fjjk1IC69qKN8rRLSItUDY9WXQFgr98ORq/ieJunuCucwk6hmrM9CZAlszE/LD/qSeUtOUcv28ngjobZ5UD+zDqLOeqC5KqHtP0I48L1wC+epXMntsd/KxslWh0+s076K8hZFg7dgJOy2HS46tytNX7AAbEzuQouQo3R0OGV//asG3POej3VQTyRTzKVoRDOO7cVGgNenI4AjfAjUJu+gcOzHqrAj5qr92TEZOZf45+pAk6p5nrfL42NBThO8GB3pXQr2/k74HpkFmVXcZJRB7RDSGfhsFCsnDFZ4N4mHQJWc1/u00z7oGzymPSDGQBEUjzIwGbjBLLDHxdCGKWuwdUq5hAH8Nk55HGOycou7ciBBXOl8E3iTaSxldqOkFLpvkMQ6G2i6dH/1ERKxx61LtQveetkGGMPaLRlsgrUGJNftuKNGEMMQgxn4JykppxHqW3KBlzNhpFUn3QELIctk5SoV12XUDVWi4yhd49F0QlbqfDbRN7ogXo12/SYhUEBS4Wa2uo/mtVKkAdo+GYLtgcggP25y+Qw/I5CenBMJtm2mVFi1b/9AwPaDQo+Yd4S7SrGPyhvcRJcSareIyCXIFSDq6j40qPxGclUv0MLHdwiqxcmmiCP9PQwSnKstCNPBx+IN91E6UfnyBYBhXOWFPZqyHG5OtdBfrx9pIa+0TtFiMBbVGUDidj5QkslyLOZ5Zhx+RMOz+47GpiSg9LaObfJdH4vRsbsZgufvt5hceGE6+lUn3zQzTcwPLaEsQv4HsNMEUKW+tt8K6ZB3GLWaWtII4g0gVlQi2T5P4ZsvBFXf2YJFj4cAL21JVcRanjD2vYk0SbYyuOM2fpBtMJR8pIbVTzdyEw3pPQdyHo4LlbYBFkM3DaXxum9qHr2MHFeAefwJRM79ou5laulfj4nqBPi6hhfT1Z9r9ToDPujOtH+0jRnTIPs4zAWY6rXzUivPJkcu3iqUsqCZcQaU5SHhKli/bHakINyRyTd1ozpdrMsE3rn2VorpgJyVDT47/Bh+xG0F8lCZKofgh4w7DTRxOcIJwkUaPqu30lHHbmc8q0JfGXOgTc496TyjdFx4R529DMzDDkSCVFKp3z8qnG46WsIOIGCp2OXvIiSIihyQFO3FnBx16NbdVlicnTwov4TUPRcsYDIx0p33c3hxOmn1RR1aygYx7XvG3tuMS8ktpfq12ENy3zwpeOit1b8ylnBHwEdaAENaVy03TOLMxIj8rZSfj2AXhnwAKPMLE1AWKufE7OkQAGg2JyJ/H5wB69k9FjwmG0UbkpDGCHNKTMSmwWC6ppV08g/VqUxP50cgXdk19u7Atr1AjCmDXdkFJXYeYwg==","flatData":"","keySize":4096}"';
const wh = new Webhook(secret);
// Throws exception in case of error returns verified content in case of success
const payload = wh.verify(payload, headers);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// import using composers autoload
require_once('vendor/autoload.php');// or manually
require_once('/path/to/svix/php/init.php');$payload='{"test": 2432232314}';// Headers are sent for each notification.
$header=array('svix-id'=>'msg_p5jXN8AQM9LWM0D4loKWxJek','svix-timestamp'=>'1614265330','svix-signature'=>'v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=',);// Throws exception in case of error returns verified content in case of success
$wh=new\Svix\Webhook('whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw');$json=$wh->verify($payload,$header);
importcom.svix.Webhook;importorg.apache.commons.lang3.StringUtils;importorg.springframework.web.bind.annotation.RequestBody;importjavax.servlet.http.HttpServletRequest;importjava.net.http.HttpHeaders;importjava.util.Arrays;importjava.util.Collections;importjava.util.List;importjava.util.Map;importjava.util.function.Function;importjava.util.stream.Collectors;publicclassVerificationZenkiWebhookMessage{privatestaticfinalStringHEADER_MESSAGE_ID="svix-id";privatestaticfinalStringHEADER_MESSAGE_SIGNATURE="svix-signature";privatestaticfinalStringHEADER_MESSAGE_TIMESTAMP="svix-timestamp";publicvoidverificationZenkiWebhookMessage(@RequestBodyStringpayload,HttpServletRequesthttpRequest)throwsException{Stringsecret="<signed secret>";Map<String,List<String>>headersMap=Collections.list(httpRequest.getHeaderNames()).stream().collect(Collectors.toMap(Function.identity(),h->Collections.list(httpRequest.getHeaders(h))));HttpHeadersheaders=HttpHeaders.of(headersMap,(headerName,headerValue)->{returnStringUtils.isNotEmpty(headerValue)&&Arrays.asList(HEADER_MESSAGE_ID,HEADER_MESSAGE_SIGNATURE,HEADER_MESSAGE_TIMESTAMP).stream().anyMatch(headerValidate->headerValidate.equals(headerName));});// Example of the content of a notification
// String payload = "{\"algorithm\":\"RSA\",\"encryptedData\":\"z7YjgSyx0VzXlDGNC4fjjk1IC69qKN8rRLSItUDY9WXQFgr98ORq/ieJunuCucwk6hmrM9CZAlszE/LD/qSeUtOUcv28ngjobZ5UD+zDqLOeqC5KqHtP0I48L1wC+epXMntsd/KxslWh0+s076K8hZFg7dgJOy2HS46tytNX7AAbEzuQouQo3R0OGV//asG3POej3VQTyRTzKVoRDOO7cVGgNenI4AjfAjUJu+gcOzHqrAj5qr92TEZOZf45+pAk6p5nrfL42NBThO8GB3pXQr2/k74HpkFmVXcZJRB7RDSGfhsFCsnDFZ4N4mHQJWc1/u00z7oGzymPSDGQBEUjzIwGbjBLLDHxdCGKWuwdUq5hAH8Nk55HGOycou7ciBBXOl8E3iTaSxldqOkFLpvkMQ6G2i6dH/1ERKxx61LtQveetkGGMPaLRlsgrUGJNftuKNGEMMQgxn4JykppxHqW3KBlzNhpFUn3QELIctk5SoV12XUDVWi4yhd49F0QlbqfDbRN7ogXo12/SYhUEBS4Wa2uo/mtVKkAdo+GYLtgcggP25y+Qw/I5CenBMJtm2mVFi1b/9AwPaDQo+Yd4S7SrGPyhvcRJcSareIyCXIFSDq6j40qPxGclUv0MLHdwiqxcmmiCP9PQwSnKstCNPBx+IN91E6UfnyBYBhXOWFPZqyHG5OtdBfrx9pIa+0TtFiMBbVGUDidj5QkslyLOZ5Zhx+RMOz+47GpiSg9LaObfJdH4vRsbsZgufvt5hceGE6+lUn3zQzTcwPLaEsQv4HsNMEUKW+tt8K6ZB3GLWaWtII4g0gVlQi2T5P4ZsvBFXf2YJFj4cAL21JVcRanjD2vYk0SbYyuOM2fpBtMJR8pIbVTzdyEw3pPQdyHo4LlbYBFkM3DaXxum9qHr2MHFeAefwJRM79ou5laulfj4nqBPi6hhfT1Z9r9ToDPujOtH+0jRnTIPs4zAWY6rXzUivPJkcu3iqUsqCZcQaU5SHhKli/bHakINyRyTd1ozpdrMsE3rn2VorpgJyVDT47/Bh+xG0F8lCZKofgh4w7DTRxOcIJwkUaPqu30lHHbmc8q0JfGXOgTc496TyjdFx4R529DMzDDkSCVFKp3z8qnG46WsIOIGCp2OXvIiSIihyQFO3FnBx16NbdVlicnTwov4TUPRcsYDIx0p33c3hxOmn1RR1aygYx7XvG3tuMS8ktpfq12ENy3zwpeOit1b8ylnBHwEdaAENaVy03TOLMxIj8rZSfj2AXhnwAKPMLE1AWKufE7OkQAGg2JyJ/H5wB69k9FjwmG0UbkpDGCHNKTMSmwWC6ppV08g/VqUxP50cgXdk19u7Atr1AjCmDXdkFJXYeYwg==\",\"flatData\":\"\",\"keySize\":4096}";
Webhookwebhook=newWebhook(secret);webhook.verify(payload,headers);// Throws exception in case of error returns verified content in case of success
}}