PaymentsIn-Person PaymentsMobile solutions

Mobile solutions

Pair the Ciontek CM30 or use Tap to Pay to take payments on iOS and Android.

View as MarkdownInstall skills

The Ciontek CM30 Bluetooth reader paired with the VINR iOS or Android SDK turns any smartphone into a full card-present terminal — supporting contactless (NFC), chip+PIN, and magnetic stripe. Combined with Tap to Pay for zero-hardware scenarios, the mobile stack covers pop-up stalls, event ticketing, field sales, and queue-busting in any environment where a fixed countertop terminal is impractical.

Platform requirementsAsk

RequirementMinimum
iOS versioniOS 15.0
DeviceiPhone 7 or later, iPad (6th gen) or later
Xcode14.0 or later
Swift5.7 or later
BluetoothRequired for CM30 pairing
NFCRequired only for Tap to Pay on iPhone (iPhone XS or later, iOS 16+)
App Store entitlementStandard distribution — no special entitlement needed for CM30. Tap to Pay requires the Apple Tap to Pay entitlement separately.

The VinrSDK package is distributed via Swift Package Manager. No CocoaPods or Carthage support is provided.

RequirementMinimum
Android versionAndroid 7.0 (API level 24)
Target SDKAPI level 34 (required by Google Play as of August 2024)
BluetoothBluetooth 4.2+ required; Bluetooth 5.0 recommended for CM30
NFCRequired only for Tap to Pay on Android
Architecturearm64-v8a, armeabi-v7a, x86_64
Kotlin1.8.0 or later (Java interop supported)

The com.vinr:sdk package is hosted on Maven Central. No custom repository configuration is needed.

CM30 Bluetooth readerAsk

The Ciontek CM30 is a compact Android mPOS device that connects to a merchant's phone over Bluetooth or WiFi. It is battery-powered, pocket-sized, and PCI PTS 5.x certified. Because it has no built-in printer, receipts are delivered digitally — see Receipts on mobile below.

Prop

Type

Enable Bluetooth on the host device

On iOS, open Settings → Bluetooth and ensure Bluetooth is on. On Android, pull down the notification shade and toggle Bluetooth on.

Power on the CM30

Press and hold the power button for two seconds until the LED indicator turns solid blue. The device broadcasts as VINR-CM30-XXXX where XXXX is the last four characters of the serial number.

Pair via the VINR SDK

Do not pair through the OS Bluetooth settings. Instead, call VINRTerminal.discoverReaders (iOS) or VinrTerminal.discoverReaders (Android) — the SDK handles the secure pairing handshake and key negotiation automatically.

Confirm connection

The LED turns solid green and the SDK fires the readerConnected event with the reader object. The device is now ready to accept payment sessions.

Keep the CM30 within 10 metres of the host device during transactions. Walls and metal fixtures reduce effective range. For fixed-counter use cases where greater reliability matters, consider the Nexgo CT20 or CT20P.

VINR iOS SDKAsk

Add the SDK via Swift Package Manager. In Xcode, go to File → Add Package Dependencies and enter:

https://github.com/vinr/vinr-ios-sdk

Select the VinrSDK library and add it to your app target.

import VinrSDK

let terminal = VINRTerminal(secretKey: ProcessInfo.processInfo.environment["VINR_SECRET_KEY"]!)

terminal.discoverReaders(connectionConfig: .bluetooth) { result in
    switch result {
    case .success(let readers):
        guard let cm30 = readers.first(where: { $0.model == .cm30 }) else { return }

        terminal.connectReader(cm30) { connectResult in
            switch connectResult {
            case .success:
                let params = VINRPaymentParams(
                    amount: 2500,
                    currency: "USD",
                    reference: "order_8821"
                )

                terminal.createPayment(params: params) { createResult in
                    switch createResult {
                    case .success(let intent):
                        terminal.collectPayment(intent: intent) { collectResult in
                            switch collectResult {
                            case .success(let confirmedIntent):
                                print("Payment succeeded:", confirmedIntent.id)
                            case .failure(let error):
                                print("Collection failed:", error.localizedDescription)
                            }
                        }
                    case .failure(let error):
                        print("Create failed:", error.localizedDescription)
                    }
                }

            case .failure(let error):
                print("Connect failed:", error.localizedDescription)
            }
        }

    case .failure(let error):
        print("Discovery failed:", error.localizedDescription)
    }
}

For scenarios where no hardware is available, the SDK also supports Tap to Pay on iPhone, which uses the device's NFC antenna directly without a companion reader.

VINR Android SDKAsk

Add the SDK to your module-level build.gradle:

dependencies {
    implementation("com.vinr:sdk:latest.release")
}

Declare the required permissions in AndroidManifest.xml:

<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
import com.vinr.sdk.Vinr
import com.vinr.sdk.terminal.VinrTerminal
import com.vinr.sdk.terminal.ConnectionConfig
import com.vinr.sdk.terminal.PaymentParams

val vinr = Vinr(secretKey = System.getenv("VINR_SECRET_KEY"))
val terminal = VinrTerminal(vinr)

terminal.discoverReaders(ConnectionConfig.Bluetooth) { result ->
    result.onSuccess { readers ->
        val cm30 = readers.firstOrNull { it.model == ReaderModel.CM30 } ?: return@onSuccess

        terminal.connectReader(cm30) { connectResult ->
            connectResult.onSuccess {
                val params = PaymentParams(
                    amount = 2500L,
                    currency = "USD",
                    reference = "order_8821"
                )

                terminal.createPayment(params) { createResult ->
                    createResult.onSuccess { intent ->
                        terminal.collectPayment(intent) { collectResult ->
                            collectResult.onSuccess { confirmedIntent ->
                                println("Payment succeeded: ${confirmedIntent.id}")
                            }.onFailure { error ->
                                println("Collection failed: ${error.message}")
                            }
                        }
                    }.onFailure { error ->
                        println("Create failed: ${error.message}")
                    }
                }
            }.onFailure { error ->
                println("Connect failed: ${error.message}")
            }
        }
    }.onFailure { error ->
        println("Discovery failed: ${error.message}")
    }
}

Handle connection statesAsk

The SDK emits connection lifecycle events that your app should handle to give operators clear feedback and to recover from transient drops automatically.

terminal.delegate = self

extension YourClass: VINRTerminalDelegate {
    func terminal(_ terminal: VINRTerminal, didUpdateReaderConnectionStatus status: VINRReaderConnectionStatus) {
        switch status {
        case .connected(let reader):
            print("Reader connected — battery:", reader.batteryLevel ?? "unknown")
        case .disconnected(let reader, let reason):
            print("Reader disconnected:", reason)
            terminal.reconnectReader(reader, retryPolicy: .exponentialBackoff(maxAttempts: 5)) { _ in }
        case .batteryLow(let reader):
            showOperatorAlert("CM30 battery is low. Please charge the reader soon.")
        }
    }
}
terminal.setConnectionListener(object : VinrConnectionListener {
    override fun onReaderConnected(reader: Reader) {
        println("Reader connected — battery: ${reader.batteryLevel ?: "unknown"}")
    }

    override fun onReaderDisconnected(reader: Reader, reason: DisconnectReason) {
        println("Reader disconnected: $reason")
        terminal.reconnectReader(reader, RetryPolicy.ExponentialBackoff(maxAttempts = 5)) {}
    }

    override fun onBatteryLow(reader: Reader) {
        showOperatorAlert("CM30 battery is low. Please charge the reader soon.")
    }
})

Do not suppress disconnected events. A missed disconnect can leave a payment session in a hung state. Always trigger a reconnect attempt or surface the status to the operator so they can intervene.

Receipts on mobileAsk

The CM30 has no built-in printer. VINR automatically offers digital receipt delivery at the end of every transaction:

  • Email — the customer enters their email address on the host device screen; VINR sends a branded receipt immediately.
  • SMS — the customer enters a mobile number; receipt delivery is near-instant in supported regions.
  • QR code — the terminal displays a QR code linking to a hosted receipt page; no contact details required.

All three methods are enabled by default. You can configure which options appear — and customise receipt branding — in the Dashboard under Settings → Receipts, or via the receipts and engagement configuration API.

Receipt delivery preferences set at the reader level persist across sessions. If a returning customer previously chose email, the SDK pre-populates the field on subsequent transactions at the same terminal.

Offline queueAsk

The CM30 can queue up to 10 transactions locally when the host device loses internet connectivity. Queued transactions are processed in order as soon as connectivity is restored; the SDK fires a queueFlushed event with an array of confirmed payment IDs.

Prop

Type

Offline transactions carry elevated fraud risk because authorisation is deferred. VINR applies velocity checks and amount caps, but you assume liability for declined or disputed offline payments that exceeded your configured limits. Review your offline settings with your account manager before enabling high-value offline acceptance.

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page