Autenticación y Cifrado de Requests

Cuando encrypted_http_communication está habilitado en tu terminal, todos los requests hacia Kushki ONE Connect deben ir firmados y cifrados. El payload original nunca viaja en texto plano.

El equipo de Kushki activa esta configuración durante el onboarding. Confirma con tu contacto técnico si aplica para tu integración antes de implementar este flujo.

Variables requeridas

Asegúrate de tener estas cuatro variables disponibles en tu cliente:

VariableDescripción
businessCodeLlave privada que autentica a tu comercio
terminalSerialNúmero de serie de la terminal SmartPOS
timestampUnix timestamp en segundos (UTC)
requestDataPayload original del request

Flujo general

Cada request pasa por dos procesos independientes que parten del mismo requestData. Uno produce la firma y el otro produce el payload cifrado. Ambos se envían juntos en el request final.

Flujo general


Cadena de derivación de claves

Antes de implementar los pasos, revisa cómo se relacionan tus inputs con las claves finales. Las tres variables de entrada (businessCode, terminalSerial, timestamp) producen exactamente dos outputs: aesKey para el cifrado y encodedKeyTimestamp para la firma.

cadena de derivación

El password es el valor central de esta cadena. Cambia cada minuto porque depende de formattedDate, lo que hace que cualquier request firmado caduque automáticamente.


Paso 1 — Genera el timestamp

Guarda este valor en una variable al inicio del flujo. Lo reutilizarás en los pasos 3, 4 y 6.

const timestamp = Math.floor(Date.now() / 1000);
// Ejemplo: 1710000000

Paso 2 — Genera formattedDate (UTC)

function unixTimestampToFormattedDate(unixTimestamp) {
const date = new Date(unixTimestamp * 1000);
const pad = (n) => String(n).padStart(2, '0');
return [
date.getUTCFullYear(),
pad(date.getUTCMonth() + 1),
pad(date.getUTCDate()),
pad(date.getUTCHours()),
pad(date.getUTCMinutes())
].join(':');
}
// Ejemplo de salida: "2026:03:24:21:55"

Paso 3 — Genera el password temporal

Este valor cambia cada minuto porque depende de formattedDate.

const token = businessCode + terminalSerial;
const base = token + formattedDate;
const key = base.padEnd(32, '0');
const password = MD5(key); // string hexadecimal de 32 caracteres

Paso 4 — Construye dataWithKey (solo para la firma)

dataWithKey es una copia de requestData con el campo key añadido. No se cifra ni se envía.

const encodedKeyTimestamp = Base64(password + timestamp);
const dataWithKey = {
...requestData,
key: encodedKeyTimestamp
};

Paso 5 — Genera la firma (Authorization)

const json = JSON.stringify(dataWithKey);
const dataJson = Base64(json);
const hash = SHA512(dataJson); // hex string

Headers resultantes:

Authorization: Basic <hash>
timestamp: <timestamp>

Paso 6 — Cifra el payload (AES-256-CBC)

Solo se cifra requestData (sin key).

const aesKey = (timestamp + "___" + password).substring(0, 32);
const iv = randomBytes(16); // padding PKCS7
const encrypted = AES_CBC(JSON.stringify(requestData), aesKey, iv);
const data = iv.hex + ':' + encrypted.hex;
// Ejemplo: "a3f1...b2c4:9e0d...7f21"

Paso 7 — Arma el request final

La estructura del request varía según el método HTTP. En ambos casos los headers son iguales; lo que cambia es cómo se entrega el campo data.

anatomía del request

POST · PATCH · PUT

Headers

Authorization: Basic <hash>
timestamp: <timestamp>
Content-Type: application/json

Body

{
"data": "<iv_hex>:<ciphertext_hex>"
}

GET

Headers

Authorization: Basic <hash>
timestamp: <timestamp>

Query param

GET /endpoint?data=<iv_hex>:<ciphertext_hex>

Elimina los demás query params del request original antes de enviarlo.


Errores de autenticación

Si la firma es inválida, recibirás:

{
"type": "AUTH",
"code": "UNAUTHORIZED",
"message": "Authorization signature is invalid"
}

Las tres causas más frecuentes tienen solución directa. Usa este árbol de diagnóstico para identificar cuál aplica a tu caso:

diagnostico de errores


Script de referencia para Postman

Configura este Pre-request Script a nivel de colección para que el flujo de firma y cifrado se ejecute automáticamente en cada request.

Variables requeridas:

VariableValor
businessCodeLlave privada de tu comercio
terminalSerialSerial de tu terminal
encrypted_http_communicationtrue para activar el flujo
function getCurrentTimestamp() {
return Math.floor(new Date().getTime() / 1000);
}
function unixTimestampToFormattedDate(ts) {
const d = new Date(ts * 1000);
const p = (n) => String(n).padStart(2, '0');
return `${d.getUTCFullYear()}:${p(d.getUTCMonth()+1)}:${p(d.getUTCDate())}:${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
}
function generateTokenPassword(token, ts) {
const CryptoJS = require('crypto-js');
const key = (token + unixTimestampToFormattedDate(ts)).padEnd(32, '0');
return CryptoJS.MD5(key).toString();
}
function encryptData(text, ts, terminalSerial) {
const CryptoJS = require('crypto-js');
const password = generateTokenPassword(pm.variables.get("businessCode") + terminalSerial, ts);
const key = (ts + "___" + password).substring(0, 32);
const iv = CryptoJS.lib.WordArray.random(16);
const enc = CryptoJS.AES.encrypt(text, CryptoJS.enc.Utf8.parse(key), {
iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7
});
return iv.toString(CryptoJS.enc.Hex) + ':' + enc.ciphertext.toString(CryptoJS.enc.Hex);
}
function buildAuthenticationHash(data, ts, terminalSerial) {
const CryptoJS = require('crypto-js');
const password = generateTokenPassword(pm.variables.get("businessCode") + terminalSerial, ts);
const encodedKeyTimestamp = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(password + ts));
data.key = encodedKeyTimestamp;
const dataJson = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(JSON.stringify(data)));
return CryptoJS.SHA512(dataJson).toString(CryptoJS.enc.Hex);
}
function executeScript() {
const terminalSerial = pm.variables.get('terminalSerial');
const businessCode = pm.variables.get('businessCode');
if (!terminalSerial) throw new Error("terminalSerial is not set");
if (!businessCode) throw new Error("businessCode is not set");
const ts = getCurrentTimestamp();
let requestData = {};
try {
if (pm.request.body?.mode === 'raw' && pm.request.body.raw)
requestData = JSON.parse(pm.request.body.raw);
} catch (e) {}
if (pm.request.method === 'GET')
pm.request.url.query.all().forEach((p) => { if (p.key !== 'data') requestData[p.key] = p.value; });
delete requestData.key;
const hash = buildAuthenticationHash(structuredClone(requestData), ts, terminalSerial);
const encryptedData = encryptData(JSON.stringify(requestData), ts, terminalSerial);
pm.request.headers.add({ key: 'Authorization', value: `Basic ${hash}` });
pm.request.headers.add({ key: 'timestamp', value: ts.toString() });
pm.request.headers.upsert({ key: 'Content-Type', value: 'application/json' });
if (pm.request.method === 'GET') {
pm.request.url.query.add({ key: 'data', value: encryptedData });
pm.request.url.query.members.forEach((p) => { if (p.key !== 'data') p.disabled = true; });
} else {
pm.request.body.mode = 'raw';
pm.request.body.raw = JSON.stringify({ data: encryptedData });
}
}
const raw = pm.variables.get('encrypted_http_communication');
const httpEncrypted = raw === true || String(raw).toLowerCase() === 'true' || raw === 1 || raw === '1';
if (httpEncrypted) executeScript();

Realiza cobros con Kushki One

Con la autenticación lista, revisa el flujo completo de cobro semi-integrado.

Códigos de error

Consulta el catálogo completo de errores, incluyendo los de autenticación fallida.