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
| Mecanismo | Cuándo aplica | Firma | Cuerpo del request |
|---|---|---|---|
| Estándar | encrypted_http_communication deshabilitado. Es el comportamiento por defecto | HMAC-SHA256 | Texto plano |
| Firma y cifrado | encrypted_http_communication habilitado en la terminal | MD5 + SHA-512 | Cifrado 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:
| Header | Valor |
|---|---|
Authorization | Firma HMAC-SHA256 del cuerpo del request, codificada en Base64 |
timestamp | Unix 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, jsondef 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
| 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": "UNAUTHORIZED","message": "Authorization signature is invalid"}
Revisa estas tres 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 modo cifrado, o 10 en modo estándar | Milisegundos en el estándar, segundos en el cifrado |
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 |
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.
Chile
Ecuador
Mexico
Peru