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 #
| Operation | Description |
|---|---|
| Sale | Process a card payment |
| Void | Cancel an earlier transaction in full |
| Refund | Return money against an earlier transaction |
| Inquiry | Check the status of an earlier transaction |
| Inquiry by Reference | Find a transaction using your merchant reference |
| E-Receipt | Retrieve a receipt URL for QR-code display |
| Reachability | Check 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 #
| Requirement | Version |
|---|---|
| Dart SDK | >= 3.5.0 |
| Flutter | >= 3.22.0 |
| Android | API 21+ |
| Android JVM | Target 17 |
| iOS | 12.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 #
| Property | Description |
|---|---|
host | Terminal IP address or service host |
serialNumber | Amwal terminal serial number |
transport | Communication transport |
ecrId | Unique identifier for the till/ECR |
currencyCode | Currency code configured for the terminal |
minorUnitDigits | Number of decimal places used by the currency |
port | ECR TCP port, normally 9100 for Wi-Fi |
connectTimeout | Connection timeout |
responseTimeout | Maximum time to wait for a transaction response |
probeTimeout | Reachability probe timeout |
secureHashKey | Terminal signing key |
autoInquireOnFailure | Automatically inquire after an unknown money-moving result |
Currency and Amount Configuration #
minorUnitDigits is critical.
| Currency | Code | Minor-unit digits | Example |
|---|---|---|---|
| OMR | 512 | 3 | 1.234 |
| USD | 840 | 2 | 12.34 |
| JPY | 392 | 0 | 1000 |
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.
| Transport | ECR Mode | Android | iOS | Description |
|---|---|---|---|---|
usbCable | 1 | Yes | No | USB cable; no IP address |
wifi | 2 | Yes | Yes | TCP connection to terminal |
bluetooth | 3 | No | No | Defined in TMS but not served |
webService | 4 | Yes | Yes | HTTPS 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 #
| Method | Purpose |
|---|---|
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 result | Meaning | Retry sale? |
|---|---|---|
EcrInquiryFound | Transaction exists | No |
EcrInquiryNotFound | Nothing was recorded | Yes, if appropriate |
EcrInquiryFailed | Still unknown | No |
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
| Result | Meaning |
|---|---|
EcrApproved | Terminal approved the transaction |
EcrDeclined | Terminal answered and explicitly refused it |
EcrFailed | No trustworthy answer was available |
EcrApproved #
An approved result contains:
amountrrnauthCodemaskedPanpartialApprovalrequestedAmountmerchantReference
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:
| Code | Meaning | Money moved |
|---|---|---|
12 | Unsupported operation | No |
13 | Invalid amount | No |
17 | Cancelled at terminal | No |
25 | Original transaction not found | No |
63 | Invalid/missing signature or clock issue | No |
96 | Another transaction is already running | No |
| Other | Backend-specific response | Depends |
Do not determine the final outcome from the response code alone. Use the typed result.
EcrFailed #
EcrFailed represents a communication or processing failure.
| Failure | Outcome known? | Meaning |
|---|---|---|
EcrUnreachable | Yes | Nothing is listening |
EcrTimeout | Unknown | Request may have been accepted but no response arrived |
EcrMalformed | Unknown | Terminal answered but response was unreadable |
EcrConnectionLost | Unknown | Connection broke during communication |
EcrUnauthenticated | Unknown | Response could not be verified |
EcrCancelled | Unknown | Application stopped waiting |
EcrUnsupported | Yes | Platform 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.
| Meaning | Field |
|---|---|
| System trace number | systemTraceNr |
| Masked card number | cardMask |
| Currency | currencyId |
| Retrieval reference number | rrn |
| Amwal transaction ID | transactionId |
| Merchant’s transaction reference | merchantReference |
| Request trace number | stan |
Response Codes #
| Code | Meaning | Money moved |
|---|---|---|
00 | Approved | Yes |
12 | Unsupported operation | No |
13 | Invalid amount | No |
17 | Cancelled at terminal | No |
25 | Original transaction not found | No |
63 | Unsigned, wrong key, or clock too far out | No |
91 | Terminal could not determine the outcome | Unknown |
94 | Already answered — duplicate request | Ask/investigate |
96 | Transaction already running | No |
Other codes such as 51, 909, and 05 may be passed through from the backend.
Recommendation: Always use the typed result (
EcrApproved,EcrDeclined, orEcrFailed) 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_ecrinstalled- 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 #
EcrApprovedhandledEcrDeclinedhandledEcrFailedhandledoutcomeIsUnknownchecked- Unknown outcomes trigger inquiry
- Sales are never automatically retried
- Partial approvals handled correctly
- Merchant references persisted
- Inquiry results reconciled before retrying
