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
  • Flutter ECR

Flutter ECR

Overview #

Amwal Pay’s Flutter package provides a single, typed Dart API for controlling an Amwal POS terminal from an Android or iOS till.

The package acts as a bridge over the native Amwal ECR SDKs:

  • Android: Amwal Kotlin ecr-sdk
  • iOS: AmwalECR

It does not implement a separate payment protocol. Flutter applications therefore behave consistently with native Android and iOS applications when communicating with the same terminal.

Supported operations #

OperationDescription
SaleProcess a card payment
VoidCancel an earlier transaction in full
RefundReturn money against an earlier transaction
InquiryCheck the status of an earlier transaction
Inquiry by ReferenceFind a transaction using your merchant reference
E-ReceiptRetrieve a receipt URL for QR-code display
ReachabilityCheck whether the terminal is reachable

Important: No card data is passed through your Flutter application. At most, the application receives a masked PAN.


Critical Rule: Unknown Outcomes #

A failed communication exchange is not necessarily a declined payment.

A timeout or lost connection can happen after the terminal has accepted the request and completed the payment.

When:

result.outcomeIsUnknown == true

Do not send the sale again.

Instead, inquire about the transaction using the original merchant reference or receipt number.

Sale Request
     │
     ▼
Terminal
     │
     ├── Approved ───────► EcrApproved
     │
     ├── Declined ───────► EcrDeclined
     │
     └── No trustworthy answer
              │
              ▼
        EcrFailed
        outcomeIsUnknown
              │
              ▼
           Inquiry
              │
       ┌──────┴──────┐
       ▼             ▼
    Found         Not Found
       │             │
       ▼             ▼
 Reconcile       Safe to retry

The SDK never automatically retries a payment.


Why Use the Amwal Flutter ECR SDK? #

An outcome you cannot misread #

Payment results are represented by separate types:

switch (result) {
  case EcrApproved():
    // Money moved

  case EcrDeclined():
    // Terminal explicitly refused

  case EcrFailed():
    // No trustworthy answer
}

This prevents an important payment error: treating a communication failure as a decline and charging the customer twice.

No automatic payment retries #

The SDK never retries:

  • Timeout
  • Connection loss
  • Host error
  • Unknown outcome

There is no retry configuration that enables automatic payment retries.

Automatic recovery of unknown outcomes #

When autoInquireOnFailure is enabled, the SDK can automatically perform an inquiry after a money-moving request fails without a trustworthy response.

The recovered transaction is available through:

EcrFailed.recovered

Partial approvals are approvals #

If the bank approves less than the requested amount, the SDK returns an EcrApproved result containing both amounts.

case EcrApproved(
  :final bool partialApproval,
  :final String amount,
  :final String requestedAmount,
):
  if (partialApproval) {
    showMessage(
      'Only $amount of $requestedAmount was approved',
    );
  }

Signed responses #

Terminal responses are authenticated against the terminal’s configured signing key.

An invalid or unverifiable response is treated as an unknown outcome and is never accepted as a successful payment.

One API for multiple transports #

The same Dart API works across the supported transports:

  • Wi-Fi
  • USB cable
  • Web Service

Decimal-safe amounts #

Money is represented by EcrAmount, not double.

EcrAmount.parse('1.234');

EcrAmount.tryParse(controller.text);

EcrAmount.fromMinorUnits(
  1234,
  minorUnitDigits: 3,
);

This avoids floating-point rounding problems with monetary values.


Installation #

Requirements #

RequirementVersion
Dart SDK>= 3.5.0
Flutter>= 3.22.0
AndroidAPI 21+
Android JVMTarget 17
iOS12.0+

The API uses Dart sealed classes and pattern matching, therefore Dart 3.5 or later is required.

Add the package #

Add the dependency to pubspec.yaml:

dependencies:
  amwal_ecr: ^0.2.1

Or install it using:

flutter pub add amwal_ecr

The native SDKs are included automatically.

Android #

The package resolves:

com.amwal-pay:ecr-sdk

from Maven Central.

iOS #

The package resolves AmwalECR through CocoaPods.

Swift Package Manager is also supported when enabled with:

flutter config --enable-swift-package-manager

Platform Configuration #

Android #

The plugin declares the required Internet permission itself.

No additional application configuration is required.

iOS #

Add the following to:

ios/Runner/Info.plist
<key>NSLocalNetworkUsageDescription</key>
<string>Connects to the payment terminal to take card payments.</string>

iOS asks the user for permission before the application can communicate with devices on the local network.

If permission is denied, the result is indistinguishable from an unreachable terminal and calls return:

EcrUnreachable

Therefore, on iOS, treat a first-run EcrUnreachable as a possible local-network permission issue before assuming the terminal or network is unavailable.


Unsupported Platforms #

On:

  • macOS
  • Windows
  • Linux
  • Web

the SDK returns:

EcrUnsupported

instead of throwing MissingPluginException.

This allows applications running on an unsupported platform to degrade gracefully.


Terminal Setup #

Import the package:

import 'package:amwal_ecr/amwal_ecr.dart';

Create a shared ECR session:

final EcrOpenedSession session = EcrSessions.open(
  host: '192.168.1.50',
  serialNumber: 'P653200085189',
  transport: EcrTransport.wifi,
  config: EcrConfig(
    ecrId: 'TILL-01',
    currencyCode: '512',
    minorUnitDigits: 3,
    port: 9100,
    connectTimeout: Duration(seconds: 10),
    responseTimeout: Duration(seconds: 120),
    probeTimeout: Duration(seconds: 3),
    secureHashKey: secretFromSecureStorage,
    autoInquireOnFailure: true,
  ),
);

final EcrTerminal terminal = session.terminal;

Recommended approach #

Prefer:

EcrSessions.open(...)

instead of constructing an EcrTerminal directly.

This ensures that sale, inquiry and receipt operations share the same transport configuration.

An EcrTerminal does not maintain a connection between calls, so it is lightweight and safe to keep for the lifetime of the application.


ECR Configuration #

PropertyDescription
hostTerminal IP address or service host
serialNumberAmwal terminal serial number
transportCommunication transport
ecrIdUnique identifier for the till/ECR
currencyCodeCurrency code configured for the terminal
minorUnitDigitsNumber of decimal places used by the currency
portECR TCP port, normally 9100 for Wi-Fi
connectTimeoutConnection timeout
responseTimeoutMaximum time to wait for a transaction response
probeTimeoutReachability probe timeout
secureHashKeyTerminal signing key
autoInquireOnFailureAutomatically inquire after an unknown money-moving result

Currency and Amount Configuration #

minorUnitDigits is critical.

CurrencyCodeMinor-unit digitsExample
OMR51231.234
USD840212.34
JPY39201000

The SDK does not cross-check currencyCode against minorUnitDigits.

For example, configuring OMR with minorUnitDigits: 2 can cause amounts to be interpreted incorrectly.

OMR example #

final amount = EcrAmount.parse('1.234');

This represents:

1.234 OMR

Secure Hash Key #

The signing key is issued by Amwal for the terminal.

Store it securely using a platform keystore, for example:

flutter_secure_storage

Never:

  • Commit the key to source control
  • Hard-code it in Dart source
  • Store it in application assets
  • Include it in publicly accessible configuration

Supported Transports #

EcrTransport corresponds to the ecrMode configured in the terminal’s TMS profile.

TransportECR ModeAndroidiOSDescription
usbCable1YesNoUSB cable; no IP address
wifi2YesYesTCP connection to terminal
bluetooth3NoNoDefined in TMS but not served
webService4YesYesHTTPS through Amwal Hub

The terminal must have:

terminalMode = 1

in its TMS profile before it accepts ECR requests.

If a transport is unsupported by the current platform, the SDK immediately returns:

EcrUnsupported

EcrAmount #

All monetary values use EcrAmount.

Do not use double for payment amounts.

Parse an amount #

final amount = EcrAmount.parse('1.234');

Parse user input safely #

final amount = EcrAmount.tryParse(controller.text);

This returns null when the input is incomplete or invalid.

Create from minor units #

final amount = EcrAmount.fromMinorUnits(
  1234,
  minorUnitDigits: 3,
);

The resulting amount is:

1.234

Supported Operations #

MethodPurpose
isReachable()Check whether the terminal is responding
probeReachability()Check reachability and return the underlying error
sale()Take a payment
voidTransaction()Void an earlier transaction
refund()Refund an earlier transaction
inquire()Look up an earlier transaction by receipt
inquireByReference()Look up an earlier transaction by merchant reference
receipt()Retrieve an e-receipt URL

Merchant Reference #

Every money-moving operation accepts:

merchantReference

This should be your own unique transaction reference.

Example:

merchantReference: 'ORDER-1001'

The same reference can then identify the transaction in:

  • Your application
  • The terminal
  • Amwal transaction records
  • Later inquiry operations

If omitted, the SDK generates a reference.

The resulting value is returned in:

EcrResult.merchantReference

For unknown outcomes, retaining this reference is particularly important because it allows the transaction to be recovered through:

inquireByReference()

Take a Payment #

final EcrResult result = await terminal.sale(
  EcrAmount.parse('1.234'),
  merchantReference: 'ORDER-1001',
);

Handle the result using typed pattern matching:

switch (result) {
  case EcrApproved(
      :final String amount,
      :final String rrn,
    ):
    completeSale(amount, rrn);

  case EcrDeclined(
      :final String reason,
    ):
    showMessage(reason);

  case EcrFailed():
    await reconcile('ORDER-1001');
}

A sale can take as long as the cardholder needs to complete the payment.

A response time of around 90 seconds can be normal, so the till should display a progress state rather than assuming a short timeout means the payment failed.


Void a Transaction #

A void cancels an earlier transaction in full.

No card is required and no amount needs to be supplied.

final EcrResult result = await terminal.voidTransaction(
  '215',
);

The transaction must have been processed on the same terminal.

Cancelling the Flutter request does not cancel the transaction at the terminal.

If a money-moving operation is cancelled while the terminal is processing it, the outcome becomes unknown and the application must perform an inquiry.


Refund a Transaction #

final EcrResult result = await terminal.refund(
  EcrAmount.parse('0.216'),
  receiptNumber: '208',
  transactionDate: '20260809',
);

transactionDate uses:

yyyyMMdd

Example:

20260809

Whether a refund is allowed, and for what amount, is determined by the backend.

The till should not attempt to reproduce those business rules locally.


Inquiry #

Inquiry is a read-only operation used to determine what happened to a previous transaction.

final EcrResult result = await terminal.inquire(
  receiptNumber: '208',
  transactionDate: '20260809',
);

Inquiry:

  • Does not authorize a payment
  • Does not present a card
  • Does not change a transaction
  • Is safe to repeat
  • Can be performed while the terminal is processing another payment

Inquiry by Merchant Reference #

When the original receipt number is unavailable, use the merchant reference:

final EcrResult result =
    await terminal.inquireByReference(
  'ORDER-1001',
);

This is particularly important when recovering an unknown sale outcome because the merchant reference is known before the terminal responds.


Resolve an Unknown Payment #

The safest recovery flow is:

final EcrResult result = await terminal.sale(
  amount,
  merchantReference: order.number,
);

if (result.outcomeIsUnknown) {
  switch (await terminal.inquireByReference(order.number)) {
    case EcrInquiryFound(
        :final EcrTransaction transaction,
      ):
      reconcile(transaction);

    case EcrInquiryNotFound():
      // Nothing was taken.
      // Only now is it safe to retry the sale.

    case EcrInquiryFailed():
      // Still unknown.
      // Ask again later.
  }

  return;
}

Recovery rules #

Inquiry resultMeaningRetry sale?
EcrInquiryFoundTransaction existsNo
EcrInquiryNotFoundNothing was recordedYes, if appropriate
EcrInquiryFailedStill unknownNo

Never retry merely because the original sale timed out.


Automatic Inquiry #

Enable automatic recovery:

autoInquireOnFailure: true,

When enabled, a money-moving operation whose response is missing can be followed by an inquiry.

The recovered result is available through:

EcrFailed.recovered

When the SDK knows the final outcome after the communication failure, the failure can have a settled/recovered state and:

outcomeIsUnknown == false

Result Outcomes #

Every payment operation produces one of three fundamental outcomes:

EcrResult
├── EcrApproved
├── EcrDeclined
└── EcrFailed
ResultMeaning
EcrApprovedTerminal approved the transaction
EcrDeclinedTerminal answered and explicitly refused it
EcrFailedNo trustworthy answer was available

EcrApproved #

An approved result contains:

  • amount
  • rrn
  • authCode
  • maskedPan
  • partialApproval
  • requestedAmount
  • merchantReference

A partial approval is still an approval.

Example:

case EcrApproved(
  :final String amount,
  :final String requestedAmount,
  :final bool partialApproval,
):
  if (partialApproval) {
    showMessage(
      'Only $amount of $requestedAmount was approved',
    );
  }

EcrDeclined #

EcrDeclined represents an explicit decision from the terminal.

It contains:

responseCode
reason

Common codes include:

CodeMeaningMoney moved
12Unsupported operationNo
13Invalid amountNo
17Cancelled at terminalNo
25Original transaction not foundNo
63Invalid/missing signature or clock issueNo
96Another transaction is already runningNo
OtherBackend-specific responseDepends

Do not determine the final outcome from the response code alone. Use the typed result.


EcrFailed #

EcrFailed represents a communication or processing failure.

FailureOutcome known?Meaning
EcrUnreachableYesNothing is listening
EcrTimeoutUnknownRequest may have been accepted but no response arrived
EcrMalformedUnknownTerminal answered but response was unreadable
EcrConnectionLostUnknownConnection broke during communication
EcrUnauthenticatedUnknownResponse could not be verified
EcrCancelledUnknownApplication stopped waiting
EcrUnsupportedYesPlatform cannot perform the operation

Important #

For unknown failures:

DO NOT RETRY
        ↓
INQUIRE
        ↓
Determine what happened

Partial Approval #

A partial approval occurs when the requested amount is greater than the amount authorized by the issuer.

Example:

Requested: 2.000 OMR
Approved:  0.500 OMR

The response is still:

{
  "responseCode": "00",
  "approved": true,
  "partialApproval": true,
  "requestedAmount": "000000002000",
  "amount": "000000000500"
}

The SDK represents this as an approval rather than a decline.


E-Receipt #

Retrieve an electronic receipt:

final EcrResult result = await terminal.receipt(
  receiptNumber: '215',
  transactionDate: '20260809',
);

Handle the result:

switch (result) {
  case EcrReceiptReady(:final String url):
    showQrCode(url);

  case EcrReceiptUnavailable(:final String reason):
    showMessage(reason);

  case EcrReceiptFailed():
    // Safe to request again.
}

The returned URL can be rendered as a QR code.

An empty or missing receiptUrl is treated as:

EcrReceiptUnavailable

A receipt response without a usable URL is not considered a valid receipt.


Check Terminal Reachability #

Before starting a payment:

if (!await terminal.isReachable()) {
  showMessage('Terminal not reachable');
  return;
}

This is useful for detecting an incorrect IP address or unavailable terminal before asking the customer to present a card.

However, reachability does not prove that the terminal is idle.

A terminal currently processing another transaction can still answer a reachability probe.


Detailed Reachability Information #

Use:

final EcrReachability probe =
    await terminal.probeReachability();

if (!probe.reachable) {
  showMessage(
    '${probe.host}:${probe.port} — ${probe.error}',
  );
}

probeReachability() provides the underlying failure information when the terminal cannot be reached.


What Goes Over the Wire #

You normally do not need to construct or parse the protocol manually.

The SDK handles:

  • Request creation
  • Signing
  • Framing
  • Transport
  • Response parsing
  • Typed result conversion

This section is useful for troubleshooting and inspecting terminal logs.

Wi-Fi #

Wi-Fi uses:

TCP → Port 9100

Messages are framed using a:

2-byte big-endian length header

USB #

USB carries the same JSON payload and protocol bytes without an IP connection.


Sale Request Example #

Application call:

final result = await terminal.sale(
  EcrAmount.parse('0.216'),
  merchantReference: 'A1B2C3D4E5F6',
);

The request sent to the terminal is conceptually:

{
  "version": 1,
  "messageType": "SALE",
  "merchantReference": "A1B2C3D4E5F6",
  "terminalSerial": "P653200085189",
  "currencyCode": "512",
  "transactionDateTime": "20260809172826",
  "ecrId": "TILL-01",
  "amount": "000000000216",
  "nonce": "9F86D081884C7D659A2FEAA0C55AD015",
  "secureHash": "A3F1..."
}

Amount representation #

The wire protocol represents the amount as 12 zero-padded digits of minor units.

For OMR:

0.216 OMR

becomes:

000000000216

The SDK handles this conversion automatically.


Approved Sale Response #

{
  "responseCode": "00",
  "responseMessage": "Approved",
  "approved": true,
  "merchantReference": "A1B2C3D4E5F6",
  "terminalSerial": "P653200085189",
  "amount": "000000000216",
  "rrn": "622113155340",
  "authCode": "517842",
  "maskedPan": "543173xxxx5785",
  "ecrResponse": {
    "success": true,
    "responseCode": "00",
    "message": "Approved",
    "data": {
      "transactionId": "318873a0-93f6-11f1-94a1-29b67ff8c66c",
      "amount": "0.216",
      "systemTraceNr": "000215",
      "batchId": "00000003",
      "rrn": "622113155340",
      "authCode": "517842",
      "cardMask": "543173xxxx5785",
      "isPartialApprove": false
    },
    "errorList": []
  }
}

The Flutter application receives:

EcrApproved(
  amount: '0.216',
  rrn: '622113155340',
  authCode: '517842',
  maskedPan: '543173xxxx5785',
  partialApproval: false,
  merchantReference: 'A1B2C3D4E5F6',
)

Declined Sale Response #

{
  "responseCode": "909",
  "responseMessage": "Insufficient funds",
  "approved": false,
  "merchantReference": "A1B2C3D4E5F6",
  "amount": "000000000216",
  "ecrResponse": {
    "success": false,
    "responseCode": "909",
    "message": "A business exception occurred",
    "errorList": [
      "Insufficient funds"
    ]
  }
}

The SDK exposes the actionable reason:

Insufficient funds

rather than relying only on the generic backend message.


Inquiry Request #

final result = await terminal.inquire(
  receiptNumber: '208',
  transactionDate: '20260809',
);

The underlying request is:

{
  "version": 1,
  "messageType": "INQUIRY",
  "merchantReference": "D4E5F6A1B2C3",
  "terminalSerial": "P653200085189",
  "currencyCode": "512",
  "transactionDateTime": "20260809171259",
  "ecrId": "TILL-01",
  "stan": "000208",
  "originalTransactionDate": "20260809"
}

Inquiry Response — Transaction Found #

{
  "responseCode": "00",
  "responseMessage": "Transaction found",
  "approved": true,
  "merchantReference": "D4E5F6A1B2C3",
  "amount": "000000000258",
  "rrn": "7862802964726844904806",
  "maskedPan": "543173******5785",
  "ecrResponse": {
    "success": true,
    "responseCode": "00",
    "message": "Transaction found",
    "data": {
      "transactionId": "e970c800-93f1-11f1-9485-e7dd858253ff",
      "systemTraceNr": "000208",
      "transactionType": "Purchase",
      "status": "Approved",
      "amount": 0.258,
      "authorizeAmount": 0.258,
      "isPartialApprove": false,
      "currencyId": "512",
      "transactionTime": "20260809165816",
      "cardMask": "543173******5785",
      "rrn": "7862802964726844904806",
      "batchId": "00000003",
      "terminalId": 31629,
      "isRefunded": false,
      "canVoid": true,
      "canRefund": true
    }
  }
}

Important #

For an inquiry:

approved: true

means the inquiry found a transaction.

It does not by itself mean the transaction was approved.

Read:

transaction.status

to determine what happened.


Inquiry Response — Not Found #

{
  "responseCode": "25",
  "responseMessage": "No transactions found for the provided STAN and Terminal",
  "approved": false,
  "merchantReference": "D4E5F6A1B2C3",
  "ecrResponse": {
    "success": false,
    "responseCode": "25",
    "data": null,
    "errorList": [
      "No transactions found for the provided STAN and Terminal"
    ]
  }
}

This produces:

EcrInquiryNotFound

No transaction was recorded, so a new sale can be considered safe only after this inquiry result.


E-Receipt Response #

{
  "responseCode": "00",
  "responseMessage": "Receipt ready",
  "approved": true,
  "merchantReference": "E5F6A1B2C3D4",
  "ecrResponse": {
    "success": true,
    "responseCode": "00",
    "data": {
      "receiptUrl": "https://test.amwalpg.com:25446/Transaction/DownloadReceipt?transactionId=318873a0-93f6-11f1-94a1-29b67ff8c66c",
      "transactionId": "318873a0-93f6-11f1-94a1-29b67ff8c66c",
      "systemTraceNr": "000215"
    }
  }
}

The SDK converts this to:

EcrReceiptReady

with the receipt URL.


Standard Field Names #

The SDK uses a consistent vocabulary across transaction responses.

MeaningField
System trace numbersystemTraceNr
Masked card numbercardMask
CurrencycurrencyId
Retrieval reference numberrrn
Amwal transaction IDtransactionId
Merchant’s transaction referencemerchantReference
Request trace numberstan

Response Codes #

CodeMeaningMoney moved
00ApprovedYes
12Unsupported operationNo
13Invalid amountNo
17Cancelled at terminalNo
25Original transaction not foundNo
63Unsigned, wrong key, or clock too far outNo
91Terminal could not determine the outcomeUnknown
94Already answered — duplicate requestAsk/investigate
96Transaction already runningNo

Other codes such as 51, 909, and 05 may be passed through from the backend.

Recommendation: Always use the typed result (EcrApproved, EcrDeclined, or EcrFailed) instead of implementing business logic based only on response codes.


Common ECR Workflows #

1. Take a payment #

final EcrResult result = await terminal.sale(
  EcrAmount.parse('1.234'),
  merchantReference: order.number,
);

switch (result) {
  case EcrApproved(
      :final String amount,
      :final String rrn,
    ):
    completeSale(amount, rrn);

  case EcrDeclined(
      :final String reason,
    ):
    showMessage(reason);

  case EcrFailed():
    await reconcile(order.number);
}

2. Cancel an unsettled transaction #

final EcrResult result =
    await terminal.voidTransaction('215');

A void returns the full amount of the original transaction.


3. Refund a completed transaction #

final EcrResult result = await terminal.refund(
  EcrAmount.parse('0.216'),
  receiptNumber: '208',
  transactionDate: '20260809',
);

4. Find out what happened #

switch (await terminal.inquire(
  receiptNumber: '208',
  transactionDate: '20260809',
)) {
  case EcrInquiryFound(
      :final EcrTransaction transaction,
    ):
    showMessage(
      '${transaction.status}: ${transaction.amount}',
    );

    voidButton.enabled = transaction.canVoid;

    refundButton.enabled =
        transaction.canRefund &&
        !transaction.isRefunded;

  case EcrInquiryNotFound(
      :final String reason,
    ):
    showMessage(reason);

  case EcrInquiryFailed():
    // Safe to ask again.
}

The canVoid and canRefund values already reflect the backend’s transaction rules.


5. Show a receipt without a printer #

switch (await terminal.receipt(
  receiptNumber: '215',
  transactionDate: '20260809',
)) {
  case EcrReceiptReady(:final String url):
    showQrCode(url);

  case EcrReceiptUnavailable(
      :final String reason,
    ):
    showMessage(reason);

  case EcrReceiptFailed():
    // Safe to ask again.
}

Render the QR code locally as soon as the URL arrives.


Recommended Payment State Handling #

A payment integration should distinguish between these three states:

                 ┌──────────────┐
                 │ Sale Request │
                 └──────┬───────┘
                        │
             ┌──────────┼──────────┐
             ▼          ▼          ▼
         Approved    Declined    Failed
             │          │          │
             ▼          ▼          ▼
        Complete     Show error   Check
          sale                     outcome
                                   │
                          ┌────────┴────────┐
                          ▼                 ▼
                       Known            Unknown
                          │                 │
                          ▼                 ▼
                       Handle           Inquiry
                                            │
                                  ┌─────────┴─────────┐
                                  ▼                   ▼
                               Found              Not Found
                                  │                   │
                                  ▼                   ▼
                              Reconcile         Retry allowed

Important Cancellation Behaviour #

Cancelling a Flutter method call only stops the application from waiting.

It does not necessarily stop the transaction on the terminal.

For example:

Flutter app
   │
   │ SALE
   ▼
Terminal
   │
   │ Customer entering PIN
   │
   X ← App cancels waiting

The terminal may continue processing the payment.

Therefore:

A cancelled money-moving request is an unknown outcome, just like a timeout or connection loss.

The correct next operation is:

Inquiry

not another sale.

Read-only operations such as inquiry and receipt are safe to cancel and repeat.


Debugging and Logs #

Android #

Debug builds log ECR requests and responses.

Use:

adb logcat -s AmwalEcr

For a single debugging session:

adb shell setprop log.tag.AmwalEcr DEBUG

iOS #

The same information is available in the Xcode console with the prefix:

[AmwalEcr]

Release builds remain quiet.


Security Recommendations #

Follow these rules when integrating the SDK:

  • Never store the terminal signing key in source control.
  • Store the key using secure platform storage.
  • Never log the full signing key.
  • Never handle raw card numbers in the Flutter application.
  • Treat a masked PAN as sensitive payment information.
  • Do not retry an unknown payment.
  • Always use the merchant reference for transaction reconciliation.
  • Validate the terminal configuration before production deployment.
  • Use HTTPS when using the Web Service transport.

Production Integration Checklist #

Before going live, verify:

Flutter #

  • Dart SDK >= 3.5.0
  • Flutter >= 3.22.0
  • amwal_ecr installed
  • Android API 21+
  • Android JVM target 17
  • iOS 12+
  • iOS local-network permission configured

Terminal #

  • Correct terminal serial number
  • Correct ECR ID
  • Correct transport
  • Correct host/IP
  • Correct port
  • TMS terminalMode = 1
  • Correct currency
  • Correct minorUnitDigits

Security #

  • Terminal signing key obtained from Amwal
  • Key stored securely
  • Key not included in source control
  • Response authentication enabled

Payment handling #

  • EcrApproved handled
  • EcrDeclined handled
  • EcrFailed handled
  • outcomeIsUnknown checked
  • Unknown outcomes trigger inquiry
  • Sales are never automatically retried
  • Partial approvals handled correctly
  • Merchant references persisted
  • Inquiry results reconciled before retrying

#

Updated on September 9, 2026

What are your Feelings

  • Happy
  • Normal
  • Sad

Share This Article :

  • Facebook
  • X
  • LinkedIn
  • Pinterest
React Native ECR
Table of Contents
  • Overview
    • Supported operations
  • Critical Rule: Unknown Outcomes
  • Why Use the Amwal Flutter ECR SDK?
  • An outcome you cannot misread
  • No automatic payment retries
  • Automatic recovery of unknown outcomes
  • Partial approvals are approvals
  • Signed responses
  • One API for multiple transports
  • Decimal-safe amounts
  • Installation
    • Requirements
    • Add the package
      • Android
      • iOS
  • Platform Configuration
    • Android
    • iOS
    • Unsupported Platforms
    • Terminal Setup
      • Recommended approach
    • ECR Configuration
    • Currency and Amount Configuration
      • OMR example
    • Secure Hash Key
    • Supported Transports
    • EcrAmount
      • Parse an amount
      • Parse user input safely
      • Create from minor units
    • Supported Operations
    • Merchant Reference
    • Take a Payment
    • Void a Transaction
    • Refund a Transaction
    • Inquiry
    • Inquiry by Merchant Reference
    • Resolve an Unknown Payment
      • Recovery rules
    • Automatic Inquiry
    • Result Outcomes
    • EcrApproved
    • EcrDeclined
    • EcrFailed
      • Important
    • Partial Approval
    • E-Receipt
    • Check Terminal Reachability
    • Detailed Reachability Information
    • What Goes Over the Wire
    • Wi-Fi
    • USB
    • Sale Request Example
      • Amount representation
    • Approved Sale Response
    • Declined Sale Response
    • Inquiry Request
    • Inquiry Response — Transaction Found
      • Important
    • Inquiry Response — Not Found
    • E-Receipt Response
    • Standard Field Names
    • Response Codes
    • Common ECR Workflows
    • 1. Take a payment
    • 2. Cancel an unsettled transaction
    • 3. Refund a completed transaction
    • 4. Find out what happened
    • 5. Show a receipt without a printer
    • Recommended Payment State Handling
    • Important Cancellation Behaviour
    • Debugging and Logs
    • Android
    • iOS
    • Security Recommendations
    • Production Integration Checklist
      • Flutter
      • Terminal
      • Security
      • Payment handling

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