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
  • ECR SDK for iOS SPM

ECR SDK for iOS SPM

Drive an Amwal POS terminal from your own iOS application over the local network.

Your application asks for a payment; the terminal reads the card, talks to the payment backend, and answers. No card data passes through your application — you receive a masked PAN at most, so integrating does not pull your app into PCI scope the way handling card numbers would.

import AmwalECR

let terminal = EcrTerminal(host: "192.168.1.50", serialNumber: "P2M12345678")

switch terminal.sale(amount: Decimal(string: "1.234")!) {
case let .approved(sale):   receipt.print(rrn: sale.rrn, auth: sale.authCode)
case let .declined(refusal): screen.show(refusal.reason)
case let .failed(_, failure): screen.show(failure.message)   // outcome unknown
}

This is the iOS counterpart of the Kotlin SDK, method for method and outcome for outcome. Both are used, unchanged, by the Flutter plugin amwal_ecr.


Installing #

Swift Package Manager #

In Xcode: File ▸ Add Package Dependencies…, then https://github.com/amwal-pay/AmwalECR-iOS-SPM.git, Up to Next Minor Version from 0.2.0.

Or in a Package.swift:

dependencies: [
    .package(url: "https://github.com/amwal-pay/AmwalECR-iOS-SPM.git", .upToNextMinor(from: "0.2.0")),
],
targets: [
    .target(name: "Till", dependencies: [.product(name: "AmwalECR", package: "AmwalECR-iOS-SPM")]),
]

The module is AmwalECR, and it brings nothing else with it: Foundation and BSD
sockets, no third-party dependency.

iOS 12.0+, macOS 12.0+, Swift 5.5+.

Using CocoaPods instead? The same sources are published as the AmwalECR pod from AmwalECR iOS-CocoaPods. Depend on one or the other, never both in the same target — two copies of the module will not link.

Local network permission #

iOS asks the user before an app may talk to devices on the local network. Add this to Info.plist or the first sale fails with unreachable and no explanation:

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

The one rule #

A failure is not a decline. Four of the six things that can happen to a request leave the outcome unknown: the terminal may have taken the money and the answer may simply not have arrived.

OutcomeThe moneyWhat a till does
.approvedtakenBook it.
.declinednot takenTell the customer, offer another card.
.failed(_, .unreachable, _)not taken — nothing was sentSafe to send again.
.failed(_, .timeout, _)unknownInquire by reference. Never resend.
.failed(_, .connectionLost, _)unknownInquire by reference. Never resend.
.failed(_, .malformed, _)unknownInquire by reference. Never resend.
.failed(_, .unauthenticated, _)unknownInquire by reference. Never resend.

failure.outcomeUnknown is that column. A response code of 91 comes back as a decline, but it is the terminal saying it does not know either — read nextStep and inquire.

Nothing in this SDK retries a money-moving request, at any level, for any failure. That is deliberate, and a caller should not add one: the second request is a second sale, and the customer is charged twice.

What the SDK does do is ask. A lost answer is followed by one inquiry — an inquiry reads and nothing more — and what it found is attached to the result:

let result = try terminal.sale(amount: total, merchantReferenceId: order.number)

switch result {
case let .failed(reference, failure, recovered):
    if case let .found(_, transaction, _) = recovered {
        book(transaction)              // the answer was lost; the outcome is not
    } else if failure.outcomeUnknown {
        // Still unknown. Ask again later, quoting `reference`. Never resend.
        _ = try? terminal.inquireByReference(reference)
    }
case .approved, .declined:
    break
}

result.settled is the short form of that first branch. Turn the follow-up off with EcrConfig.autoInquireOnFailure if the till runs its own reconciliation.


Operations #

MethodNeeds
ReachabilityisReachable()—
Salesale(amount:merchantReferenceId:)amount
Voidvoid(receiptNumber:originalTerminalId:merchantReferenceId:)the original’s receipt number
Refundrefund(amount:receiptNumber:transactionDate:originalTerminalId:merchantReferenceId:)amount, receipt number, date
Inquiryinquire(receiptNumber:transactionDate:originalTerminalId:merchantReferenceId:)receipt number, date
Inquiry by referenceinquireByReference(_:transactionDate:originalTerminalId:merchantReferenceId:)the original’s reference
E-receiptreceipt(receiptNumber:transactionDate:originalTerminalId:merchantReferenceId:)receipt number, date

Both inquiries read and change nothing, so they are safe to repeat, and the terminal answers them even while it is taking a payment — which is exactly when a till needs them.

merchantReferenceId is optional everywhere and is the till’s own name for the transaction: an order number, a basket id, whatever already names it in the caller’s system. Left out, the SDK generates one. Either way it comes back on the outcome, and it is the only identifier a till holds before the terminal
answers — which is what makes inquireByReference the lookup that still works when nothing else does.

The money-moving calls throw only for arguments that cannot be used: a reference over 32 characters or carrying a space, & or =; a secret that is not hex. Nothing is sent in that case. Everything that happens on the wire is an EcrResult, never an exception.


Signing the link #

A terminal refuses what it cannot verify, so in practice a till needs the secret Amwal issues for it. The app owns persistence (Keychain / settings) and passes one value on EcrConfig.secureHashKey for the selected mode — LAN (Wi‑Fi / USB cable) and Web Service use different secrets, but the SDK only
consumes the field you assign:

// App-owned: load the secret for this terminal mode (never hardcode in source)
let secret = settings.secureHashKey(for: selectedMode)

var config = EcrConfig()
config.secureHashKey = secret

let plan = EcrSessions.plan(
    link: .lan(host: host, port: config.port),
    config: config
)
guard plan.isReady else {
    screen.show(plan.issues.joined(separator: "\n"))
    return
}
let session = EcrSessions.open(terminalSerial: serial, plan: plan)
_ = try session.sale(amount: amount)

Prefer EcrSessions.open so sale, inquiry, and recovery share one transport dispatch (LAN / USB cable / Web Service). You can still construct EcrTerminal or EcrWebServiceTerminal directly when you already know the link.

Web Service Hub bases: SIT https://test.amwalpg.com:25452, UAT https://test.amwalpg.com:15452, PROD https://pos.amwalpg.com.

Every request is then signed — HMAC-SHA256 over the sorted top-level fields, with a per-message nonce — and every answer is checked, both that it carries this till’s signature and that it echoes this request’s nonce. An answer failing either check is .unauthenticated: something else may have replied on the
terminal’s port, so the answer is discarded rather than believed. It is not a decline, and the transaction may well have completed.

EcrConfig.secureHashKeyError says whether a key is usable before you send anything; a key that is not throws EcrInvalidArgument at the first call rather than being sent unsigned.

Every call blocks while the terminal works, which for a sale is as long as the cardholder takes. Run them off the main thread and call cancel() from another thread to stop waiting:

DispatchQueue.global(qos: .userInitiated).async {
    let result = terminal.sale(amount: total)
    DispatchQueue.main.async { screen.show(result) }
}

// The operator gave up. This stops the wait — it does not stop the terminal,
// and the outcome is unknown.
terminal.cancel()

Configuration #

let terminal = EcrTerminal(
    host: "192.168.1.50",
    serialNumber: "P2M12345678",
    config: EcrConfig(
        ecrId: "TILL-01",         // how this till names itself
        currencyCode: "512",      // OMR
        minorUnitDigits: 3,       // baisa
        port: 9100,
        connectTimeout: 10,       // seconds
        responseTimeout: 120,     // the cardholder's time, not the network's
        probeTimeout: 3           // isReachable only
    )
)

responseTimeout is 120 seconds because a sale waits for a human being to present a card and key a PIN. Shortening it does not make the terminal faster; it makes a completed sale time out and land in the unknown-outcome path.


Amounts #

Amounts are Decimal, never Double. A binary float cannot hold 1.234, and an amount that is off by a thousandth is a wrong charge.

guard let amount = EcrDecimal.parse(field.text ?? "") else { return }   // no locale surprises
terminal.sale(amount: amount)

The conversion to the wire’s minor units happens once, inside the SDK, half-up — matching the Kotlin SDK to the last minor unit, so an Android till and an iOS till cannot disagree about a rounding boundary.

Amounts come back as strings in major units ("1.234"), exactly as reported.


Building and testing #

swift test            # 37 tests, no simulator needed

Unit tests share signing placeholders via EcrTestConfigs (aligned with ecr_sdk): SECURE_HASH_KEY_ECR_WIFI, SECURE_HASH_KEY_ECR_WIFI_OTHER, and SECURE_HASH_KEY_WEBSERVICE, exposed as lan / lanOther / webService configs. Never commit real Amwal keys.

The suite is not incidental to the platform story: EcrDecimalTests, EcrMessageTests and EcrResponseReaderTests assert this SDK against the Kotlin SDK’s own test payloads and rounding boundaries. That is what keeps “identical on both platforms” a checkable claim.

Continuous integration #

codemagic.yaml runs on Codemagic. Every push and pull request runs swift test and compiles the package for an iOS device and the simulator. A vX.Y.Z tag runs the same, checks the tag against the top
CHANGELOG.md entry, and then resolves that exact version from GitHub into a throwaway consumer package — the tag is the SwiftPM release, so that is the release being read back. Nothing is uploaded and no credentials are needed.

Updated on September 10, 2026

What are your Feelings

  • Happy
  • Normal
  • Sad

Share This Article :

  • Facebook
  • X
  • LinkedIn
  • Pinterest
Android ECR
Table of Contents
  • Installing
    • Swift Package Manager
    • Local network permission
  • The one rule
  • Operations
  • Signing the link
  • Configuration
  • Amounts
  • Building and testing
    • Continuous integration

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