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

3
  • WooCommerce Subscription & Recurring Payments
  • 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

Laravel Package

2
  • Installation
  • Configuration
whmcs

WHMCS

2
  • WHMCS Overview
  • WHMCS Installation
smartbox

SMARTBOX

4
  • Express ApplePay Implementation
  • Offsite Implementation
  • Pre Requisites
  • Implementation
merchant cloud notification

Merchant Cloud Notification

1
  • Merchant Cloud Notification Integration Guide
Magento

Magento

2
  • Magento 2 Overview
  • Magento Installation
secure hash calculation

Secure Hash Calculation

1
  • Secure Hash Calculation
Opencart

Opencart

2
  • OpenCart Overview
  • Opencart Installtion
wp travel engine

Wp Travel Engine

1
  • Wp Travel Engine Installation
ZenCart

ZenCart

2
  • ZenCart Overview
  • Zencart Installation
webhook

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
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
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

Flutter SDK

2
  • Implementation
  • Flutter SDK Overview
andriod ecr

POS ECR

6
  • ECR SDK for .NET
  • Flutter ECR SDK
  • ECR SDK for iOS Cocoapods
  • ECR SDK for iOS SPM
  • Android ECR
  • React Native ECR
View Categories
  • Home
  • Amwal Pay Developer Portal
  • POS ECR
  • Flutter ECR SDK

Flutter ECR SDK

Amwal’s Flutter package drives a POS terminal from an Android or iOS till through one Dart API. It is a typed bridge over the native Amwal ECR SDKs — the Kotlin ecr-sdk on Android and AmwalECR on iOS — and implements no payment protocol of its own, so a Flutter till and a native till behave identically against the same terminal.

Sale, void, refund, inquiry and e-receipt, with the same types, the same units and the same outcomes on both platforms. No card data passes through your application: you receive a masked PAN at most.

A failed exchange is not a decline. A timeout or a lost connection can happen after the terminal accepted the request and completed the payment. When outcomeIsUnknown is true, do not send the sale again — ask the terminal what happened.


Why this SDK #

A payments SDK is judged on what it does when things go wrong, so that is where this one starts.

An outcome you cannot misread #

EcrApproved, EcrDeclined and EcrFailed are separate types, so “the terminal refused” and “we never found out” cannot be confused. That confusion is the bug that charges a customer twice, and the type system will not let you write it.

switch (result) {
  case EcrApproved(): // money moved
  case EcrDeclined(): // the terminal said no
  case EcrFailed():   // nobody knows yet — do not retry
}

It never retries a payment #

There is no retry setting, and no amount of configuration will add one. A request whose answer went missing may already have taken the money.

It recovers the answer for you #

When a reply goes missing, the package sends one inquiry using the reference the sale carried, and hands you what it found on EcrFailed.recovered. Most unknown outcomes are resolved before your code sees them.

A partial approval arrives as an approval #

When the bank authorises less than was asked for, you get the approval and both figures — so the goods are not released for money that never arrived.

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

The answer is signed, not just the request #

Anything on a shop network can reply on port 9100. Every answer is checked against this terminal’s key and against the request it claims to answer, so nothing can claim an approval for a payment that never happened. A reply that fails either check is EcrUnauthenticated — unknown, never an approval.

Four transports, one API #

Wi-Fi, USB cable, Web Service, and the payment app on the terminal itself. The terminal’s TMS profile decides; your code changes one enum and nothing else.

Amounts are decimals, never floats #

EcrAmount throughout. 0.1 + 0.2 is not 0.3 in binary floating point, and this is money.

No card data reaches your app #

A masked PAN at most, so integrating does not pull your application into PCI scope the way handling card numbers would.


Installation #

Add the package to pubspec.yaml:

dependencies:
  amwal_ecr: ^0.2.1

Or from the command line:

flutter pub add amwal_ecr

The native SDKs come with it. On Android it resolves com.amwal-pay:ecr-sdk from Maven Central; on iOS it resolves AmwalECR from CocoaPods trunk, or from Swift Package Manager when the project is built with flutter config --enable-swift-package-manager.

Requirements #

ComponentMinimum
Dart SDK>=3.5.0 — the API uses sealed classes and pattern matching
Flutter>=3.22.0
AndroidAPI 21 and above; JVM target 17
iOS12.0 and above

Android #

The plugin declares INTERNET itself. Nothing is required of the app.

iOS: local network permission #

Add to ios/Runner/Info.plist:

<key>NSLocalNetworkUsageDescription</key>
<string>Connects to the payment terminal to take card payments.</string>

iOS asks the user before an app may talk to devices on the local network, and a refusal is indistinguishable from a terminal that is switched off — every call answers EcrUnreachable. Treat a first-run EcrUnreachable on iOS as a permission question before a networking one.

Other platforms #

On macOS, Windows, Linux and Web every call answers EcrUnsupported rather than throwing MissingPluginException, so an app that runs somewhere unexpected degrades instead of crashing.


Terminal setup #

import 'package:amwal_ecr/amwal_ecr.dart';

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;

Prefer EcrSessions.open over building an EcrTerminal directly, so sale, inquiry and receipt all share one transport. An EcrTerminal holds no connection between calls, so it is cheap to build and safe to keep for the life of the app.

minorUnitDigits is the setting to get right: 3 for OMR, 2 for USD, 0 for JPY. Nothing cross-checks it against currencyCode, so a mismatch sends every amount off by a factor of ten and neither side objects.

The signing key is issued by Amwal per terminal. Keep it in the platform keystore (flutter_secure_storage), never in source control or an app bundle.


Common tasks #

Each task below is complete on its own.

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);   // never retry
}

The call takes as long as the cardholder takes. Ninety seconds is not unusual — show a spinner, not a countdown.

Cancel a transaction that has not been settled yet #

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

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

Refund one that has #

The cardholder presents their card to receive the money.

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

Whether a 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.

Find out what happened #

The one operation the terminal answers while it is busy, which is exactly when a till needs it.

switch (await terminal.inquire(
  receiptNumber: '208',
  transactionDate: '20260809',
)) {
  case EcrInquiryFound(:final EcrTransaction transaction):
    // Found is not paid — read the status.
    showMessage('${transaction.status}: ${transaction.amount}');
    voidButton.enabled = transaction.canVoid;
    refundButton.enabled = transaction.canRefund && !transaction.isRefunded;
  case EcrInquiryNotFound(:final String reason):
    showMessage(reason);
  case EcrInquiryFailed():
    // Harmless — an inquiry changes nothing. Try again.
}

canVoid and canRefund already have the void window and the backend’s rules applied, so a till can enable its buttons from them rather than guessing.

Hand the customer a receipt without a printer #

switch (await terminal.receipt(
  receiptNumber: '215',
  transactionDate: '20260809',
)) {
  case EcrReceiptReady(:final String url):
    showQrCode(url);   // the customer scans it and reads it on their phone
  case EcrReceiptUnavailable(:final String reason):
    showMessage(reason);
  case EcrReceiptFailed():
    // Safe to ask again.
}

Render the QR locally: it then appears the moment the URL arrives, and survives the network dropping afterwards — which is when a customer is standing at the counter waiting.

Check before you charge #

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

Turns a wrong address into an immediate answer instead of a wait the cardholder sits through. It proves the port is open, not that the terminal is free — a terminal already taking a payment answers a probe too.

Use probeReachability() when you want to show why:

final EcrReachability probe = await terminal.probeReachability();
if (!probe.reachable) {
  showMessage('${probe.host}:${probe.port} — ${probe.error}');
}

Transports #

EcrTransport mirrors the ecrMode in the terminal’s TMS profile.

EcrTransportecrModeAndroidiOSConnection
usbCable1YesNoUSB cable; carries no IP address
wifi2YesYesTCP to the terminal, port 9100
bluetooth3NoNoDefined in TMS, not served
webService4YesYesHTTPS to the Amwal Hub
appToApp5YesNoThe Amwal payment app on this same device

A terminal does not accept ECR requests until its TMS profile carries terminalMode 1. On a transport a platform cannot serve, every operation answers EcrUnsupported immediately without opening anything.

When your till runs on the terminal (app-to-app) #

appToApp is the one transport with no address, because there is nothing between the two ends: your app hands the transaction to the Amwal payment app installed beside it and waits for the answer.

final EcrTerminal terminal = EcrTerminal.appToApp(
  serialNumber: 'P653200085189',
  config: EcrConfig(ecrId: 'TILL7', currencyCode: '512', minorUnitDigits: 3),
);

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

Everything else on this page is unchanged — the same operations, outcomes and response codes. What you do differently, in order:

  1. Nothing in your manifest. The package declares the payment app in <queries>, and the manifest merge gives that to your app.
  2. Where you register a terminal, offer ecrMode 5 and hide the IP and port fields. There is nothing to put in their place — which application takes a payment is fixed. The serial number still has to be the terminal the payment app is provisioned as.
  3. Open it with EcrTerminal.appToApp(serialNumber: …), or pass EcrTransport.appToApp with EcrPaymentApp.packageName where an address would go.
  4. Call probeReachability() before you take an amount, if you like: it launches nothing and answers whether the payment app is there and will accept a request.
  5. Run the sale. The payment app comes to the front, takes the card, and steps back when the operator has finished with the receipt.

What is specific to this transport:

  • Always send a merchantReference, because either app can be destroyed mid-payment and it is the only handle left to ask what happened.
  • The merchant must be signed in on the payment app, and is told so plainly if not.
  • Fetching an e-receipt works but costs another handover, so print from the answer where you can.

Operations not sent over app-to-app #

signOn() and closeReceipt() answer EcrUnsupported here without launching anything, and neither refusal is about capability — the terminal answers both perfectly well over a socket.

  • A sign-on would spend a handover, opening the payment app the moment an operator picks a terminal, to learn what the next refusal carries anyway: a request the profile does not permit comes back with the current profile attached.
  • A close-receipt asks for something that has already happened. The answer to an app-to-app transaction is held until the operator closes the receipt — that is what the handover is for — so by the time your till has a result the terminal is back on its idle screen.

Both are one more EcrUnsupported outcome, which your till already handles, so there is nothing mode-specific to write.


Supported operations #

Amounts #

Amounts are EcrAmount, never double — a binary float cannot hold 1.234, and an amount off by a thousandth of a rial reconciles against nothing.

EcrAmount.parse('1.234');                            // from a text field
EcrAmount.tryParse(controller.text);                 // null while half-typed
EcrAmount.fromMinorUnits(1234, minorUnitDigits: 3);  // 1.234

Operations #

MethodWhat it does
isReachable()Whether the terminal is answering
probeReachability()The same probe, plus the underlying error when it fails
sale(amount, merchantReference: …)Takes a payment; the cardholder presents their card
voidTransaction(receiptNumber, …)Cancels an earlier transaction in full. No card, no amount
refund(amount, receiptNumber: …, transactionDate: …)Returns money against an earlier transaction
inquire(receiptNumber: …, transactionDate: …)What became of an earlier transaction. Reads only
inquireByReference(originalReference, …)The same question, by the reference you sent
signOn()What the terminal is and what it will accept. Reads only
closeReceipt()Tells the terminal the cashier has finished with the sale
receipt(receiptNumber: …, transactionDate: …)The e-receipt as a URL, to show as a QR code
final EcrResult result = await terminal.sale(
  EcrAmount.parse('1.234'),
  merchantReference: 'ORDER-1001',
);

final EcrResult refund = await terminal.refund(
  EcrAmount.parse('0.216'),
  receiptNumber: '208',
  transactionDate: '20260809',   // yyyyMMdd — a receipt number is only
);                               // unique within a terminal's day

inquire, inquireByReference and receipt authorise nothing, present no card and change nothing, so they are safe to repeat — and the terminal answers them even while it is taking a payment. That is what makes an unknown outcome recoverable rather than a guess.

Merchant reference #

Every money-moving call takes merchantReference: your own name for the transaction. Pass an order number and the same string identifies the sale in your books, in the terminal’s records and in any later lookup. Leave it out and one is generated; either way it comes back on EcrResult.merchantReference, and it is the only handle you hold before the terminal answers.

Cancelling #

Cancelling stops the app waiting; it does not reverse anything at the terminal. A cardholder mid-PIN carries on. A cancelled money-moving request is an unknown outcome, exactly like a timeout, and the next step is an inquiry rather than a retry. Cancelling a read-only operation is harmless.


Ask the terminal what it will accept #

The merchant’s TMS profile decides which operations a terminal serves and the amounts each one allows, and it can change at any moment — the terminal picks that up on its next heartbeat. A till that never asks keeps offering a button that will now be refused, and finds out in front of a cardholder.

switch (await terminal.signOn()) {
  case EcrSignOnAvailable(:final EcrTerminalCapabilities capabilities):
    final bool canRefund = capabilities.permits(EcrTransactionType.refund);
    final EcrPermittedTransaction? sale =
        capabilities.limitsFor(EcrTransactionType.sale);
    print('Sales from ${sale?.minAmount} to ${sale?.maxAmount}');

  case EcrSignOnUnavailable(:final String reason):
    // The terminal answered and cannot serve: not on its idle screen, or
    // nobody signed in on it. Show the reason — it is the difference between
    // "wait a moment" and "go and sign in on the terminal".
    print(reason);

  case EcrSignOnFailed(:final EcrFailure failure):
    // Nothing answered. Leave the till as it was and let the refusal teach it.
    print(failure.message);
}

EcrSignOnUnavailable is an answer, not a failure. “It is there and cannot take a payment” and “nothing answered” call for different things from a till, and reporting the first as the second sends somebody to look at a network that is working perfectly.

Sign-on reads only, so ask as often as you like — at start of day, and whenever you want to check your picture of the terminal is current. The answer is a snapshot and goes stale the moment TMS changes anything, which is safe: the terminal checks its own profile on every request, and a refusal carries the current profile with it. A till that never signs on twice still corrects itself.

An ecrMode this package has never heard of keeps its number on capabilities.ecrMode and leaves capabilities.transport null. An operation it cannot name is left out of permitted rather than guessed at — a till cannot offer a button it has no code for.


Put the receipt away when the cashier is done #

The terminal leaves its receipt on screen until somebody dismisses it, because normally the operator is standing there. When your till drove the transaction they are at the till instead, nobody dismisses it, and the terminal refuses the next transaction until somebody walks over and presses back.

// The moment the cashier closes your outcome dialog.
await terminal.closeReceipt();

Only you know when that moment is, which is why the terminal does not do it on a timer. An earlier build did, and took receipts off merchants who were still reading them.

Safe to repeat: an already-idle terminal answers EcrReceiptClosedIdle just the same, because the request asks for a state rather than an action. Nothing is lost either — receipt() fetches the e-receipt again and inquire() the transaction. A terminal too old to know the request answers EcrReceiptClosedRefused; treat that as “cannot be asked” and carry on.

Neither sign-on nor close-receipt reaches a Web Service terminal, and neither is sent over app-to-app — see Transports.


Result outcomes #

EcrResult ── EcrApproved   money moved
          ├─ EcrDeclined   the terminal answered and refused
          └─ EcrFailed     no trustworthy answer — see EcrFailure
switch (await terminal.sale(EcrAmount.parse('1.234'))) {
  case EcrApproved(:final String amount, :final String rrn, :final String authCode):
    completeSale(amount, rrn, authCode);
  case EcrDeclined(:final String reason):
    showMessage(reason);
  case EcrFailed(:final EcrFailure failure) when failure.outcomeIsUnknown:
    // Do not retry. Ask the terminal what happened.
    await reconcile();
  case EcrFailed(:final EcrFailure failure):
    showMessage(failure.message);
}

EcrApproved #

Carries amount, rrn, authCode, maskedPan, partialApproval and requestedAmount. A partial approval is an approval: the bank authorised less than was asked for, and the goods go out only once the difference is collected by other means.

EcrDeclined #

Carries responseCode and reason. Codes worth reading:

CodeHelperMeaning
96isTerminalBusyA transaction is already running. Nothing was attempted
17isCancelledAtTerminalThe operator or cardholder cancelled
25isOriginalNotFoundNo such receipt number for that day
63—Unsigned, signed with the wrong key, or the clock is too far out
91—outcomeIsUnknown is true — the one decline that is not a decision

EcrFailed #

Carries a typed failure and, when the package has already followed up, the inquiry it found:

EcrFailureOutcome known?Means
EcrUnreachableKnown — nothing happenedNothing is listening
EcrTimeoutUnknownAccepted, never answered
EcrMalformedUnknownAnswered, illegibly
EcrConnectionLostUnknownBroke part way through
EcrUnauthenticatedUnknownThe answer could not be shown to have come from the terminal
EcrCancelledUnknownYou gave up waiting
EcrUnsupportedKnown — nothing happenedThis platform cannot do it

Resolving an unknown outcome #

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

if (result.outcomeIsUnknown) {
  // Do not retry. Ask the terminal, quoting the reference you sent — a receipt
  // number arrives *in* the answer, which is what went missing.
  switch (await terminal.inquireByReference(order.number)) {
    case EcrInquiryFound(:final EcrTransaction transaction):
      reconcile(transaction);
    case EcrInquiryNotFound():
      // Nothing was taken. Only now is it safe to send the sale again.
    case EcrInquiryFailed():
      // Still unknown. Ask again later; do not retry the sale.
  }
  return;
}

Most of the time the package has already asked for you. With autoInquireOnFailure (on by default), a money-moving request whose answer never arrived is followed by one inquiry, and what it found is on EcrFailed.recovered. When settled is set, the delivery failed but the outcome is known, and outcomeIsUnknown is false.

Nothing in this package retries. Not on a timeout, not on a lost connection, not on a host error. There is no setting that turns retrying on, because there is no failure worth charging a customer twice for.


What goes over the wire #

You do not need this to use the package — it signs, frames and parses for you. It is here for the afternoon you are looking at a log and want to know whether the terminal was ever asked.

Wi-Fi and USB cable carry the same JSON. On Wi-Fi it is TCP to port 9100, framed with a 2-byte big-endian length header; on the cable the same bytes travel with no IP involved.

A sale #

terminal.sale(EcrAmount.parse('0.216'), merchantReference: 'A1B2C3D4E5F6')

Request #

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

amount is 12 zero-padded digits of minor units — 0.216 OMR at three decimal places. nonce and secureHash are added when a signing key is configured.

Response — approved #

{
  "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": []
  }
}

That arrives as:

EcrApproved(
  amount: '0.216',          // major units — converted for you
  rrn: '622113155340',
  authCode: '517842',
  maskedPan: '543173xxxx5785',
  partialApproval: false,
  merchantReference: 'A1B2C3D4E5F6',
)

Response — declined #

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

reason on EcrDeclined is “Insufficient funds” — the package prefers the backend’s errorList over its message, because the message is often a generic string while the list is the sentence a cashier can act on.

Response — partial approval #

{
  "responseCode": "00",
  "responseMessage": "Approved",
  "approved": true,
  "amount": "000000000500",
  "partialApproval": true,
  "requestedAmount": "000000002000",
  "rrn": "622113155340",
  "authCode": "517842"
}

2.000 was asked for and 0.500 was taken. Note the response code is 00 — this is why the package reads approved and never decides from the code alone.

An inquiry #

The one to know, because it is how an unknown outcome gets resolved.

terminal.inquire(receiptNumber: '208', transactionDate: '20260809')

Request #

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

Response — 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
    }
  }
}

approved: true here means found, not paid. What became of the transaction is status. An inquiry that finds a declined sale is a successful inquiry — which is why the package returns EcrInquiryFound and puts the outcome on transaction.status rather than collapsing the two.

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"]
  }
}

EcrInquiryNotFound — nothing was recorded, so nothing happened. This is the only answer on which retrying a sale is safe.

A receipt #

terminal.receipt(receiptNumber: '215', transactionDate: '20260809')

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"
    }
  }
}

An empty or absent receiptUrl is EcrReceiptUnavailable, whatever the response code claims. A receipt with no URL is not a receipt.

A sign-on #

terminal.signOn()

Response #

{
  "success": true,
  "responseCode": "00",
  "data": {
    "available": true,
    "ecrMode": 2,
    "ecrModeName": "WIFI",
    "terminalName": "Counter 3",
    "currencyCode": "512",
    "minorUnitDigits": 3,
    "eReceipt": true,
    "physicalReceipt": false,
    "permittedTransactions": [
      { "messageType": "SALE", "minAmount": "0.100", "maxAmount": "5.000" },
      { "messageType": "INQUIRY" }
    ]
  }
}

available: false still arrives as a successful answer, carrying a reason — that is EcrSignOnUnavailable, not EcrSignOnFailed. Amount limits are major units, as every answer reports an amount, and an operation with no limits carries neither field.

A close-receipt #

terminal.closeReceipt()

Response #

{
  "success": true,
  "responseCode": "00",
  "message": "The terminal is on its idle screen"
}

The same answer whether a receipt was on screen or not: the request asks for a state, and that state now holds either way. A terminal too old to know the request answers 12.

Field names in an answer #

Every answer spells the same thing the same way, so a till learns one vocabulary:

MeaningName
System trace numbersystemTraceNr
Masked card numbercardMask
CurrencycurrencyId
Retrieval reference numberrrn
Amwal’s transaction idtransactionId
The reference you sentmerchantReference

The request side keeps stan for the trace number, and always has.


Response codes #

CodeMeaningMoney moved
00ApprovedYes
12Unsupported operationNo
13The amount is not validNo
17Cancelled at the terminalNo
25The original transaction was not foundNo
63Unsigned, signed with the wrong key, or the clock is too far outNo
91The terminal could not say what happenedUnknown
94Already answered — this request was sent beforeAsk
96A transaction is already running on this terminalNo

Anything else is the backend’s own code passed through — 51, 909, 05. Read the typed outcome rather than matching on codes.


Seeing what is actually sent #

A debug build logs every call and every answer. Nothing needs turning on:

adb logcat -s AmwalEcr

On iOS the same lines go to the Xcode console, prefixed [AmwalEcr]. Release builds stay quiet; to make one talk for a session:

adb shell setprop log.tag.AmwalEcr DEBUG
Updated on October 15, 2026

What are your Feelings

  • Happy
  • Normal
  • Sad

Share This Article :

  • Facebook
  • X
  • LinkedIn
  • Pinterest
ECR SDK for .NET
Table of Contents
  • Why this SDK
    • An outcome you cannot misread
    • It never retries a payment
    • It recovers the answer for you
    • A partial approval arrives as an approval
    • The answer is signed, not just the request
    • Four transports, one API
    • Amounts are decimals, never floats
    • No card data reaches your app
  • Installation
  • Requirements
    • Android
    • iOS: local network permission
    • Other platforms
  • Terminal setup
  • Common tasks
    • Take a payment
    • Cancel a transaction that has not been settled yet
    • Refund one that has
    • Find out what happened
    • Hand the customer a receipt without a printer
    • Check before you charge
  • Transports
    • When your till runs on the terminal (app-to-app)
      • Operations not sent over app-to-app
  • Supported operations
    • Amounts
    • Operations
    • Merchant reference
    • Cancelling
  • Ask the terminal what it will accept
  • Put the receipt away when the cashier is done
  • Result outcomes
    • EcrApproved
    • EcrDeclined
    • EcrFailed
  • Resolving an unknown outcome
  • What goes over the wire
    • A sale
      • Request
      • Response — approved
      • Response — declined
      • Response — partial approval
    • An inquiry
      • Request
      • Response — found
      • Response — not found
    • A receipt
      • Response
    • A sign-on
      • Response
    • A close-receipt
      • Response
    • Field names in an answer
  • Response codes
  • Seeing what is actually sent

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