Autenticación y Cifrado de Requests

Every call to Kushki ONE Connect is authenticated with a signature you compute from your Business-Code, and the payload travels encrypted. There is one mechanism, and it is the same everywhere.

Requirements

Before you start:

  • Your Business-Code: the private key that authenticates your merchant account. The integrations team hands it over during onboarding.
  • The device’s terminalSerial. The integrations team hands it over during onboarding as well.

What every request carries

Two headers and an encrypted body:

ElementValue
AuthorizationBasic followed by the SHA-512 hash. The Basic prefix is required
timestampUnix timestamp in seconds, in UTC — must fall within ±5 minutes of server time
Body{"data": "<iv_hex>:<ciphertext_hex>"}

The original payload never travels in plain text. Everything documented as a request body across the Kushki ONE guides is the plaintext you encrypt, not what goes on the wire.

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. 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.

Kushki ONE Connect key derivation chain

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. This applies to /sync/abort and /async/abort on the local network, and to the print job status endpoint: the identifier they used to carry as a query parameter travels inside the encrypted payload instead.

Authentication errors

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

{
"type": "AUTH",
"code": "AUTH-001",
"message": "Invalid or expired credentials"
}

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

CauseHow you spot itFix
timestamp in the wrong unitThe value has 13 digits instead of 10Seconds, not milliseconds
Clock outside the tolerance windowFails consistently on one machine and works on anotherThe timestamp must fall within ±5 minutes of server time. Sync the clock with NTP
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
Empty string signed instead of {}Fails only on /abortSign the literal {}
Body re-serialized after signingFails every time, and the payload looks correctSerialize once, sign that string, send that same string

Authentication error diagnostic tree

Postman reference script

Set this pre-request script at the collection level so the flow runs automatically on every request. Define the businessCode and terminalSerial 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);
}
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 });
}
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.