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
| Variable | Descripción |
|---|---|
businessCode | Llave privada que autentica a tu comercio |
terminalSerial | Número de serie de la terminal SmartPOS |
timestamp | Unix timestamp en segundos, en UTC |
requestData | Payload 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.
Cadena de derivación de claves
Las tres variables de entrada producen exactamente dos salidas: aesKey para el cifrado y encodedKeyTimestamp para la firma.
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 PKCS7const 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.
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:
| Causa | Cómo la detectas | Solución |
|---|---|---|
Unidad de timestamp equivocada | El valor tiene 13 dígitos en vez de 10 | Segundos, no milisegundos |
| Reloj fuera de la ventana de tolerancia | Falla siempre en una máquina y funciona en otra | El timestamp debe caer dentro de ±5 minutos de la hora del servidor. Sincroniza el reloj con NTP |
formattedDate en hora local | La firma falla de forma intermitente según la hora del día | Calcula la fecha con métodos UTC |
Campo key incluido en el payload cifrado | Falla siempre, desde el primer request | Cifra requestData sin key; dataWithKey solo se usa para firmar |
Cadena vacía firmada en vez de {} | Falla solo en /abort | Firma el literal {} |
| Body re-serializado después de firmar | Falla siempre, y el payload parece correcto | Serializa una vez, firma ese string y envía ese mismo string |
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.
Colombia
Ecuador
Mexico
Peru