Autenticación y Cifrado de Requests

When encrypted_http_communication is enabled on your terminal, all requests to Kushki ONE Connect must be signed and encrypted. The original payload never travels in plain text.

The Kushki team activates this setting during onboarding. Confirm with your technical contact whether it applies to your integration before implementing this flow.

Required variables

Make sure you have these four variables available in your client:

VariableDescription
businessCodePrivate key that authenticates your merchant account
terminalSerialSerial number of the SmartPOS terminal
timestampUnix timestamp in seconds (UTC)
requestDataOriginal request payload

General flow

Each request goes through two independent processes that start from the same requestData. One produces the signature and the other produces the encrypted payload. Both are sent together in the final request.

Flujo general EN


Key derivation chain

Before implementing the steps, review how your inputs relate to the final keys. The three input variables (businessCode, terminalSerial, timestamp) produce exactly two outputs: aesKey for encryption and encodedKeyTimestamp for signing.

cadena de derivación EN

The password is the central value in this chain. It changes every minute because it depends on formattedDate, which causes any signed request to expire automatically.


Step 1 — Generate the timestamp

Store this value in a variable at the start of the flow. You will reuse it in steps 3, 4, and 6.

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

Step 2 — Generate 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(':');
}
// Example output: "2026:03:24:21:55"

Step 3 — Generate the temporary password

This value changes every minute because it depends on formattedDate.

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

Step 4 — Build dataWithKey (for signing only)

dataWithKey is a copy of requestData with the key field added. It is not encrypted or sent.

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

Step 5 — Generate the signature (Authorization)

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

Resulting headers:

Authorization: Basic <hash>
timestamp: <timestamp>

Step 6 — Encrypt the payload (AES-256-CBC)

Only requestData is encrypted (without key).

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

Step 7 — Assemble the final request

The request structure varies depending on the HTTP method. In both cases the headers are the same; what changes is how the data field is delivered.

anatomía del request EN

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>

Remove all other query params from the original request before sending it.


Authentication errors

If the signature is invalid, you will receive:

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

The three most common causes each have a direct fix. Use this decision tree to identify which one applies to your case:

diagnostico de errores EN


Postman reference script

Configure this Pre-request Script at the collection level so the signing and encryption flow runs automatically on every request.

Required variables:

VariableValue
businessCodeYour merchant private key
terminalSerialYour terminal serial number
encrypted_http_communicationtrue to enable the flow
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();

Charge with Kushki ONE

With authentication ready, review the complete semi-integrated charge flow.

Error codes

Check the full error catalog, including authentication failure codes.