Authentication

Secure Authentication with HMAC-SHA256 Signature

To ensure secure communication with the REST API, requests to protected endpoints must be signed using the HMAC-SHA256 algorithm. This involves generating a request signature based on specific headers and including both the signature and public access key in the request headers.

📘

Note:

All requests need to include secure header authentication.


Steps to create a REST call

1. Set Required Headers

Include the following headers in your HTTP request:

  • HTTP Method: POST
  • Request Path
    • Asynchronous: /v/1/payment/initiate/async
    • Synchronous: /v/1/payment/initiate/sync

HEADER

DESCRIPTION

X-pos-id

The POS ID is a unique identifier for the originating Point of Sale terminal. In multi-terminal environments, each device requires a distinct alphanumeric identifier (e.g., POS1, POS2, CHECKOUT_A).

X-tu-authorization

The HMAC signature header. Contains the access key, signed headers, and signature.

protocol:TU1,accesskey:{{accessKey}},signedheaders:User-Agent;X-tu-date;X-tu-random,signature:{{signature}}

  • accessKey:To be provided by Ecentric, and treated as sensitive information.
  • signature:To be derived from steps below.

X-tu-random

A unique 17-character random alphanumeric string. Provides entropy and uniqueness.

X-tu-serial

The serial number of the target payment terminal for this payment request.

X-tu-date

ISO 8601 UTC timestamp (e.g. 2025-07-22T16:20:00Z). Prevents replay attacks.

User-Agent

Identifies the client application. Used as the first input in the HMAC chain.


2. Create Signature

The signature is calculated as follows using HMAC-SHA256:

  • First HMAC: HMAC(secretKey, User-Agent)
  • Second HMAC: HMAC(result1, X-tu-date)
  • Third HMAC: HMAC(result2, X-tu-random)

The final result is encoded as a lowercase hex string and placed in the X-tu-authorization header.

Signature Example:

// Simulated environment object to store variables
const environment = {};

// Initialize input values
const secretKey = "your-secret-key";       // Replace with actual value
const userAgent = "your-user-agent";       // Replace with actual value

// Generate timestamp and random value
const tuDate = new Date().toISOString();
const randomVal = (Math.random().toString(36) + '00000000000000000').slice(2, 18);

// Store them in our simulated "environment"
environment["X-tu-date"] = tuDate;
environment["X-tu-random"] = randomVal;

// Generate the signature
let kSigning = secretKey;
kSigning = CryptoJS.HmacSHA256(userAgent, kSigning);
kSigning = CryptoJS.HmacSHA256(tuDate, kSigning);
kSigning = CryptoJS.HmacSHA256(randomVal, kSigning);

// Store the signature in our environment object
environment["signature"] = kSigning.toString(CryptoJS.enc.Hex);
#!/bin/bash

# ==== Configurable Inputs ====
SECRET-KEY="your-secret-key"       # Replace with actual value
USER_AGENT="your-user-agent"       # Replace with actual value

# ==== Generate timestamp and random value ====
TU_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
RANDOM_VAL=$(shuf -i 10000000000000000-99999999999999999 -n 1 | cut -c 1-17)

# ==== HMAC Signature Chaining ====

# Generate signature by hmac of the headers
SIGNATURE="$SECRET_KEY"

# First HMAC: User-Agent
SIGNATURE=$(echo -n "$USER_AGENT" | openssl dgst -sha256 -hmac "$SIGNATURE" -binary | xxd -p -c 256)

# Second HMAC: tudate (convert hex back to binary for next step)
SIGNATURE=$(echo -n "$TU_DATE" | openssl dgst -sha256 -hmac "$(echo "$SIGNATURE" | xxd -r -p)" -binary | xxd -p -c 256)

# Third HMAC: randomVal
SIGNATURE=$(echo -n "$RANDOM_VAL" | openssl dgst -sha256 -hmac "$(echo "$SIGNATURE" | xxd -r -p)" -binary | xxd -p -c 256)
private static final SecureRandom SECURE_RANDOM = new SecureRandom();

// ==== Generate 17-digit random numeric string ====
private static String generateRandomValue() {
    StringBuilder sb = new StringBuilder(17);
    for (int i = 0; i < 17; i++) {
        sb.append(SECURE_RANDOM.nextInt(10));
    }
    return sb.toString();
}

// ==== HMAC-SHA256 helper ====
private static byte[] hmacSha256(String data, byte[] key) throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
    mac.init(keySpec);
    return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
}

3. Create Payload

To initiate a transaction request, the client must construct a payload containing the required transaction fields. This payload should be included in the body of the HTTP POST request.

'{"launchType":"SALE","merchantID":"910100000000001","posID":"POS-STORE123-TERM01","serialNumber":"PC05P2CG10036","transactionAmount":1500}'

4. Send the API Request

Full Example:

#!/bin/bash

# ==== Configurable Inputs ====
API_URL="https://paymentuat.test.thumbzup.com/posbuddy-cloud/v/1"
ACCESS_KEY="AAABBSS9NLLKKKK1MMMM"
SECRET_KEY="11rAAAceqB1RBBkYDcgjGtZSjgWpLxGlZeYetpET"
SERIAL_NUMBER="PC05P2CG10036"
USER_AGENT="MyPOSApp/1.0"
MERCHANT_ID="910100000000001"

# Generate ISO 8601 timestamp
TU_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

# Generate 17-character random number
RANDOM_VAL=$(shuf -i 10000000000000000-99999999999999999 -n 1 | cut -c 1-17)

# ==== HMAC Chaining Logic ====

# Generate signature by hmac of the headers
SIGNATURE="$SECRET_KEY"

# First HMAC: user agent
SIGNATURE=$(echo -n "$USER_AGENT" | openssl dgst -sha256 -hmac "$SIGNATURE" -binary | xxd -p -c 256)

# Second HMAC: date (convert hex back to binary for next step)
SIGNATURE=$(echo -n "$TU_DATE" | openssl dgst -sha256 -hmac "$(echo "$SIGNATURE" | xxd -r -p)" -binary | xxd -p -c 256)

# Third HMAC: random value
SIGNATURE=$(echo -n "$RANDOM_VAL" | openssl dgst -sha256 -hmac "$(echo "$SIGNATURE" | xxd -r -p)" -binary | xxd -p -c 256)

# Make the request
RESPONSE=$(curl --request POST \
  --url "$API_URL/payment/sync" \
  --header "X-pos-id: POS-STORE123-TERM01" \
  --header "X-tu-authorization: protocol:TU1,accesskey:$ACCESS_KEY,signedheaders:User-Agent;X-tu-date;X-tu-random,signature:$SIGNATURE" \
  --header "X-tu-random: $RANDOM_VAL" \
  --header "X-tu-serial: $SERIAL_NUMBER" \
  --header "X-tu-date: $TU_DATE" \
  --header "User-Agent: $USER_AGENT" \
  --header "Content-Type: application/json" \
  --data "{
    \"launchType\": \"SALE\",
    \"merchantID\": \"$MERCHANT_ID\",
    \"posId\": \"POS-STORE123-TERM01\",
    \"serialNumber\": \"$SERIAL_NUMBER\",
    \"transactionAmount\": 1500
  }" \
  )

echo "Response: $RESPONSE"
package org.example;

import kong.unirest.core.HttpResponse;
import kong.unirest.core.JsonNode;
import kong.unirest.core.RequestBodyEntity;
import kong.unirest.core.Unirest;
import kong.unirest.core.json.JSONObject;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.Map;
import java.util.UUID;

//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
  
		private static String generateRandomString() {
        StringBuilder sb = new StringBuilder(18);
        for (int i = 0; i < 17; i++) {
            sb.append(SECURE_RANDOM.nextInt(10));
        }
        return sb.toString();
    }
  
		private static byte[] hmacSha256(String data, byte[] key) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
        mac.init(keySpec);
        return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
    }
  
    public static Map<String, String> generateHeaders(String secretKey, String accessKey, String userAgent, String posId, String serialNumber) throws Exception {
        String currentDate = String.valueOf(Instant.now().getEpochSecond());
        String randomValue = generateRandomString();
        byte[] kSigning = hmacSha256(userAgent, secretKey.getBytes(StandardCharsets.UTF_8));
        kSigning = hmacSha256(currentDate, kSigning);
        kSigning = hmacSha256(randomValue, kSigning);
        String signatureHex = HexFormat.of().formatHex(kSigning);
      
				Map<String, String> headers = new HashMap<>();
        headers.put("content-type", "application/json");
        headers.put("Accept", "application/json");
        headers.put("X-tu-date", currentDate);
        headers.put("X-tu-random", randomValue);
        headers.put("X-tu-serial", serialNumber);
        headers.put("User-Agent", userAgent);
        headers.put("X-pos-id", posId);
        headers.put("x-tu-authorization", "protocol:TU1,accesskey:" + accessKey + ",signedheaders:User-Agent;X-tu-date;X-tu-random,signature:" + signatureHex);
      
				return headers;
    }

    public static void main(String[] args) throws Exception {
        String apiUrl = "https://paymentuat.test.thumbzup.com/posbuddy-cloud/v/1/payment/sync";
        String accessKey = "AAABBSS9NLLKKKK1MMMM"; // Provided by Ecentric
        String secretKey = "11rAAAceqB1RBBkYDcgjGtZSjgWpLxGlZeYetpET"; // Provided by Ecentric
        String merchantID = "910100000000001";
        String serialNumber = "PC05P2CG10036"; // The serial number of the target payment terminal for this payment request
        String userAgent = "MyPOSApp/1.0";
        String posId = "POS-STORE123-TERM01"; // Unique identifier for the POS sending the request
      
				JSONObject extraParameters = new JSONObject();
        extraParameters.put("merchantName", "test merchant");
        extraParameters.put("transactionUuid", UUID.randomUUID().toString());
      
				JSONObject requestBody = new JSONObject();
        requestBody.put("launchType", "SALE");
        requestBody.put("merchantID", merchantID);
        requestBody.put("posId", posId);
        requestBody.put("serialNumber", serialNumber);
        requestBody.put("transactionAmount", 1500);
        requestBody.put("extraParameters", extraParameters);
      
				System.out.println("Sending request: " + requestBody + "...\n");
      
				Map<String, String> headers = generateHeaders(secretKey, accessKey, userAgent, serialNumber, posId);
        RequestBodyEntity request = Unirest.post(apiUrl)
                .headers(headers)
                .body(requestBody.toString());
      
				HttpResponse<JsonNode> response = request.asJson();
      
				System.out.println("Received response: " + response.getBody());
    }
}

5. API Verifies the Signature

On the server side, the API:

The API verifies the incoming request's authenticity by reconstructing the signature on the server side using the shared secret key. It applies the same HMAC-SHA256 chaining logic to the request headers User-Agent, X-tu-date, and X-tu-random, in that exact order. The server then compares the computed signature against the signature value provided in the X-tu-authorization header. If the signatures match, the request is trusted and processed; otherwise, it is rejected with an authorization error.

📘

Security Notes:

  • Keep you secret key secure, never send it in requests.
  • Always use a new X-tu-random per request to prevent replay attacks.
  • Ensure your device clocks are synced to UTC (Important for timestamp validation).