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 Cocoapods

ECR SDK for iOS Cocoapods

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 #

CocoaPods #

In your Podfile:

pod 'AmwalECR', '~> 0.2.0'

then pod install.

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 Swift Package Manager instead? The same sources are published as a Swift package from AmwalECR-iOS-SPM. 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 #

pod lib lint AmwalECR.podspec --allow-warnings

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 pod lib lint (iOS and macOS, with the test spec) and checks the podspec version against the top CHANGELOG.md entry. A vX.Y.Z tag checks the tag against the podspec, pushes to CocoaPods trunk — skipping, not re-pushing, a
version that is already there — and then reads the version back from the trunk API. The trunk token lives in a Codemagic environment group named cocoapods_credentials, as COCOAPODS_TRUNK_TOKEN; see the release policy in amwal-ecr-flutter.

Updated on September 10, 2026

What are your Feelings

  • Happy
  • Normal
  • Sad

Share This Article :

  • Facebook
  • X
  • LinkedIn
  • Pinterest
Table of Contents
  • Installing
    • CocoaPods
    • 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