Zenki
Zenki Principal Habilitar modo claro/oscuro Habilitar modo claro/oscuro Habilitar modo claro/oscuro Regresar a la página principal

Zenki Webhooks

¿Qué es un Webhook?

Un webhook, por decirlo de la manera más simple, permite a las aplicaciones el intercambio de información de manera automática. Lo que hacen es aportar una solución sencilla para el intercambio de datos entre aplicaciones a partir de eventos que se registran en el sistema.

En el mundo de Zenkipay, los webhooks permiten que el comercio sea notificado cuando se ha producido un evento de la plataforma en tiempo real, lo que le permite crear procesos personalizados.

¿Cómo se configura un Webhook?

Para configurar un Webhook, primero debe crear un endpoint en el que se enviarán las notificaciones. Este endpoint debe ser una URL que acepte peticiones POST. Una vez que se haya generado el endpoint, debe configurar el Webhook en el portal de Zenkipay.

Para configurar su endpoint en Zenkipay, aquí le decimos cómo hacerlo: configure su Webhook de Zenkipay en el portal de Zenkipay.

¿Qué eventos son notificados?

Zenkipay enviará una petición POST a su endpoint con un JSON que contiene la información del evento.

Zenkipay puede notificarle sobre los siguientes eventos:

  • Pago completado, llamado order.paid

El objeto JSON ‘order.paid’

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

‘order.paid’ campos de datos:

Campo Descripción Formato
algorithm Algoritmo utilizado para el cifrado String
encryptedData Datos cifrados String
keySize Tamaño de la llave utilizada para el cifrado int
flatData No utilizado String

Una vez que descifren los datos informados en el campo encryptedData, obtendrá el siguiente objeto JSON:

 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
  }
}

Objeto JSON con el evento de pago de la Orden (Zenkipay Order Payload Event):

Campo Descripción Formato Opcional
timestamp Fecha y hora cuando el evento fue notificado en UTC long No
eventDetailsVersion Versión del objeto de evento que se utiliza para la notificación, inicia con v1 String No
eventDetails Objeto JSON con el detalle del evento Object No
orderId Identificador único de la Orden en Zenkipay String No
merchantOrderId Identificador único de la Orden de su sistema (si fue informado) String Yes
shopperCartId Identificador único del carrito de su sistema (si fue informado) String Yes
transactionStatus Estatus de la transacción en Zenkipay, para un pago completo = COMPLETED String No
orderStatus Estatus de la Orden en Zenkipay String No
placedAt Fecha en la que fue emitida la orden en Zenkipay long No
totalAmount Monto total de la orden number No
currency Moneda String No
merchantPayment Objeto JSON que representa el pago que recibirá el Merchant por la orden (en Crypto) Object No
amount Monto que se le pagará al comercio en crypto number No
currency Criptomoneda en la que se le pagará al comercio String No
exchangeRate Tipo de cambio con el que se calculó el valor en USD Object No
from Moneda origen de la orden String No
to Moneda destino de intercambio para el calculo de pago al Merchant, siempre USD String No
value Monto resultante del intercambio number No
timestamp Fecha y hora en la que se realizo el intercambio (UTC) long No
transactionHash Identificador único de la transacción en el blockchain String No
cryptoLoveFiatAmount Monto calculado correspondiente al concepto de “Crypto Love” number Yes
cryptoLovePercentage Porcentaje que tiene configurado el comercio para el descuento por “Crypto Love” number No
originalPurchaseData String que representa el objeto original utilizado para generar la orden en Zenkipay String Yes
merchantPluginId Identificador único del Plugin que utiliza el comercio para ejecutar la transacción String Yes

¿Cómo descifro el evento de pago de la Orden?

Utilizando su llave privada RSA, puede desencriptar el evento de pago de la Orden. Para obtener más información sobre cómo generar su llave privada RSA, consulte la sección Cómo generar su llave privada RSA.

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;
    }

}

Verifica los Webhooks

Como patron de seguridad, se firma cada webhook y sus metadatos con una clave única para cada endpoint. Esta firma se puede utilizar para verificar que el webhook proviene realmente de Zenkipay, y sólo procesarlo si el origen es valido. Cada llamada de webhook incluye tres cabeceras con información adicional que se utilizan para la verificación:

  • svix-id: Identificador único del mensaje del webhook. Este identificador es único para todos los mensajes.
  • svix-signature: Firma codificada en Base64.
  • svix-timestamp: Marca de tiempo 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: Añade esta dependencia
    implementation "com.svix:svix:0.68.0"

// Maven:  Añade esta dependencia en el archivo POM:
    <dependency>
        <groupId>com.svix</groupId>
        <artifactId>svix</artifactId>
        <version>0.68.0</version>
    </dependency>

A continuación, verifique los webhooks utilizando el siguiente código. La carga útil es el cuerpo(cadena) de la solicitud, y las cabeceras son los encabezados pasados en la solicitud.

💡 Es necesario utilizar el cuerpo de la solicitud sin procesar cuando se verifican los webhooks, ya que la firma criptográfica es sensible incluso a los más mínimos cambios. Debes tener cuidado con los frameworks que parsean la petición como JSON porque esto también romperá la verificación de la firma.

Para realizar la validación es necesario del la firma secreta, aquí te decimos como obtener la firma secreta.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { Webhook } from "svix";

const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

// Las cabeceras son enviadas por cada notificación 
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);
// Lanza una excepción en caso de error, devuelve el contenido verificado en caso de éxito
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}';
// Las cabeceras son enviadas por cada notificación 
$header = array(
        'svix-id'  => 'msg_p5jXN8AQM9LWM0D4loKWxJek',
        'svix-timestamp' => '1614265330',
        'svix-signature' => 'v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=',
    );

// Lanza una excepción en caso de error, devuelve el contenido verificado en caso de éxito
$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));
        });
        //Ejemplo del contenido de una notificacion
        // 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);
        // Lanza una excepción en caso de error, devuelve el contenido verificado en caso de éxito
    }
}

Aquí te decimos cómo configurar tu Webhook en Zenkipay rapidamente y sin complicaciones.