Zenki
Zenki Home Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Zenki Webhooks

What is a Webhook?

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:

  • Payment completed, called order.paid

The ‘order.paid’ JSON payload

1
2
3
4
5
6
{
  "algorithm": "RSA",
  "encryptedData": "encryptedData",
  "keySize": 4096,
  "flatData": ""
}

‘order.paid’ data fields:

Field Description Format
algorithm Algorithm name used for cipher String
encryptedData Encrypted Data String
keySize Size of Key used for cipher int
flatData Not used String

Once you decrypt the encrypted data, you will get the following JSON payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "timestamp":1665513008007,
  "eventDetailsVersion":"v1",
  "eventDetails":{
    "orderId":"2c910b24d19946e5a2c98c819a7c93f7",
    "merchantOrderId":"l94jcn457i0xux1t",
    "shopperCartId":"l94jcn4520loi2ji",
    "transactionStatus":"COMPLETED",
    "orderStatus":"AWAITING_SHIPMENT",
    "placedAt":1665512929679,
    "totalAmount":1000,
    "currency":"MXN",
    "merchantPayment":{
      "amount":0.038282960887513325,
      "currency":"ETH",
      "exchangeRate":{
        "from":"MXN",
        "to":"USD",
        "value":20.00,
        "timestamp":1665512931995
      }
    },
    "merchantPluginId":"adf7b2f192f44d42a42e55f3078f596b",
    "transactionHash":"0xee8a3a5eb2a972785b7a56320682bbb843c29409c60dec2d25dbd3eaff91cf26",
    "cryptoLoveFiatAmount":15.000,
    "cryptoLovePercentage":1.5,
    "originalPurchaseData":null
  }
}

Zenkipay Order Payload Event item fields:

Field Description Format Optional
timestamp 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)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * Decrypt message with RSA private key
 *
 * @param string $encrypted_msg Base64 string holds the encrypted message.
 *
 * @return string decrypted message.
 */
public function decryptSecretMessage($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 {
            throw new Exception('Problem decrypting the message');
        }
        $offset += $chunk_size;
    }
    return $decrypted;
}

// Calling function
$decrypted_data = decryptSecretMessage($encrypted_msg);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class DecryptZenkiWebhookMessage {

    private static final String RSA_ALGORITHM = "RSA";
    private static final int RSA_KEY_SIZE = 4096;

    public static void main(String[] args) throws Exception {
        String encryptedMessage = "encrypted message";
        String privateKeyContent = "<private key>";

        DecryptZenkiWebhookMessage decryptZenkiWebhookMessage = new DecryptZenkiWebhookMessage();

        System.out.println(encryptedMessage);
        PrivateKey privateKey =
                decryptZenkiWebhookMessage.generateRSAPrivateKey(Base64.getDecoder().decode(privateKeyContent));

        String decryptedMessage = decryptZenkiWebhookMessage.decryptSecretMessage(encryptedMessage, privateKey);
        System.out.println(decryptedMessage);

    }

    public String decryptSecretMessage(String cipherMessage, PrivateKey privateKey) throws Exception {
        Cipher decryptCipher = Cipher.getInstance(RSA_ALGORITHM);
        decryptCipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decryptedMessageBytes = blockCipher(Base64.getDecoder().decode(cipherMessage), decryptCipher);
        return new String(decryptedMessageBytes, StandardCharsets.UTF_8);
    }

    public RSAPrivateKey generateRSAPrivateKey(byte[] byteArrayOfPrivateKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        /* Generate private key from byte content */
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(byteArrayOfPrivateKey, RSA_ALGORITHM);
        KeyFactory kf = KeyFactory.getInstance(RSA_ALGORITHM);
        return (RSAPrivateKey) kf.generatePrivate(keySpec);
    }

    private byte[] blockCipher(byte[] bytes, Cipher cipher) throws IllegalBlockSizeException, BadPaddingException{
        byte[] scrambled;

        byte[] toReturn = new byte[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
        int length = (RSA_KEY_SIZE /8);

        byte[] buffer = new byte[length];

        for (int i=0; i< bytes.length; i++){

            if ((i > 0) && (i % length == 0)){
                scrambled = cipher.doFinal(buffer);
                toReturn = append(toReturn,scrambled);
                int newLength = length;
                if (i + length > bytes.length) {
                    newLength = bytes.length - i;
                }
                buffer = new byte[newLength];
            }
            buffer[i%length] = bytes[i];
        }
        
        scrambled = cipher.doFinal(buffer);
        toReturn = append(toReturn,scrambled);

        return toReturn;
    }

    private static byte[] append(byte[] prefix, byte[] suffix){
        byte[] toReturn = new byte[prefix.length + suffix.length];
        System.arraycopy(prefix, 0, toReturn, 0, prefix.length);
        System.arraycopy(suffix, 0, toReturn, prefix.length, suffix.length);
        return toReturn;
    }

}

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
composer require svix/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.

Signing secret is required for validation, here we tell you about it how to obtain a signature secret.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import com.svix.Webhook;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;

import javax.servlet.http.HttpServletRequest;
import java.net.http.HttpHeaders;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class VerificationZenkiWebhookMessage {
    private static final String HEADER_MESSAGE_ID        = "svix-id";
    private static final String HEADER_MESSAGE_SIGNATURE = "svix-signature";
    private static final String HEADER_MESSAGE_TIMESTAMP = "svix-timestamp";


    public void verificationZenkiWebhookMessage(@RequestBody String payload, HttpServletRequest httpRequest) throws Exception {
        String secret = "<signed secret>";

        Map<String, List<String>> headersMap = Collections.list(httpRequest.getHeaderNames())
                .stream()
                .collect(Collectors.toMap(
                        Function.identity(),
                        h -> Collections.list(httpRequest.getHeaders(h))
                ));

        HttpHeaders headers = HttpHeaders.of(headersMap, (headerName, headerValue) -> {
            return StringUtils.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}";

        Webhook webhook = new Webhook(secret);
        webhook.verify(payload, headers);
        // Throws exception in case of error returns verified content in case of success
    }
}

You can configure your Zenkipay Webhook quickly without any complications