Protected Documentation
LESS Integration Guide
Enter authentication key to access the SDK reference.
Invalid key. Please check and retry.
Android EDC Integration Guide

LESS SDK Developer Reference

Technical guide for integrating Android EDC terminals with the LESS digital receipt platform.

SDK Version: 2.2.0 Target: Android API 26+ Client: OkHttp 5.4+

Overview & Scope

The LESS SDK encapsulates all cryptographic signing, clock drift mitigation, response model parsing, and network retry routines required for Android EDC integration.

Managed by LESS SDK

  • HMAC-SHA256 signature generation (String-to-Sign v1.2)
  • Cryptographic nonce (UUID) and Unix timestamp generation
  • Automatic clock-skew retry on HTTP 401 drift
  • Connection lifecycle and OkHttp client pooling
  • Provisioning exchange (provision())
  • Type-safe payload builders (SlipRequestBuilder, BatchRequestBuilder)
  • Response deserialization and structured error classification

EDC Application Responsibilities

  • Retrieve bootstrap code via TMS or provisioning channel
  • Store provisioned secret in Android KeyStore (AES/GCM)
  • Map hardware transaction data to Builder parameters
  • Render QR code from trustedUrl and display claimCode
  • Maintain local offline SQLite queue when offline
  • Handle customer PDPA consent for SMS and Email delivery

Environment & Setup

API Base URL

val baseUrl = "https://less-api.loxbit.co.th"
Endpoint Configuration
Do not set baseUrl to customer-facing portal domains (e.g. less-customer.loxbit.co.th). The SDK requires direct access to the core API host on port 443.

Network Requirements

Parameter Specification
Protocol HTTPS (TLS 1.2+ mandatory, Port 443)
TLS Certificate Public Trusted Root (Sectigo) — No custom CA pinning required
Connectivity Pre-flight curl -I https://less-api.loxbit.co.th/healthz

System Flows

1. Device Provisioning Flow

Executed once per device lifecycle during initial setup before processing any transactions.

2. Transaction Flow (Online & Offline Queue)

Installation

1. Git Submodule Configuration

git submodule add http://172.29.220.3:3000/panuwat/terminal-sdk-android.git terminal-sdk-android

2. Gradle Project Setup

In settings.gradle.kts:

include(":terminal-sdk-android")
project(":terminal-sdk-android").projectDir = File("./terminal-sdk-android")

In app/build.gradle.kts:

dependencies {
    implementation(project(":terminal-sdk-android"))
}

In AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Build Prerequisites

Requirement Minimum Version Notes
JDK 17+ Required for AGP 8.2.2+
Android minSdk API 26 (Android 8.0) Modern KeyStore & Instant support
Android compileSdk API 34 Current platform target
Kotlin Plugin 2.2.21+ Unified across all Gradle modules

Step-by-Step Guide

Step 01
Provision Terminal

Exchange the 48-hour single-use bootstrap code for the permanent device secret key.

val result = LESSClient.provision(
    baseUrl = baseUrl,
    serial = deviceSerial,
    bootstrapCode = code
)
// result.serial -> echoes terminal serial
// result.secret -> save immediately to Android KeyStore
Step 02
Initialize Client

Instantiate LESSClient using the retrieved hardware serial and encrypted secret.

val client = LESSClient(
    serial = deviceSerial,
    secret = loadFromKeyStore()
)
Step 03
Submit Transaction (Online Slip)

Use SlipRequestBuilder to generate and sign the transaction payload.

val payload = SlipRequestBuilder()
    .header("1.0", requestId = UUID.randomUUID().toString(), timestamp = Instant.now())
    .transaction(rrn = "123456789012", refCode = "REF-1001", amount = 350.00, currency = "THB")
    .txnDateTime(Instant.now())
    .txnStatus("approved")
    .merchant(
        bankCode = "004",
        mid = "MID12345678",
        tid = "TID12345",
        serviceType = "PURCHASE",
        mainMid = mainMidFromTms,
        mainTid = mainTidFromTms
    )
    .deliveryQr()
    .buildJson()

val slip = client.postSlip(baseUrl, payload)
// Render QR from: slip.trustedUrl
// Display text code: slip.claimCode
Step 04
Offline Queue & Batch Synchronization

If network connection fails during transaction processing, batch sync offline items using BatchRequestBuilder.

val batch = BatchRequestBuilder()
    .header(requestId = UUID.randomUUID().toString(), timestamp = Instant.now())
    .merchant(
        bankCode = "004", mid = "MID12345678", tid = "TID12345", serviceType = "PURCHASE",
        mainMid = mainMidFromTms, mainTid = mainTidFromTms
    )
    .addItem { txn("RRN001", "REF001", 120.00, "THB").txnStatus("approved").deliveryQr() }
    .addItem { txn("RRN002", "REF002", 500.00, "THB").txnStatus("approved").deliveryQr() }
    .buildJson()

val syncResult = client.syncBatch(baseUrl, batch)
syncResult.results.forEach { item ->
    if (item.isSuccess) {
        removeFromLocalQueue(item.rrn)
    } else {
        logSyncFailure(item.rrn, item.errorCode)
    }
}
Step 05
Android KeyStore Implementation
private fun getOrCreateSecretKey(): SecretKey {
    val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
    if (!ks.containsAlias("less_device_secret")) {
        val kg = KeyGenerator.getInstance("AES", "AndroidKeyStore")
        kg.init(
            KeyGenParameterSpec.Builder(
                "less_device_secret",
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
            )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .build()
        )
        kg.generateKey()
    }
    return (ks.getEntry("less_device_secret", null) as KeyStore.SecretKeyEntry).secretKey
}

Builders Reference

SlipRequestBuilder Methods

Method Parameters Requirement Description
.header(...) version, reqId, time Required Sets schema version ("1.0"), UUID request ID, and timestamp.
.transaction(...) rrn, ref, amount, ccy Required Core transaction details (currency must be "THB").
.merchant(...) bank, mid, tid, svc Required Merchant scope. serviceType validated by server (e.g. PURCHASE, WELFARE).
.deliveryQr() - Required* QR code delivery on screen (*or deliverySms / deliveryEmail).
.deliverySms(...) mobile, consentTime Required* SMS delivery with explicit PDPA consent timestamp.
.deliveryEmail(...) emailAddress Required* Email delivery (RFC 5322 compliant format).
.cardData(pan, scheme) String, String Optional Masked PAN (e.g. "411111******1111"). Full PAN rejected.
.approvalCode(v) String Optional Host authorization approval code.
.traceNo(v) / .batchNo(v) String, String Optional Terminal trace and batch numbers.
.dcc(...) ccy, amt, rate, consent, fee Optional Dynamic Currency Conversion parameters (consent must be true).
.buildJson() - - Serializes validated payload to JSON String.

Endpoints & Models

POST /api/v1/terminals/provision

Exchanges single-use bootstrap code for permanent HMAC secret.

// Response Data
{
  "serial": "POS-987654321",
  "secret": "sec_live_9f823bc842918471"
}
POST /api/v1/slips

Registers an online electronic slip with cryptographic validation.

// Response Data
{
  "transaction_id": "txn_884210941",
  "token": "tok_live_c48b291a",
  "ref_code": "REF-1001",
  "claim_code": "481920",
  "trusted_url": "https://less-customer.loxbit.co.th/v1/s/tok_live_c48b291a",
  "expires_at": "2026-09-18T10:00:00Z"
}
POST /api/v1/sync/batch

Synchronizes queued offline slips in a single bulk request.

// Response Data
{
  "total": 2,
  "accepted": 2,
  "rejected": 0,
  "results": [
    {
      "rrn": "RRN001",
      "result": "accepted",
      "retryable": false,
      "trusted_url": "https://less-customer.loxbit.co.th/v1/s/tok_01"
    }
  ]
}

Error Codes

Error Code HTTP Condition Resolution
REPLAY_DETECTED 401 Timestamp skew > 10 min Automatically retried by SDK with server clock sync.
INVALID_SIGNATURE 401 HMAC-SHA256 mismatch Check device secret integrity and raw body structure.
INVALID_BOOTSTRAP_CODE 401 Code expired or invalid Request new bootstrap code from TMS channel.
TERMINAL_INACTIVE 403 Service type or TID mismatch Verify serviceType matches registration in TMS.
FULL_PAN_REJECTED 400 Unmasked PAN submitted Mask PAN (first 6, last 4 digits) before calling builder.
DUPLICATE_CONFLICT 409 RRN payload mismatch Ensure idempotent resubmission has identical payload.
SERVICE_UNAVAILABLE 503 Temporary backend outage Retry with exponential backoff (retryable=true).

Delivery & PDPA Consent

Method Mandatory Fields Validation Rules
qr None Screen QR code generated on terminal display.
sms mobile_number, consent_timestamp Requires explicit user PDPA consent opt-in.
email email_address Requires RFC 5322 valid format.
qr_and_sms mobile_number, consent_timestamp Dual delivery mode with consent verification.

SDK Source & AAR

Building Release AAR

cd terminal-sdk-android
./gradlew assembleRelease
# Output artifact: ./build/outputs/aar/terminal-sdk-android-release.aar

Package Structure

com.loxbit.less.sdk/
├── LESSClient.kt                   — Primary SDK client interface
├── LESSSecurityInterceptor.kt      — HMAC auth & clock-skew retry interceptor
├── SlipRequestBuilder.kt           — Type-safe builder for POST /api/v1/slips
├── BatchRequestBuilder.kt          — Type-safe builder for POST /api/v1/sync/batch
├── SlipCreateResponse.kt           — Transaction response model
├── SyncBatchResponse.kt            — Batch sync response models
├── ApiError.kt                     — Error codes and domain error classification
└── ProvisionResult.kt              — Device provisioning model