Skip to content
logo
  • Products
    • Amwal Checkout
    • Merchant App
    • Merchant Control Panel
  • Pricing
  • Developers
  • About us
  • Contact Us
Edit Content
  • Products
    • Amwal Checkout
    • Merchant App
    • Merchant Control Panel
  • Pricing
  • Developers
  • About us
  • Contact Us
Login
Get Started
Login
Get Started
BA Booking

BA Booking

2
  • BA Booking overview
  • BA Booking Installation
amwalpay woocommerce

Woocommerce

2
  • Overview
  • Woocommerce Installation
CS-Cart

CS Cart

2
  • CS-Cart Overview
  • CS CART Installation
Shopify

Shopify

2
  • Shopify Overview
  • Shopify Installation
odoo

Odoo

2
  • Odoo Overview
  • Odoo Installation
whmcs

WHMCS

2
  • WHMCS Overview
  • WHMCS Installation
Magento

Magento

2
  • Magento 2 Overview
  • Magento Installation
Opencart

Opencart

2
  • OpenCart Overview
  • Opencart Installtion
ZenCart

ZenCart

2
  • ZenCart Overview
  • Zencart Installation
Drupal

Drupal

2
  • Drupal Overview
  • Drupal Installation
PrestaShop

PrestaShop

2
  • PrestaShop Overview
  • PrestaShop Installation
Contact Form 7

Contact Form 7

2
  • Contact Form 7 Overview
  • Contact Form 7 Installation
Joomla

Joomla

2
  • Joomla Overview
  • Joomla Installation
AMWAL INTEGRATED PAYMENT LINK

Integrated Payment Link

1
  • Implementation

ECR

5
  • ECR SDK for iOS Cocoapods
  • ECR SDK for iOS SPM
  • Android ECR
  • Flutter ECR
  • React Native ECR
Android SDk

Native Android SDK

2
  • Pre Requisites
  • Implementation
ios SDk

Native iOS SDK

3
  • Apple Pay Specific Configuration
  • Pre Requisites
  • Implementation
React SDk

React Native SDK

1
  • Implementation

Laravel Package

2
  • Installation
  • Configuration

Flutter SDK

2
  • Implementation
  • Flutter SDK Overview

SMARTBOX

4
  • Express ApplePay Implementation
  • Offsite Implementation
  • Pre Requisites
  • Implementation

Merchant Cloud Notification

1
  • Merchant Cloud Notification Integration Guide

Secure Hash Calculation

1
  • Secure Hash Calculation

Wp Travel Engine

1
  • Wp Travel Engine Installation

Webhooks

12
  • Get Order by Merchant Reference
  • Get Transaction by Merchant Reference
  • Acquiring Session Token
  • Initialize Payment
  • Encryption and Decryption
  • Refund Payment
  • Void Payment
  • Get Transaction by ID
  • Transactions with Statistics
  • Transactions Summary
  • Delete Customer Token
  • Pay by Token
View Categories
  • Home
  • Amwal Pay Developer Portal
  • Mobile SDKs
  • ECR
  • Android ECR

Android ECR

1. Before you start #

You need three things.

A terminal in ECR mode. Amwal’s TMS decides this, not your application. Ask whoever administers the terminal profile to set:

FieldValue
terminalMode1 (ECR-attached)
ecrMode1 USB cable, 2 Wi-Fi, or 4 Web Service

Until TMS sets terminalMode: 1, the terminal does not accept ECR requests. With ecrMode: 1 or 2 it listens on TCP port 9100; with ecrMode: 4 it does not listen on TCP — the till uses HTTPS to the Hub instead (see the table below).

ecrModeTransportSDK client
2 Wi-FiTCP to terminal IP, port 9100EcrTerminal via EcrLink.Lan
1 USB cableAndroid Open Accessory over a cable, no IPEcrTerminal via EcrLink.UsbCable and an EcrChannel
4 Web ServiceHTTPS to Amwal HubEcrWebServiceTerminal via EcrLink.WebService
3 BluetoothNot served over TCP in this SDK—

The same network (LAN only). For Wi-Fi ECR the till and the terminal must be able to reach each other on the LAN. Web Service ECR only needs internet access from the till.

The terminal’s address. With ecrMode: 2 the terminal shows its IP and port under the card scheme logos on its idle screen. Read it off and store it against the terminal’s serial number.

Store terminals by serial number and look the address up when you send. A terminal that reconnects to Wi-Fi can come back on a different IP, and the serial is the thing that does not change.

Requirements: Java 17, Kotlin coroutines. #

2. Add the SDK #

The SDK is on Maven Central, so it resolves like any other public library — no private repository, no credentials.

// build.gradle.kts  (or settings.gradle.kts, if you declare repositories there)
repositories {
    mavenCentral()
}

dependencies {
    implementation("com.amwal-pay:ecr-sdk:1.0.5")
}

Groovy:

repositories { mavenCentral() }

dependencies {
    implementation 'com.amwal-pay:ecr-sdk:1.0.5'
}

Maven:

<dependency>
    <groupId>com.amwal-pay</groupId>
    <artifactId>ecr-sdk</artifactId>
    <version>1.0.5</version>
</dependency>

The current version is listed at https://central.sonatype.com/artifact/com.amwal-pay/ecr-sdk. Pin it — do
not use a version range. Published versions never change, so a fix arrives as a new version and you decide when to take it; a range would let a payment client change behaviour without a commit on your side.

kotlinx.serialization is an implementation detail; it is not on the public API, so it cannot clash with your own JSON library. Coroutines are on the public API and come in transitively — you do not need to declare them.


3. Address a terminal #

The SDK separates how you reach a terminal (EcrLink) from credentials and till settings (EcrConfig).
Use EcrSessions.plan to validate both before you send — it returns issues rather than throwing, so your UI can show what is missing.

LAN / Wi‑Fi ECR #

val plan = EcrSessions.plan(
    link = EcrLink.Lan(host = "192.168.1.50", port = 9100),
    config = EcrConfig(
        secureHashKey = secrets.lanKeyFor(terminal.serialNumber),
        ecrId = "TILL-04",
    ),
    rawSecureHashKey = secrets.lanKeyFor(terminal.serialNumber),
)

if (!plan.isReady) {
    screen.show(plan.issues.joinToString("\n"))
    return
}

val terminal = EcrSessions.lanTerminal(
    terminalSerial = "P653200085189",
    plan = plan,
    logger = EcrLogger { Log.d("Ecr", it) },
)

One instance addresses one terminal. It holds no connection between calls, so it is cheap to keep and safe to reuse for the life of your application.

Probe before you pay. probeReachability() opens a TCP handshake and returns whether the listener answered, plus the underlying error when it did not:

when (val probe = terminal.probeReachability()) {
    probe.reachable -> proceed()
    else -> screen.show("${probe.host}:${probe.port} — ${probe.error ?: "not reachable"}")
}

isReachable() is a convenience wrapper around probe.reachable.

Everything else has a working default. To change the till’s identity, the currency, or the timeouts, pass them in EcrConfig:

EcrConfig(
    ecrId = "TILL-04",          // appears on the terminal's records
    currencyCode = "512",       // ISO 4217 numeric — 512 is OMR
    minorUnitDigits = 3,        // decimal places the currency has
    port = 9100,
    responseTimeout = 120.seconds,
)

minorUnitDigits must match your currency. It decides where the decimal point falls when the amount goes on the wire. Setting 840 (USD) while leaving 3 sends every amount ten times too large, and neither the SDK nor the terminal will object. See EcrConfig.

What the app owns vs what the SDK owns #

Your applicationSDK
Terminal registry (serial, IP, mode, merchant/terminal IDs)Transport choice, signing, wire format
Secure hash keys (from Amwal, per mode)Validation via EcrSessions.plan()
UI and credential storageHub URLs (EcrEnvironment), REST paths
Mapping stored records → EcrLinkBuilding EcrTerminal / EcrWebServiceTerminal

Do not duplicate transport rules in the app — pass a link and config, let EcrSessions validate and assemble the client.


4. Web Service ECR #

When TMS sets ecrMode: 4, the till talks to the Amwal Hub over HTTPS instead of TCP to the terminal’s IP. The SDK resolves Hub URLs from EcrEnvironment; your app only picks SIT, UAT, or PROD and supplies backend IDs plus the Web Service secure hash key — which is not the same secret as LAN ECR.

val plan = EcrSessions.plan(
    link = EcrLink.WebService(
        merchantId = "13593",
        terminalId = "101311",
    ),
    config = EcrConfig(
        secureHashKey = secrets.webServiceKey,
        environment = EcrEnvironment.UAT,
    ),
    rawSecureHashKey = secrets.webServiceKey,
)

if (!plan.isReady) return screen.show(plan.issues.joinToString("\n"))

val terminal = EcrSessions.webServiceTerminal(
    terminalSerial = "P653200008251-Web",
    plan = plan,
    logger = EcrLogger { Log.d("EcrWebService", it) },
)

when (val result = terminal.sale(BigDecimal("1.234"))) {
    is EcrResult.Approved -> completeSale(result)
    is EcrResult.Declined -> screen.show(result.reason)
    is EcrResult.Failed   -> handleUnknownOutcome(result.failure)
}

Web Service signing uses field secureHashValue and the same sorted key=value&… payload as LAN ECR, but under the Web Service key Amwal issued for REST. Using the LAN key produces valid-looking code that the Hub rejects.

Receipt fetch is LAN-only today — use Web Service for sale, void, refund, and inquiry by receipt number (STAN + date).

LAN vs Web Service — what the SDK supports today #

CapabilityLAN (EcrTerminal)Web Service (EcrWebServiceTerminal)
Sale / void / refundYesYes
Inquiry by receipt (STAN + date)YesYes
Inquiry by merchant referenceYes — inquireByReference()Not wired — method exists but does not send originalMerchantReference on the wire
Receipt (e-receipt URL)Yes — receipt()No
originalTerminalId (other till)Yes — void/refund/inquiry/receiptNo — not on the public API or JSON body
Reachability probeYes — probeReachability()No — use Hub connectivity instead
Auto-inquire after unknown failureYes — EcrConfig.autoInquireOnFailure (default true)No
Request signing fieldsecureHash + noncesecureHashValue (no nonce)
Response signature checkYes (LAN)No — Hub JSON envelope only

Use separate signing keys per row. EcrSessions.plan validates the right credentials for each mode before you send.


5. Your first sale #

suspend fun takePayment(amount: BigDecimal) {
    if (!terminal.isReachable()) {
        screen.show("Terminal not reachable — check it is on and on this network")
        return
    }

    when (val result = terminal.sale(amount)) {
        is EcrResult.Approved -> {
            receipt.print(amount = result.amount, rrn = result.rrn, auth = result.authCode)
        }
        is EcrResult.Declined -> screen.show(result.reason)
        is EcrResult.Failed   -> screen.show(result.failure.message)
    }
}

sale suspends while the cardholder pays. That is normally a few seconds and can be ninety; keep your progress UI up and do not impose a shorter timeout of your own.

Use BigDecimal, never Double. 0.1 + 0.2 is not 0.3 in binary floating point, and this is money.

terminal.sale(BigDecimal("1.234"))   // correct
terminal.sale(BigDecimal(1.234))     // wrong — the Double is already imprecise

Check first (LAN only). isReachable() and probeReachability() apply to EcrTerminal TCP connections only — not Web Service ECR. They turn a wrong address or a terminal on another network into an immediate answer rather than a wait the cardholder sits through. They prove the port is open, not that the terminal is idle — a terminal already taking a payment answers the probe and then rejects the request as busy.

when (val probe = terminal.probeReachability()) {
    probe.reachable -> proceed()
    else -> screen.show("${probe.host}:${probe.port} — ${probe.error ?: "not reachable"}")
}

6. Handling the three outcomes #

EcrResult has three cases and the difference between the last two is the whole game:

MeaningWhat a till should do
ApprovedMoney was takenComplete the sale
DeclinedThe terminal answered and refusedAsk for another payment method
FailedNothing was learnedDo not retry blindly — see §9
when (val result = terminal.sale(amount)) {
    is EcrResult.Approved -> {
        result.amount           // "1.234" — what was actually taken, major units
        result.responseCode     // "00"
        result.rrn              // retrieval reference number
        result.authCode         // authorisation code; placeholder on a void
        result.maskedPan        // "543173xxxx5785" — may be empty
        result.partialApproval  // see §7
        result.raw              // the terminal's full answer, as JSON text
    }
    is EcrResult.Declined -> {
        result.responseCode
        result.reason
        result.nextStep       // NONE, or INQUIRE_BY_MERCHANT_REFERENCE
        result.raw
    }
    is EcrResult.Failed -> {
        result.failure
        result.recovered      // LAN auto-inquiry finding, when configured
        result.nextStep       // always INQUIRE_BY_MERCHANT_REFERENCE
        result.settled        // true when recovered is Found
    }
}

result.raw is the terminal’s answer verbatim, as text. It is there so you never lose a field the SDK does not surface — log it with the transaction.

Do not decide from responseCode. A partial approval that the operator then voids carries the bank’s own 00. EcrResult already accounts for this: an outcome that is not an approval never arrives as Approved.

Signing what you send #

Anything on the shop network can reach the terminal’s port. Signing is what separates your till from everything else on that network.

Amwal issues secrets per terminal and per transport. Put them in the config and the SDK signs every LAN request and verifies LAN responses (when a key is configured). Web Service requests are signed; Hub responses are read from the JSON envelope without a local signature check.

// LAN ECR — field secureHash, Wi‑Fi and USB cable key
val lanPlan = EcrSessions.plan(
    link = EcrLink.Lan(host = terminal.ipAddress),
    config = EcrConfig(secureHashKey = secrets.lanKeyFor(terminal.serialNumber)),
    rawSecureHashKey = secrets.lanKeyFor(terminal.serialNumber),
)

// Web Service ECR — field secureHashValue, REST key (different secret)
val wsPlan = EcrSessions.plan(
    link = EcrLink.WebService(
        merchantId = terminal.merchantId,
        terminalId = terminal.terminalId,
    ),
    config = EcrConfig(
        secureHashKey = secrets.webServiceKey,
        environment = EcrEnvironment.UAT,
    ),
    rawSecureHashKey = secrets.webServiceKey,
)

Both transports use the same algorithm — HMAC-SHA256 over a sorted key=value&… string, key decoded from hex — but different field names and different secrets. Using the LAN key against the Hub (or the Web Service key over TCP) fails with authentication errors that look like network problems.

Validate keys with EcrConfig.isValidSecureHashKey() in your settings UI before persisting them.

Three things signing buys you, and one it does not:

  • Nothing on the network can issue a refund on your terminal
  • Nothing can change an amount in flight
  • Nothing can replay a captured sale, and nothing can fake an approval
    back to your till
  • It does not hide amounts from a passive listener — the traffic is
    authenticated, not encrypted. There is no card number in it to protect

Treat keys like passwords: not in your repository, not in a log, and not shared between customers. If one leaks, ask Amwal to rotate it.

Until a terminal has been issued a key it accepts unsigned messages, so you can configure this before or after Amwal switches it on without a flag day.

If every call comes back Failure.Unauthenticated, the key here and the key on the terminal (or Hub) do not match — and the wrong key type for the mode is the first thing to check.

Naming the transaction as your system does #

Every outcome carries a merchantReference, and every operation takes one.

If your system already numbers what it sells — an order number, a basket id, a ticket number — pass that, and the same string identifies the transaction in your books, in the terminal’s records and in any later inquiry. There is nothing to map and nothing to reconcile by timestamp:

val result = terminal.sale(amount, merchantReference = order.number)
// result.merchantReference == order.number

Leave it out and the SDK generates one, and reports it back on the result. Store it with the sale either way — it is what names the transaction afterwards:

val result = terminal.sale(amount)
orders.record(order.id, reference = result.merchantReference)

The reference is 1–32 characters of printable ASCII, without spaces and without & or =. Anything else throws IllegalArgumentException before a connection is opened, so a bad reference fails at the till rather than at the terminal.

Uniqueness is yours to decide: the terminal carries the reference and does not check it. Reusing one is allowed and is occasionally what you want — a retry of the same order — but two genuinely different sales sharing a reference will be indistinguishable in your own records later.


7. Partial approval #

The bank may authorise less than was asked for. That is an approval, not a refusal, and the customer still owes the difference:

is EcrResult.Approved -> {
    if (result.partialApproval) {
        val short = result.requestedAmount.toBigDecimal() - result.amount.toBigDecimal()
        screen.show("Only ${result.amount} was approved. Collect $short by other means.")
        // Do not release the goods until the balance is settled.
    } else {
        completeSale(result.amount)
    }
}

amount is always what was actually taken. Ignoring partialApproval means handing over goods for money you did not receive.


8. Void, refund, inquiry, receipt #

Void — cancel a transaction in full #

val result = terminal.void(receiptNumber = "215")

No amount: a void returns exactly what the original took. No card is presented for a transaction taken on the same terminal, so it completes in about a second.

Voids can be refused before anything reaches the backend — the original was not found, it is outside the void window TMS configured, or it cannot be voided (usually because it already was). Those arrive as Declined with the reason.

Do not print authCode on a void. It reverses an authorisation rather than making one, so the terminal returns a placeholder; printing it reads as an approval that never happened.

Refund — return money against an earlier transaction #

val result = terminal.refund(
    amount = BigDecimal("0.500"),
    receiptNumber = "208",
    transactionDate = "20260809",   // yyyyMMdd
)

The date is required because a receipt number is only unique within a terminal’s day. The cardholder must present their card to receive the money.

Whether the refund is allowed, and for how much, is the backend’s decision. Do not pre-validate it in your till; send it and report the answer.

Inquiry — ask what became of a transaction #

when (val inquiry = terminal.inquire(receiptNumber = "208", transactionDate = "20260809")) {
    is EcrInquiry.Found -> {
        val txn = inquiry.transaction
        screen.show("${txn.type} ${txn.status} ${txn.amount} ${txn.currency}")
        if (txn.canVoid) enableVoidButton()
    }
    is EcrInquiry.NotFound -> screen.show(inquiry.reason)
    is EcrInquiry.Failed   -> screen.show(inquiry.failure.message)
}

status is the transaction’s outcome, not the inquiry’s — finding a declined sale is a successful inquiry. canVoid and canRefund have already had the void window and the backend’s rules applied.

Safe to repeat, and answered even while the terminal is taking a payment.

Inquiry by merchant reference (LAN only). After a lost sale, you often have the merchantReference but not yet a receipt number:

when (val inquiry = terminal.inquireByReference(
    originalReference = order.number,
    transactionDate = "20260809",   // optional on LAN; narrows the search
)) {
    is EcrInquiry.Found -> reconcile(inquiry.transaction)
    is EcrInquiry.NotFound -> // safe to retry the sale
    is EcrInquiry.Failed   -> escalate()
}

On Web Service, use inquire(receiptNumber, transactionDate) with a STAN until reference lookup is wired on the Hub JSON body.

Receipt — the e-receipt as a QR code (LAN only) #

// EcrTerminal only — not available on EcrWebServiceTerminal
when (val receipt = terminal.receipt(receiptNumber = "215", transactionDate = "20260809")) {
    is EcrReceipt.Ready       -> screen.showQrCode(receipt.url)
    is EcrReceipt.Unavailable -> screen.show(receipt.reason)
    is EcrReceipt.Failed      -> screen.show(receipt.failure.message)
}

The customer scans the code and reads the receipt on their own phone. Render the QR locally — it then appears the moment the URL arrives and survives the network dropping afterwards.

Acting on another terminal’s transaction (LAN only) #

On LAN ECR, a void, refund, inquiry or receipt can target a transaction taken on a different till. EcrWebServiceTerminal does not expose originalTerminalId today.

terminal.void(receiptNumber = "215", originalTerminalId = "31629")

Leave it out and the terminal assumes its own.


9. When the answer never comes #

This is the most important section in this guide.

is EcrResult.Failed -> when (val failure = result.failure) {
    is Failure.Unreachable    -> // nothing is listening: safe, nothing happened
    is Failure.Timeout        -> // UNKNOWN — the payment may have completed
    is Failure.ConnectionLost -> // UNKNOWN — the payment may have completed
    is Failure.Malformed      -> // an answer the SDK could not read
}

Unreachable is safe: the connection was refused, so nothing was sent. The other three are unknown outcomes. The card flow on the terminal does not depend on the socket staying up, so the terminal may have taken the money and simply been unable to say so.

Never treat an unknown outcome as a decline. Resolve it:

suspend fun resolveUnknownOutcome(receiptNumber: String, date: String): Boolean {
    // Safe to call as often as you like, and answered even mid-transaction.
    return when (val inquiry = terminal.inquire(receiptNumber, date)) {
        is EcrInquiry.Found -> inquiry.transaction.status.equals("Approved", true)
        is EcrInquiry.NotFound -> false      // it never happened; retrying is safe
        is EcrInquiry.Failed -> {
            escalateToOperator()             // still unknown — a human must look
            false
        }
    }
}

With EcrConfig.autoInquireOnFailure = true (the default, LAN only), EcrTerminal follows an unknown failure with inquireByReference() using the sale’s merchantReference and attaches the
finding to EcrResult.Failed.recovered. Check result.recovered and result.settled before prompting the operator to inquire manually. EcrWebServiceTerminal does not auto-inquire.

When you resolve manually:

  • By reference (after a lost sale where you stored merchantReference) —
    inquireByReference() on LAN only today.
  • By receipt number — inquire(receiptNumber, transactionDate) on either
    transport, when you already have the STAN from a partial answer or the terminal
    display.

Retrying a sale blind is how a customer gets charged twice. Reconcile against the terminal’s batch if both routes still leave the outcome unknown.


10. Building your own form #

EcrTransactionType declares what each operation needs, so drive your UI from it instead of repeating the rules:

val type = EcrTransactionType.REFUND

if (type.requiresAmount)       showAmountField()
if (type.requiresOriginalStan) showReceiptNumberField()
if (type.requiresOriginalDate) showDateField()
if (type.allowsOtherTerminal)  showOtherTerminalSwitch()

terminal.run(type, amount = amount, originalStan = receipt, originalDate = date)
requiresAmountrequiresOriginalStanrequiresOriginalDatemovesMoney
SALE✔✔
VOID✔✔
REFUND✔✔✔✔
INQUIRY✔✔
RECEIPT✔✔

run() accepts only the types that move money. INQUIRY and RECEIPT go through inquire() and receipt(), because they answer with something they found rather than something they performed — conflating the two is how a lookup gets booked as a payment.

EcrTransactionType.menuOptions is the subset an operator picks from: sale, void, refund, inquiry. RECEIPT is absent by design — a receipt follows a transaction already on screen, so it belongs on that result rather than in a list of operations.

Validate only what you can answer. Whether an amount is within the terminal’s limits, whether a transaction may be voided, whether a refund is allowed — all of that needs the payment backend and the terminal’s TMS configuration. Send the request and report the answer.


11. Threading and lifecycle #

Every call is a suspend function and does its blocking I/O on Dispatchers.IO. You do not need a thread of your own.

class TillViewModel : ViewModel() {
    private val terminal = EcrTerminal(host = "192.168.1.50", serialNumber = "P653…")

    fun pay(amount: BigDecimal) {
        viewModelScope.launch {
            val result = terminal.sale(amount)   // suspends; does not block the UI
            _state.value = result
        }
    }
}

Cancellation stops the SDK waiting; it does not stop the transaction. If your screen closes mid-payment the terminal carries on with the cardholder. Treat a cancelled call exactly like Failure.Timeout — an unknown outcome.

Hold the in-flight call somewhere that survives a configuration change (a ViewModel, not an Activity), or a rotation mid-payment loses the result.

Pass your own dispatcher when you need to control it, for instance in tests:

EcrTerminal(host = "…", serialNumber = "…", io = testDispatcher)

12. Logging #

Silent by default — the SDK never writes to a log you did not choose.

// LAN ECR — Logcat tag "Ecr" in the sample app
EcrSessions.lanTerminal(
    terminalSerial = "P653200085189",
    plan = lanPlan,
    logger = EcrLogger { message -> Log.d("Ecr", message) },
)

// Web Service ECR — separate tag so mixed logs stay readable
EcrSessions.webServiceTerminal(
    terminalSerial = "P653200008251-Web",
    plan = wsPlan,
    logger = EcrLogger { message -> Log.d("EcrWebService", message) },
)

When signing is enabled, diagnostic output includes:

  • The formula: HMAC-SHA256(sorted key=value payload, Hex.parse(secret)) → uppercase hex
  • A fingerprint of the key (length and first/last four hex characters) — never the full secret
  • The payload string being signed (excluding the hash field itself)
  • The computed hash value

Request and response payloads may contain masked PANs and transaction references. They are safe from a PCI perspective but are still transaction records — store them accordingly and never forward them to analytics or crash reporters unchanged.

EcrConfig.toString() redacts secureHashKey as <redacted> if you log config objects during debugging.


13. Testing without hardware #

A stand-in listener that speaks the protocol is published alongside the SDK:

curl -O https://raw.githubusercontent.com/amwal-pay/ECR-simulator/main/tools/fake_pos_server.py

python fake_pos_server.py --port 9100                      # approves everything
python fake_pos_server.py --port 9100 --decline 51          # declines everything
python fake_pos_server.py --port 9100 --delay 8             # slow cardholder
python fake_pos_server.py --port 9100 --not-found           # inquiries answer 25

Register a terminal in your till pointing at your machine’s LAN IP and that port.

--delay is the case worth testing and the one most often skipped: what your UI does while it waits, and what happens when the operator walks away mid-sale.

Also test, before you ship:

  • Terminal switched off → Failure.Unreachable
  • Wi-Fi dropped mid-sale (turn the machine’s Wi-Fi off after sending) →
    Failure.ConnectionLost, and confirm you do not book it as a decline
  • A second request while one is running → 96, terminal busy
  • A void of an unknown receipt number → 25

14. Going to production #

A checklist worth walking before the first live terminal:

  • minorUnitDigits matches the currency — check an amount on the wire
  • Amounts are BigDecimal end to end, never Double
  • approved decides the outcome, not responseCode
  • partialApproval is handled, and goods are held back for the balance
  • Timeout / ConnectionLost / Unauthenticated are never treated as declines
  • On LAN, check EcrResult.Failed.recovered / settled before retrying a sale
  • LAN and Web Service keys are stored separately and mapped to the correct mode
  • secureHashKey comes from configuration rather than source code
  • Keys are not written to any log, crash report or analytics event
  • EcrSessions.plan() is used (or equivalent validation) before sending
  • authCode is not printed on a void
  • Terminals are stored by serial number, address looked up at send time
  • result.raw is logged with each transaction for dispute resolution
  • result.merchantReference is stored with the sale, generated or your own
  • The in-flight call survives a rotation or a backgrounded app
  • TMS has the correct terminalMode and ecrMode on every terminal
  • Someone other than the developer has run the failure cases in §13

Updated on September 10, 2026

What are your Feelings

  • Happy
  • Normal
  • Sad

Share This Article :

  • Facebook
  • X
  • LinkedIn
  • Pinterest
ECR SDK for iOS SPM
Table of Contents
  • 1. Before you start
  • Requirements: Java 17, Kotlin coroutines.
  • 2. Add the SDK
  • 3. Address a terminal
    • LAN / Wi‑Fi ECR
    • What the app owns vs what the SDK owns
  • 4. Web Service ECR
    • LAN vs Web Service — what the SDK supports today
  • 5. Your first sale
  • 6. Handling the three outcomes
    • Signing what you send
    • Naming the transaction as your system does
  • 7. Partial approval
  • 8. Void, refund, inquiry, receipt
    • Void — cancel a transaction in full
    • Refund — return money against an earlier transaction
    • Inquiry — ask what became of a transaction
    • Receipt — the e-receipt as a QR code (LAN only)
    • Acting on another terminal's transaction (LAN only)
  • 9. When the answer never comes
  • 10. Building your own form
  • 11. Threading and lifecycle
  • 12. Logging
  • 13. Testing without hardware
  • 14. Going to production

Secure. Seamless – Powering Payments for Every Business.

Sign Up
Support

4th Floor, Majan Tower Building
North Al Ghubrah, P.O. Box 233, P.C 118
Muscat, Sultanate of Oman

: support@amwal-pay.com

📞 : +96824121845

Resources
  • Developers
  • Careers
Company
  • About us
  • Contact Us
  • Contact Sales
  • Partners

2026 © AmwalPay. All Rights Reserved.

  • Terms & Conditions
  • Privacy Policy