Skip to content

Transactions

startTransaction(amount:currency:type:identityId:) presents Apple's tap sheet, reads the card, submits the payment to Finix, and returns a TapToPayTransactionResult. Progress updates arrive on the transactionEvents publisher.

Prepare the reader

prepareReader() async throws authenticates with Finix, hands Apple a reader token, and brings up the secure card-reading session. The first call after install can take several seconds; subsequent calls are fast and a prepared session is reused across transactions.

Preparation is required in these cases:

WhenReason
At app launch or foreground (once linked)Apple requires warm-up before the first tap. autoPrepareOnForeground only re-prepares an existing session.
After every transactionThe SDK does not re-prepare automatically. Keep the reader ready for the next Buyer.
After refreshConfiguration()Refreshing invalidates the reader session.
Before retrying after .readerPreparationFailedResolve the underlying cause (see Error reference), then prepare again.

Warm-up pattern

warmUpIfNeeded is a thin wrapper over prepareReader() that avoids stacking duplicate preparation calls. Call it from your scene's foreground transition (for example, when scenePhase becomes .active), gated on a "device provisioned and linked" state so fresh installs do not attempt a preparation that will fail.

Show an indeterminate indicator while preparation runs. If reader configuration is ongoing, switch to a determinate indicator driven by configurationProgress.

On failure, prepareReader() throws TapToPayError.readerPreparationFailed(String?). The associated message may include Apple configuration codes (2011, 2012, 2013). See the Error reference.

Show configuration progress

configurationProgress is a Combine publisher that emits the reader configuration progress Apple reports (0–100). The SDK reads these values from Apple's PaymentCardReader.events. While values arrive, show a determinate progress indicator as Apple's Tap to Pay on iPhone guidelines require.

Keep the Tap to Pay on iPhone checkout option selectable even while configuration is still running; show the progress indicator after the merchant selects it, rather than blocking the option until configuration finishes.

Take a payment

startTransaction(amount:currency:type:identityId:surchargeAmount:tipAmount:promptForSignature:) async throws -> TapToPayTransactionResult

ParameterTypeDescription
amountIntAmount in the currency's minor unit (cents for USD). 1250 is $12.50.
currencyStringISO 4217 currency code, e.g. "USD".
typeTransactionType.sale (default), .authorization, or .refund. Each has a displayName ("Sale", "Authorization", "Refund") for UI labels.
identityIdString?Finix Buyer Identity ID. Optional; defaults to nil.
surchargeAmountInt?Surcharge added on top of amount, in the currency's minor unit. Optional; defaults to nil.
tipAmountInt?Tip added on top of amount, in the currency's minor unit. Optional; defaults to nil.
promptForSignatureBoolPrompt the Buyer to sign on the device after approval. Defaults to false.

Reader calls require the foreground. From the background, startTransaction() throws .backgroundRequestNotAllowed.

Subscribe to transaction events

transactionEvents is a Combine publisher (AnyPublisher<TapToPayTransactionEvent, Never>) that emits the transaction's progress:

EventWhen it firesSuggested UI
.preparingTransactionThe transaction is starting.Show your processing screen now. Apple expects your UI to respond within about a second of the merchant starting a payment.
.readingCardApple's tap sheet is on screen, waiting for a tap.No UI changes needed while Apple's sheet is on screen.
.cardReadThe card read completed; the tap sheet dismisses.Keep your processing screen up.
.processingThe SDK is submitting the payment to Finix.Keep your processing screen up.
.success(TapToPayTransactionResult)The payment succeeded.Show your approved screen. The result includes the Finix Transfer. See TapToPayTransactionResult below.
.failure(TapToPayError)The payment failed.Show your declined or error screen. See the Error reference.

Handle all current cases and include @unknown default so future SDK versions can add events without breaking your build.

The publisher replays the latest event

A new subscriber immediately receives the most recent event, which can include a .success or .failure left over from a previous transaction. Subscribe before you call startTransaction, and ignore terminal events that arrive before you have started a new transaction. Tracking your own "transaction in progress" flag is a simple way to do this.

Process a sale

A returned TapToPayTransactionResult indicates success: transferState is "SUCCEEDED" and transferId identifies the Finix Transfer. The SDK submits the Transfer itself; do not create another Transfer for the same payment. Declines and processing failures throw TapToPayError.transactionFailed and emit .failure on transactionEvents.

The SDK does not re-prepare the reader after a transaction. Re-prepare in a defer to keep the reader ready for the next tap.

Add surcharges, tips, and signature prompts

Pass surchargeAmount, tipAmount, and promptForSignature on the same startTransaction call to collect those values alongside a sale. All are optional and can be combined with identityId.

Authorize now, capture later

The tap experience is identical to a sale. The authorization places a hold; the funds are not captured until a separate API call. Capture, void, or let the authorization expire using the Finix API. See Auth and captures for the capture flow.

Refund to a tapped card

An unreferenced refund: funds are returned to whichever card the Buyer taps, without reference to a prior Transfer.

To refund a specific prior payment, reverse its Transfer through the Finix API using the stored transferId instead.

Associate a Buyer Identity

Pass a Finix Buyer Identity ID as the identityId parameter to associate the resulting Transfer with that Identity. identityId is optional and defaults to nil.

Cancel a transaction

TriggerError thrown
Buyer dismisses Apple's tap sheetTapToPayError.transactionCancelled
cancelTransaction() called programmaticallyTapToPayError.transactionCancelled

The error surfaces at the startTransaction() call site. No separate cancellation event is emitted on transactionEvents.

The result object

TapToPayTransactionResult

FieldTypeDescription
amountIntAmount in the currency's minor unit (cents for USD).
currencyStringCurrency code, e.g. "USD".
cardBrandString?Card brand, e.g. "VISA", "MASTERCARD".
last4String?Last four digits of the card number.
maskedCardNumberString?Masked PAN, e.g. "**** **** **** 1234".
cardTypeString?Reserved. Currently nil on live transactions; do not rely on it.
emvDataString?Reserved. Currently nil on live transactions; do not rely on it.
timestampDateWhen the transaction completed.
readerIdentifierString?Apple reader identifier. Useful in support requests.
transactionIdentifierString?Apple transaction identifier.
transferIdString?The Finix Transfer ID. The payment as it appears in the Finix Dashboard and API.
transferStateString?Transfer state; "SUCCEEDED" on returned results.
approvalCodeString?Processor approval code.
traceIdString?Finix trace ID. Include it in support requests.

TapToPayTransactionResult is Equatable and Sendable. You cannot construct one directly; in your tests, use the mock factory.

Reconcile against the Finix Transfer

transferId is String?; it is populated for payments the SDK successfully submitted to Finix. For your local records, store transferId when present and fall back to transactionIdentifier otherwise. Only call the Finix API when transferId is non-nil:

curl -i https://finix.sandbox-payments-api.com/transfers/{transfer_id} \
  -u {your_api_key}:{your_api_secret} \
  -H 'Accept: application/hal+json' \
  -H 'Finix-Version: 2022-02-01'

Tap to Pay on iPhone Transfers have entry_mode CONTACTLESS and include card-present details (brand, masked account number, approval code).

Localize the Buyer experience

func setUserInterfaceLanguage(_ language: Locale.Language?) sets the language for Apple's Buyer-facing screens on the SDK instance. Pass nil to revert to the device language.

tapToPay.setUserInterfaceLanguage(Locale.Language(identifier: "fr-CA"))
ScopeBehavior
Buyer-facing screens (tap sheet, PIN prompts)Follow the language set via setUserInterfaceLanguage(_:). Defaults to the device language.
Merchant-facing UI, including Apple's Terms and Conditions sheetAlways follow the device language (Apple platform behavior).
SDK error messages (errorDescription)English only. Map error cases to localized copy on your side.
Use BCP-47 identifiers

Construct Locale.Language with a hyphenated BCP-47 identifier like "fr-CA". Do not pass Locale.identifier, which produces underscore-separated values like fr_CA that Apple's tap sheet rejects. The SDK does not validate the identifier.

The setting persists on the SDK instance until changed or set to nil.

try await tapToPay.prepareReader()

@MainActor
final class TapToPayWarmer {
    private var warmupTask: Task<Void, Never>?

    func warmUpIfNeeded(using tapToPay: FinixTapToPay) {
        guard warmupTask == nil else { return } // A warm-up is already running
        warmupTask = Task {
            defer { warmupTask = nil }
            guard FinixTapToPay.isSupported(),
                  await tapToPay.isAccountLinked() else { return }
            try? await tapToPay.prepareReader()
        }
    }
}

tapToPay.configurationProgress
    .receive(on: DispatchQueue.main)
    .sink { progress in
        print(progress) // Implement your own indicator
    }
    .store(in: &cancellables)

import Combine

var cancellables = Set<AnyCancellable>()

// The publisher replays the latest event to a new subscriber, which can be
// the .success or .failure of a *previous* transaction.
var transactionInProgress = false

tapToPay.transactionEvents
    .receive(on: DispatchQueue.main)
    .sink { event in
        switch event {
        case .preparingTransaction:
            transactionInProgress = true
            showProcessingScreen()
        case .readingCard, .cardRead, .processing:
            transactionInProgress = true
            keepProcessingScreenVisible()
        case .success(let result):
            guard transactionInProgress else { break } // Replayed from a previous transaction
            transactionInProgress = false
            showApprovedScreen(result)
        case .failure(let error):
            guard transactionInProgress else { break } // Replayed from a previous transaction
            transactionInProgress = false
            showFailureScreen(error)
        @unknown default:
            break // Future SDK versions may add events; ignore unknown ones
        }
    }
    .store(in: &cancellables)

func takePayment(amount: Int, using tapToPay: FinixTapToPay) async {
    // Re-prepare the reader after the transaction, regardless of outcome.
    defer {
        Task { try? await tapToPay.prepareReader() }
    }
    do {
        let result = try await tapToPay.startTransaction(
            amount: amount,   // In cents: 1250 = $12.50
            currency: "USD",
            type: .sale
        )
        handleApproved(result)
    } catch TapToPayError.transactionCancelled {
        returnToCheckout()
    } catch {
        presentFailure(error) // See the Error reference
    }
}

let result = try await tapToPay.startTransaction(
    amount: 1000,
    currency: "USD",
    type: .sale,
    identityId: "IDxxxxxxxxxxxxx",
    surchargeAmount: 30,
    tipAmount: 200,
    promptForSignature: true
)

let result = try await tapToPay.startTransaction(
    amount: 5000,   // $50.00 hold
    currency: "USD",
    type: .authorization
)

let result = try await tapToPay.startTransaction(
    amount: 1250,   // $12.50 refund
    currency: "USD",
    type: .refund
)

let result = try await tapToPay.startTransaction(
    amount: 1250,
    currency: "USD",
    type: .sale,
    identityId: "IDxxxxxxxxxxxxx"
)

Button("Cancel") {
    Task {
        try? await tapToPay.cancelTransaction()
        // The in-flight startTransaction call throws .transactionCancelled.
    }
}

func handleApproved(_ result: TapToPayTransactionResult) {
    // A returned result is an approved payment (transferState == "SUCCEEDED").
    let referenceId = result.transferId ?? result.transactionIdentifier

    receiptStore.save(
        id: referenceId,
        amount: result.amount,
        currency: result.currency,
        cardBrand: result.cardBrand,
        last4: result.last4,
        approvalCode: result.approvalCode,
        completedAt: result.timestamp
    )
}

// Buyer-facing screens in French (Canada). Use the BCP-47 tag, with a hyphen.
tapToPay.setUserInterfaceLanguage(Locale.Language(identifier: "fr-CA"))

// Back to the device language:
tapToPay.setUserInterfaceLanguage(nil)