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:
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)
/**
* 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;}}
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
composerrequiresvix/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.
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);
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));});//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}";
Webhookwebhook=newWebhook(secret);webhook.verify(payload,headers);// Lanza una excepción en caso de error, devuelve el contenido verificado en caso de éxito
}}