Autenticación y Cifrado de Requests

Todas las comunicaciones hacia Kushki ONE Connect se autentican con una firma que calculas a partir de tu Business-Code, y el payload viaja cifrado. Hay un solo mecanismo, y es el mismo en todas partes.

Requisitos

Qué lleva cada request

Cada request va firmado y cifrado. El payload original nunca viaja en texto plano.

Variables que necesitas

VariableDescripción
businessCodeLlave privada que autentica a tu comercio
terminalSerialNúmero de serie de la terminal SmartPOS
timestampUnix timestamp en segundos, en 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.

General sign-and-encrypt flow in Kushki ONE Connect

Cadena de derivación de claves

Las tres variables de entrada producen exactamente dos salidas: aesKey para el cifrado y encodedKeyTimestamp para la firma.

Kushki ONE Connect key derivation chain

El password es el valor central de la 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 reutilizas en los pasos 3, 4 y 6.

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

Paso 2: genera formattedDate en 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

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

Paso 4: construye dataWithKey

Es una copia de requestData con el campo key añadido. Solo se usa para firmar.

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

Paso 5: genera la firma

const json = JSON.stringify(dataWithKey);
const dataJson = Base64(json);
const hash = SHA512(dataJson); // cadena hexadecimal

Headers resultantes:

Authorization: Basic <hash>
timestamp: <timestamp>

Paso 6: cifra el payload

Se cifra únicamente requestData, sin el campo key.

const aesKey = (timestamp + "___" + password).substring(0, 32);
const iv = randomBytes(16); // relleno 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

Los headers son iguales en todos los métodos. Lo que cambia es cómo entregas el campo data.

Anatomy of a signed and encrypted HTTP request

En POST, PATCH y PUT, el dato cifrado va en el cuerpo:

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

En GET, va como parámetro de consulta:

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

Elimina los demás parámetros de consulta del request original antes de enviarlo.

Errores de autenticación

Una firma inválida siempre devuelve la misma respuesta, sin distinguir la causa:

{
"type": "AUTH",
"code": "AUTH-001",
"message": "Invalid or expired credentials"
}

Revisa estas causas en orden. Cubren la gran mayoría de los casos:

CausaCómo la detectasSolución
Unidad de timestamp equivocadaEl valor tiene 13 dígitos en vez de 10Segundos, no milisegundos
Reloj fuera de la ventana de toleranciaFalla siempre en una máquina y funciona en otraEl timestamp debe caer dentro de ±5 minutos de la hora del servidor. Sincroniza el reloj con NTP
formattedDate en hora localLa firma falla de forma intermitente según la hora del díaCalcula la fecha con métodos UTC
Campo key incluido en el payload cifradoFalla siempre, desde el primer requestCifra requestData sin key; dataWithKey solo se usa para firmar
Cadena vacía firmada en vez de {}Falla solo en /abortFirma el literal {}
Body re-serializado después de firmarFalla siempre, y el payload parece correctoSerializa una vez, firma ese string y envía ese mismo string

Authentication error diagnostic tree

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. Define las variables businessCode y terminalSerial en el entorno.

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);
data.key = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(password + ts));
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 });
}
}
executeScript();
Acepta cobros con Kushki ONE

Con la autenticación resuelta, revisa los flujos de cobro presencial y sus variantes síncrona y asíncrona.

Errores de autenticación

Consulta el catálogo completo de códigos de error, incluidos los de credenciales y permisos.