PaymentsIn-Person PaymentsTap to Pay

Tap to Pay

Accept contactless payments directly on an iPhone or Android device — no hardware terminal required.

View as MarkdownInstall skills

Tap to Pay turns a merchant's iPhone or Android device into a contactless payment terminal. Customers tap a card, Apple Pay, or Google Pay wallet directly on the device screen — no companion hardware, no card reader dongle, and no additional accessories required. VINR implements this through Apple's Tap to Pay on iPhone API and the Android NFC payment APIs, so the cryptographic card data never leaves the platform's secure enclave.

RequirementsAsk

  • iPhone XS or later
  • iOS 16.0 or later
  • VINR iOS SDK (VinrSDK via Swift Package Manager)
  • An Apple developer account with the Tap to Pay on iPhone entitlement granted by Apple
  • Android device with hardware NFC (check PackageManager.FEATURE_NFC)
  • Android 9 (API level 28) or later
  • VINR Android SDK (com.vinr:sdk) added as a Gradle dependency
  • android.permission.NFC declared in your manifest

Tap to Pay on iPhone is available in the United States, United Kingdom, Australia, Canada, and a growing list of European markets. Android NFC acceptance is available wherever VINR processes card-present transactions. Contact VINR support to confirm availability in your region before shipping.

Accepted payment typesAsk

Both iOS and Android accept the following contactless payment types:

TypeExamples
Contactless cardsVisa, Mastercard, Amex (physical or virtual, NFC-enabled)
Apple PayiPhone, Apple Watch, iPad
Google PayAndroid devices with NFC

Chip+PIN and magnetic stripe are supported only on dedicated VINR hardware terminals. See Mobile solutions for an overview of hardware options.

iOS integrationAsk

Install the SDK via Swift Package Manager

Add the VINR iOS SDK to your Package.swift or via Xcode's package manager UI:

dependencies: [
    .package(
        url: "https://github.com/vinr/vinr-ios-sdk.git",
        from: "1.0.0"
    )
],
targets: [
    .target(
        name: "YourApp",
        dependencies: ["VinrSDK"]
    )
]

Request the Tap to Pay entitlement

In the Apple Developer portal, navigate to your App ID, enable the Tap to Pay on iPhone capability, and download the updated provisioning profile. Add the entitlement to your .entitlements file:

<key>com.apple.developer.proximity-reader.payment.acceptance</key>
<true/>

Apple reviews entitlement requests within one to two business days. Your app will fail to launch Tap to Pay without this entitlement, even in sandbox.

Discover the reader

Initialize the VINR client and discover the Tap to Pay reader before presenting a payment:

import VinrSDK

let vinr = Vinr(secretKey: ProcessInfo.processInfo.environment["VINR_SECRET_KEY"] ?? "")

Task {
    let reader = try await vinr.tapToPay.discoverReader()
    try await reader.connect()
}

Create a payment intent and collect payment

let intent = try await vinr.payments.create(
    amount: 2500,
    currency: "USD",
    captureMethod: .automatic
)

let result = try await reader.collectPayment(intentId: intent.id)

The SDK presents the built-in Apple UI asking the customer to tap their card or wallet. No custom UI is required.

Handle the result

switch result.status {
case .succeeded:
    print("Payment succeeded: \(result.paymentId ?? "")")
    await fulfillOrder(result.metadata)
case .requiresCapture:
    try await vinr.payments.capture(id: result.paymentId ?? "")
case .failed:
    print("Payment failed: \(result.errorCode ?? "unknown")")
default:
    break
}

Android integrationAsk

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

dependencies {
    implementation("com.vinr:sdk:1.+")
}

Declare NFC permission in AndroidManifest.xml:

<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />

Initialize the client and accept a contactless payment:

import com.vinr.sdk.Vinr
import com.vinr.sdk.tapTopay.TapToPayReader

val vinr = Vinr(secretKey = System.getenv("VINR_SECRET_KEY") ?: "")

lifecycleScope.launch {
    val reader = vinr.tapToPay.discoverReader(context)
    reader.connect()

    val intent = vinr.payments.create(
        amount = 2500,
        currency = "USD",
        captureMethod = "automatic"
    )

    val result = reader.collectPayment(intentId = intent.id)

    when (result.status) {
        "succeeded" -> fulfillOrder(result.paymentId)
        "requires_capture" -> vinr.payments.capture(id = result.paymentId)
        "failed" -> handleFailure(result.errorCode)
    }
}

Prop

Type

PCI scopeAsk

Tap to Pay on iPhone operates under Apple's certified L2 kernel, which carries an Apple-issued SAQ-A equivalent certification. The VINR Android SDK routes sensitive card data exclusively through the device's NFC controller and VINR's PCI-certified processing backend.

Never attempt to read, log, or store card data from NFC callback delegates or BroadcastReceivers. The platform APIs intentionally withhold raw card data from the application layer. Any attempt to intercept it will result in a rejected submission on iOS and may violate PCI DSS on Android. VINR's SDK surface only exposes the resulting payment intent ID, never card numbers or CVVs.

Merchants using Tap to Pay are in the same PCI scope as merchants using VINR-hosted checkout: minimal, with no requirement to handle raw card data. See Compliance overview for the full scope breakdown.

Receipts and refundsAsk

VINR automatically sends a digital receipt to the customer's email or phone number if one is associated with their wallet or card. No receipt printer is required.

To refund a Tap to Pay payment, the customer must be present to tap their original card or wallet again. Refunds cannot be issued remotely for card-present transactions; the tap verification confirms the card is still in the customer's possession.

import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

const refund = await vinr.refunds.create({
  payment: 'pay_3Nf8x2a',
  reason: 'requested_by_customer',
});

The refund object follows the same status lifecycle (pendingsucceeded | failed) as online refunds. See Refunds for full details on partial refunds, fee handling, and failure recovery.

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page