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:
| Element | Value |
|---|---|
Authorization | Basic followed by the SHA-512 hash. The Basic prefix is required |
timestamp | Unix 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
| Variable | Description |
|---|---|
businessCode | The private key that authenticates your merchant account |
terminalSerial | The SmartPOS terminal serial number |
timestamp | Unix timestamp in seconds, in UTC |
requestData | The 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.
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.
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 paddingconst 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.
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:
| Cause | How you spot it | Fix |
|---|---|---|
timestamp in the wrong unit | The value has 13 digits instead of 10 | Seconds, not milliseconds |
| Clock outside the tolerance window | Fails consistently on one machine and works on another | The timestamp must fall within ±5 minutes of server time. Sync the clock with NTP |
formattedDate in local time | The signature fails intermittently depending on the time of day | Compute the date with UTC methods |
key field included in the encrypted payload | Fails every time, from the very first request | Encrypt requestData without key; dataWithKey is only for signing |
Empty string signed instead of {} | Fails only on /abort | Sign the literal {} |
| Body re-serialized after signing | Fails every time, and the payload looks correct | Serialize once, sign that string, send that same string |
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.
Chile
Colombia
Ecuador
Peru