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
| Mechanism | When it applies | Signature | Request body |
|---|---|---|---|
| Standard | encrypted_http_communication disabled. This is the default behavior | HMAC-SHA256 | Plain text |
| Sign and encrypt | encrypted_http_communication enabled on the terminal | MD5 + SHA-512 | Encrypted 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:
| Header | Value |
|---|---|
Authorization | HMAC-SHA256 signature of the request body, Base64-encoded |
timestamp | Unix 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, 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)), # 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
| 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.
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:
| Cause | How you spot it | Fix |
|---|---|---|
Wrong timestamp unit | The value has 13 digits in encrypted mode, or 10 in standard mode | Milliseconds in standard, seconds in encrypted |
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 |
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.
Chile
Colombia
Ecuador
Peru