Skip to content

Transactions

To run a transaction, call startTransaction(amount:currency:type:identityId:). The SDK presents Apple's tap sheet, reads the card, submits the payment to Finix, and returns the completed result. Progress updates arrive on the transactionEvents publisher so you can update your UI at each stage.

This page covers the full transaction lifecycle: preparing the reader, taking a payment, handling the result, and localizing the payment experience.

Prepare the reader

Prepare the reader

prepareReader() configures Apple's payment card reader on the device: it authenticates with Finix, hands Apple a reader token, and brings up the secure card-reading session. The first preparation after install can take several seconds. Later preparations are much faster, and a prepared session is reused across transactions.

Before you start: the SDK must be initialized and the merchant's Apple Account linked. The app must be in the foreground.

Apple expects the reader to be ready before the Buyer starts waiting, so warm it up ahead of checkout:

WhenWhy
At app launch or foreground (once linked)Apple requires warm-up at launch or foreground so the first tap is fast. autoPrepareOnForeground only re-prepares when a session already exists, so your app is still responsible for the first prepareReader() call.
After every transactionThe SDK does not re-prepare automatically after a transaction completes. Call prepareReader() again after each transaction (for example, in a defer as shown in Process a sale) so the reader is ready for the next Buyer.
After refreshConfiguration()Refreshing invalidates the reader session; prepare again before the next transaction.
Before retrying after .readerPreparationFailedResolve the cause (see Troubleshooting and support), then prepare again.

Warm-up pattern

Warm-up pattern

Prepare once per foreground transition and avoid stacking duplicate calls. Call warmUpIfNeeded from your scene's foreground transition (for example, when scenePhase becomes .active). Only warm up once the merchant has completed Tap to Pay on iPhone setup: gate on your "device provisioned and linked" state so fresh installs don't attempt a preparation that will always fail.

If the user is waiting on a preparation (for example, during first-time setup), show an indeterminate progress indicator. The SDK does not report preparation progress percentages.

If preparation fails, prepareReader() throws TapToPayError.readerPreparationFailed(String?) with the underlying reason in the associated message, including Apple configuration codes such as 2011, 2012, and 2013. See the Error reference and Troubleshooting and support for what each code means and how to recover.

Take a payment

The type parameter is a TransactionType: .sale (the default), .authorization, or .refund. Each provides a displayName ("Sale", "Authorization", "Refund") you can use for UI labels.

Before you start:

  • The SDK is initialized with an activated Device, the account is linked, and the reader is prepared.
  • Your app is in the foreground. Reader calls from the background throw .backgroundRequestNotAllowed.
  • You are subscribed to transactionEvents. Subscribe before starting the transaction.

Subscribe to transaction events

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 Handle the result.
.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

Process a sale

Amounts are integers in the currency's minor unit: cents for USD (1250 is $12.50).

If startTransaction returns, the payment succeeded. The result's transferState is "SUCCEEDED" and transferId identifies the Finix Transfer. Declines and processing failures throw TapToPayError.transactionFailed (and emit .failure) instead. The SDK submits the Transfer itself, so do not create another Transfer for the same payment.

The example re-prepares the reader in a defer because the SDK does not re-prepare automatically after a transaction. This keeps the reader ready for the next tap.

Authorize now, capture later

Authorize now, capture later

Place a hold now and capture it later. This is useful for tabs or delayed fulfillment.

The tap experience is identical to a sale. The difference is in how Finix records the transaction. Capture, void, or let the authorization expire using the Finix API. See Auth and captures for the full flow.

Refund to a tapped card

Refund to a tapped card

Return funds to a Buyer's card without referencing an earlier payment (an unreferenced refund). The Buyer taps the card that should receive the funds.

To refund a specific earlier payment instead, reverse its Transfer through the Finix API using the transferId you stored from the original result.

Associate a Buyer Identity

Associate a Buyer Identity

To tie the payment to a Finix Buyer Identity (such as a stored Buyer profile), pass its ID. identityId is optional and defaults to nil.

Cancel a transaction

Cancel a transaction

The Buyer can always dismiss Apple's tap sheet. When that happens, your startTransaction call throws TapToPayError.transactionCancelled. To cancel programmatically (for example, from a cancel button on your processing screen), call cancelTransaction().

Handle the cancellation where you called startTransaction (as in the sale example above). No separate cancellation event is published.

Handle the result

A returned TapToPayTransactionResult is an approved payment. Use its fields to build receipts and to reconcile the payment on your backend.

The result object

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.

Record and reconcile

Record and reconcile

Store a reference to the payment as soon as you have the result. Use the Finix transferId when present, and fall back to Apple's identifier otherwise.

On your backend, the payment is a regular Finix Transfer. Fetch it any time to reconcile:

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 include card-present details (brand, masked account number, approval code) with entry_mode CONTACTLESS.

Receipts

Apple requires that merchants can offer the Buyer a digital receipt after every outcome, and the delivery method must keep the Buyer's contact details confidential. SMS, email, QR codes, and the share sheet all qualify. Render receipts from the result fields above (brand, last four, amount, approval code, timestamp). App Review checks for this. See Prepare for App Review.

Localize the payment experience

Localize the payment experience

By default, Apple's tap sheet follows the device language. If your app has its own language selection (for example, a merchant who serves Buyers in another language), set the language on the SDK with setUserInterfaceLanguage(_:).

Use BCP-47 tags

Pass hyphenated BCP-47 identifiers like "fr-CA". Don't pass the output of Locale.identifier, which produces underscore-separated values like fr_CA that Apple's tap sheet does not accept. The SDK passes your tag through without validation.

Scope of the setting:

  • It changes Buyer-facing screens only (the tap sheet and PIN-style prompts where applicable).
  • Merchant-facing UI, including Apple's Terms and Conditions sheet, always follows the device language (an Apple platform behavior).
  • SDK error messages are not localized; errorDescription strings are English. Map error cases to your own localized copy for user-facing messages.
  • The setting persists on the SDK instance for subsequent transactions until you change it or pass nil.

Next: Error reference.

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()
        }
    }
}

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: 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)