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. Existen dos mecanismos, y el que te corresponde depende de la configuración de tu terminal.

Esta guía cubre los dos, y sobre todo la diferencia que más errores provoca entre ellos.

Requisitos

Cuál mecanismo te aplica

MecanismoCuándo aplicaFirmaCuerpo del request
Estándarencrypted_http_communication deshabilitado. Es el comportamiento por defectoHMAC-SHA256Texto plano
Firma y cifradoencrypted_http_communication habilitado en la terminalMD5 + SHA-512Cifrado con AES-256-CBC

El equipo de Kushki activa el modo cifrado durante el onboarding. Confirma con tu contacto técnico cuál aplica a tu integración antes de implementar.

Mecanismo estándar

Envía dos headers en cada request:

HeaderValor
AuthorizationFirma HMAC-SHA256 del cuerpo del request, codificada en Base64
timestampUnix timestamp en milisegundos

La firma se calcula así:

Authorization = Base64( HMAC-SHA256( cuerpoDelRequest, businessCode ) )
  • Javascript
  • Python
import crypto from "node:crypto";
function buildHeaders(payload, businessCode) {
const body = JSON.stringify(payload);
const sig = crypto.createHmac("sha256", businessCode)
.update(body).digest("base64");
return {
headers: {
"Content-Type": "application/json",
Authorization: sig,
timestamp: String(Date.now()), // milisegundos
},
body,
};
}
import hmac, hashlib, base64, time, json
def build_headers(payload: dict, business_code: str):
body = json.dumps(payload, separators=(",", ":"))
sig = hmac.new(business_code.encode(), body.encode(),
hashlib.sha256).digest()
return {
"Content-Type": "application/json",
"Authorization": base64.b64encode(sig).decode(),
"timestamp": str(int(time.time() * 1000)), # milisegundos
}, body

Mecanismo de firma y cifrado

Cuando encrypted_http_communication está habilitado, 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": "UNAUTHORIZED",
"message": "Authorization signature is invalid"
}

Revisa estas tres 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 modo cifrado, o 10 en modo estándarMilisegundos en el estándar, segundos en el cifrado
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

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, terminalSerial y encrypted_http_communication 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 });
}
}
const raw = pm.variables.get('encrypted_http_communication');
const httpEncrypted = raw === true || String(raw).toLowerCase() === 'true' || raw === 1 || raw === '1';
if (httpEncrypted) 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.