Autenticación y Cifrado de Requests

Every call to Kushki ONE Connect is authenticated with a signature you compute from your Business-Code. There are two mechanisms, and the one that applies to you depends on your terminal’s configuration.

This guide covers both, and above all the difference between them that causes the most errors.

Requirements

Which mechanism applies to you

MechanismWhen it appliesSignatureRequest body
Standardencrypted_http_communication disabled. This is the default behaviorHMAC-SHA256Plain text
Sign and encryptencrypted_http_communication enabled on the terminalMD5 + SHA-512Encrypted with AES-256-CBC

The Kushki team turns on encrypted mode during onboarding. Confirm with your technical contact which one applies to your integration before you implement.

Standard mechanism

Send two headers with every request:

HeaderValue
AuthorizationHMAC-SHA256 signature of the request body, Base64-encoded
timestampUnix timestamp in milliseconds

The signature is computed like this:

Authorization = Base64( HMAC-SHA256( requestBody, 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()), // milliseconds
},
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)), # milliseconds
}, body

Sign-and-encrypt mechanism

When encrypted_http_communication is enabled, every request is signed and encrypted. The original payload never travels in plain text.

Variables you need

VariableDescription
businessCodeThe private key that authenticates your merchant account
terminalSerialThe SmartPOS terminal serial number
timestampUnix timestamp in seconds, in UTC
requestDataThe original request payload

General flow

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

General sign-and-encrypt flow in Kushki ONE Connect

Key derivation chain

The three input variables produce exactly two outputs: aesKey for encryption and encodedKeyTimestamp for the signature.

Kushki ONE Connect key derivation chain

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

Step 1: generate the timestamp

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

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

Step 2: generate formattedDate in 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

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

This is a copy of requestData with the key field added. It is used only for signing.

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

Step 5: generate the signature

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

Resulting headers:

Authorization: Basic <hash>
timestamp: <timestamp>

Step 6: encrypt the payload

Only requestData is encrypted, without the key field.

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 headers are the same across methods. What changes is how you deliver the data field.

Anatomy of a signed and encrypted HTTP request

On POST, PATCH and PUT, the encrypted data goes in the body:

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

On GET, it goes as a query parameter:

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

Remove every other query parameter from the original request before sending it.

Authentication errors

An invalid signature always returns the same response, with no distinction between causes:

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

Check these three causes in order. They cover the vast majority of cases:

CauseHow you spot itFix
Wrong timestamp unitThe value has 13 digits in encrypted mode, or 10 in standard modeMilliseconds in standard, seconds in encrypted
formattedDate in local timeThe signature fails intermittently depending on the time of dayCompute the date with UTC methods
key field included in the encrypted payloadFails every time, from the very first requestEncrypt requestData without key; dataWithKey is only for signing

Authentication error diagnostic tree

Postman reference script

Set this pre-request script at the collection level so the sign-and-encrypt flow runs automatically on every request. Define the businessCode, terminalSerial and encrypted_http_communication variables in your environment.

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();
Accept payments with Kushki ONE

With authentication sorted out, review the card-present payment flows and their sync and async variants.

Authentication errors

Look up the full catalog of error codes, including credentials and permissions.