FinanceKit: Reading Financial Transaction Data in iOS Apps
Your users already track their spending in Apple Wallet. With iOS 18, Apple cracked open that vault — not with a REST
API and OAuth dance, but with a privacy-first, on-device framework called FinanceKit. One await call and a system
permission sheet are all that stand between your app and a user’s real transaction history from Apple Card and
participating financial institutions.
This guide covers requesting authorization, querying transactions with predicates and sort descriptors, observing live changes, and building a SwiftUI view that surfaces spending data. We will not cover Apple Pay integration (that is PassKit territory) or server-side receipt validation.
Contents
- The Problem
- FinanceKit at a Glance
- Requesting Authorization
- Querying Transactions
- Observing Live Changes
- Building a Transaction List in SwiftUI
- Advanced Usage
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Personal finance apps have historically relied on third-party aggregators — Plaid, Yodlee, MX — that screen-scrape bank credentials or use fragile OAuth connections. The result is a UX that asks users to hand their bank password to a third party, connections that break when banks update their login flows, and a data pipeline that routes sensitive financial information through external servers.
// ❌ The old world: third-party aggregation
final class LegacyTransactionFetcher {
private let plaidClient: PlaidClient
func fetchTransactions() async throws -> [Transaction] {
// 1. User enters bank credentials in a third-party WebView
let linkToken = try await plaidClient.createLinkToken()
let publicToken = try await plaidClient.presentLink(token: linkToken)
let accessToken = try await plaidClient.exchangePublicToken(publicToken)
// 2. Plaid scrapes the bank on your behalf
return try await plaidClient.getTransactions(accessToken: accessToken)
// Data transits Plaid's servers, your server, then the device
}
}
This architecture introduces latency, privacy concerns, and a dependency on a third-party vendor’s uptime. FinanceKit eliminates the middleman for Apple Card and supported institutions by reading transaction data directly from the device’s Wallet database.
FinanceKit at a Glance
FinanceKit is an iOS 18+ framework that provides read-only
access to financial data stored in Apple Wallet. The framework exposes three primary data types:
FinanceStore— The entry point. You request authorization and query data through a shared instance.Transaction— A single financial transaction: amount, merchant, date, category, and status.Account— A financial account (Apple Card, savings account) with balance and institution metadata.
All data stays on-device. FinanceKit never sends financial information to your servers or Apple’s — the queries execute against a local database managed by the Wallet app.
Apple Docs:
FinanceKit— Apple Developer Documentation
Entitlement and Capability
Before writing any code, you need the FinanceKit entitlement. In Xcode, navigate to your target’s Signing &
Capabilities tab, click + Capability, and add FinanceKit. You also need to add a usage description to your
Info.plist:
<key>NSFinanceUsageDescription</key>
<string>PixarBudget uses your transaction data to categorize your spending by movie studio.</string>
Warning: Apps distributed outside the App Store (enterprise or TestFlight-only) still require the FinanceKit entitlement. Apple reviews the usage description during App Review — vague descriptions like “to improve your experience” will be rejected.
Requesting Authorization
FinanceKit uses a two-step authorization model. First, you check the current authorization status. Then, if the status
is .notDetermined, you request access. The system presents a permission sheet that clearly describes what data your
app can read.
import FinanceKit
@available(iOS 18.0, *)
final class PixarBudgetStore {
private let financeStore = FinanceStore.shared
/// Checks and requests authorization for transaction data.
/// Returns `true` if the user granted access.
func ensureAuthorized() async -> Bool {
let status = FinanceStore.isDataAvailable(.financialData)
guard status == .available else {
// Device does not support FinanceKit (no Wallet data)
return false
}
do {
let authStatus = try await financeStore.requestAuthorization()
return authStatus == .authorized
} catch {
print("Authorization request failed: \(error.localizedDescription)")
return false
}
}
}
The requestAuthorization() call is async — it suspends until the user taps Allow or Deny on the system sheet. If the
user has already granted or denied permission, the call returns immediately without showing a sheet.
Tip: Call
FinanceStore.isDataAvailable(.financialData)before requesting authorization. On devices without any configured financial accounts in Wallet, this returns.unavailable, and you can skip the authorization flow entirely and show a helpful empty state instead.
Querying Transactions
Once authorized, you query transactions using a predicate-and-sort-descriptor pattern that will feel familiar if you have worked with Core Data or SwiftData.
Fetching All Transactions
@available(iOS 18.0, *)
extension PixarBudgetStore {
/// Fetches all transactions, sorted by date descending.
func fetchAllTransactions() async throws -> [Transaction] {
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)]
)
return try await financeStore.transactions(query: query)
}
}
The transactions(query:) method returns an array of
Transaction values. Each transaction exposes
properties like merchantName, transactionAmount, transactionDate, creditDebitIndicator, and status.
Filtering with Predicates
For a Pixar budget tracker, you might want to find all transactions at entertainment merchants above a certain amount — say, every time Buzz Lightyear’s family spent more than $50 at a movie theater:
@available(iOS 18.0, *)
extension PixarBudgetStore {
/// Fetches entertainment transactions above a given threshold.
func fetchEntertainmentTransactions(
minimumAmount: Decimal
) async throws -> [Transaction] {
let predicate = #Predicate<Transaction> { transaction in
transaction.transactionAmount.amount >= minimumAmount
&& transaction.transactionDescription.localizedStandardContains("entertainment")
}
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate
)
return try await financeStore.transactions(query: query)
}
}
Fetching Accounts
You can also query the user’s financial accounts to display balances or group transactions by account:
@available(iOS 18.0, *)
extension PixarBudgetStore {
/// Fetches all financial accounts (Apple Card, savings, etc.)
func fetchAccounts() async throws -> [Account] {
let query = AccountQuery(
sortDescriptors: [SortDescriptor(\Account.displayName, order: .forward)]
)
return try await financeStore.accounts(query: query)
}
/// Fetches transactions for a specific account.
func fetchTransactions(for account: Account) async throws -> [Transaction] {
let predicate = #Predicate<Transaction> { transaction in
transaction.accountID == account.id
}
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate
)
return try await financeStore.transactions(query: query)
}
}
Note: FinanceKit returns transactions from Apple Card and any financial institution that has adopted Apple’s FinanceKit data-sharing protocol. Not all banks participate yet — check
fetchAccounts()to see which accounts are available on a given device.
Observing Live Changes
FinanceKit provides a sequence-based observation API for monitoring new transactions in real time. This is ideal for updating a dashboard as the user makes purchases throughout the day:
@available(iOS 18.0, *)
extension PixarBudgetStore {
/// Observes transaction changes and calls the handler for each batch.
func observeTransactionChanges() -> some AsyncSequence {
financeStore.transactionHistory(
since: .now,
query: TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)]
)
)
}
}
The returned async sequence yields FinanceStore.TransactionHistory.Event values — each event contains an array of
inserted, updated, or deleted transactions. Consume it with a standard for await loop:
@available(iOS 18.0, *)
func startObserving(store: PixarBudgetStore) async {
for await event in store.observeTransactionChanges() {
await MainActor.run {
applyChanges(event)
}
}
}
Because this is an AsyncSequence, cancelling the enclosing Task terminates the observation cleanly — no delegate
deregistration or observer removal required. See AsyncSequence and AsyncStream for the
full pattern.
Building a Transaction List in SwiftUI
Here is a complete SwiftUI view that ties authorization, fetching, and display together. Imagine Woody is building a personal budget app to track the Toy Story gang’s spending:
import SwiftUI
import FinanceKit
@available(iOS 18.0, *)
struct ToyBoxSpendingView: View {
@State private var transactions: [Transaction] = []
@State private var isAuthorized = false
@State private var isLoading = false
@State private var errorMessage: String?
private let store = PixarBudgetStore()
var body: some View {
NavigationStack {
Group {
if !isAuthorized {
authorizationPrompt
} else if isLoading {
ProgressView("Loading transactions...")
} else if let errorMessage {
ContentUnavailableView(
"Something went wrong",
systemImage: "exclamationmark.triangle",
description: Text(errorMessage)
)
} else {
transactionList
}
}
.navigationTitle("Toy Box Budget")
.task { await authorize() }
}
}
private var authorizationPrompt: some View {
ContentUnavailableView {
Label("Transaction Access Required", systemImage: "lock.shield")
} description: {
Text("Allow Toy Box Budget to read your transaction history.")
} actions: {
Button("Grant Access") {
Task { await authorize() }
}
.buttonStyle(.borderedProminent)
}
}
private var transactionList: some View {
List(transactions, id: \.id) { transaction in
TransactionRow(transaction: transaction)
}
.refreshable { await loadTransactions() }
}
private func authorize() async {
isAuthorized = await store.ensureAuthorized()
if isAuthorized {
await loadTransactions()
}
}
private func loadTransactions() async {
isLoading = true
errorMessage = nil
do {
transactions = try await store.fetchAllTransactions()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
The TransactionRow component extracts display-ready data from each Transaction:
@available(iOS 18.0, *)
struct TransactionRow: View {
let transaction: Transaction
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(transaction.merchantName ?? "Unknown Merchant")
.font(.headline)
Text(transaction.transactionDate, style: .date)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text(
transaction.transactionAmount.amount,
format: .currency(
code: transaction.transactionAmount.currencyCode
)
)
.font(.body.monospacedDigit())
.foregroundStyle(
transaction.creditDebitIndicator == .credit
? .green
: .primary
)
}
.padding(.vertical, 4)
}
}
Tip:
Transaction.transactionAmountis aCurrencyAmountstruct with separateamount(Decimal) andcurrencyCode(String) properties. Always use thecurrencyCodefrom the transaction rather than hardcoding a currency — users with international accounts will have transactions in multiple currencies.
Advanced Usage
Grouping Transactions by Category
FinanceKit transactions include a transactionType property that maps to broad categories. You can use Swift’s
Dictionary(grouping:by:) to build a category breakdown — perfect for a pie chart in a Pixar-themed budget dashboard:
@available(iOS 18.0, *)
extension PixarBudgetStore {
/// Groups transactions by type and returns totals per category.
func spendingByCategory() async throws -> [Transaction.TransactionType: Decimal] {
let transactions = try await fetchAllTransactions()
let grouped = Dictionary(grouping: transactions) { $0.transactionType }
return grouped.mapValues { transactions in
transactions.reduce(Decimal.zero) { total, transaction in
total + transaction.transactionAmount.amount
}
}
}
}
Handling the “Denied” State Gracefully
Users can revoke FinanceKit access at any time in Settings. Your app should handle this without crashing:
@available(iOS 18.0, *)
extension PixarBudgetStore {
/// Attempts to fetch transactions, returning an empty array if access was revoked.
func fetchTransactionsSafely() async -> [Transaction] {
do {
let authStatus = try await financeStore.requestAuthorization()
guard authStatus == .authorized else {
return []
}
return try await fetchAllTransactions()
} catch let error as FinanceKit.FinanceKitError {
switch error {
case .authorizationDenied:
// User revoked access — show a re-authorization prompt
return []
case .dataNotAvailable:
// No financial accounts configured
return []
default:
return []
}
} catch {
return []
}
}
}
Warning: Do not cache financial transaction data to disk without encryption. FinanceKit deliberately restricts data to on-device, in-memory access. If you need to persist aggregated data (e.g., monthly totals), store only the computed summaries — never raw transaction details — and encrypt them with the Data Protection API.
Date-Range Queries
Most finance features need a date-bounded query. Here is a helper that fetches transactions for the current month:
@available(iOS 18.0, *)
extension PixarBudgetStore {
/// Fetches transactions from the start of the current month to now.
func fetchCurrentMonthTransactions() async throws -> [Transaction] {
let calendar = Calendar.current
let now = Date.now
guard let startOfMonth = calendar.date(
from: calendar.dateComponents([.year, .month], from: now)
) else {
return []
}
let predicate = #Predicate<Transaction> { transaction in
transaction.transactionDate >= startOfMonth
&& transaction.transactionDate <= now
}
let query = TransactionQuery(
sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
predicate: predicate
)
return try await financeStore.transactions(query: query)
}
}
Performance Considerations
FinanceKit queries execute against a local SQLite database managed by the Wallet process. Performance characteristics differ from network-based APIs:
- Cold queries (first query after app launch) involve an IPC round-trip to the Wallet daemon and may take 100-300ms for a few hundred transactions. Subsequent queries are faster because the Wallet process keeps the database connection warm.
- Predicate pushdown matters. Predicates passed to
TransactionQueryare evaluated inside the Wallet process, not in your app’s memory space. A query with a tight date range predicate fetches only matching rows, while fetching all transactions and filtering client-side loads the entire history into your process. - Pagination is not built into the API. If a user has years of Apple Card history (thousands of transactions), a
single
transactions(query:)call returns everything matching the predicate. Use date-range predicates to batch your queries — fetch one month at a time for infinite-scroll UIs. - Memory — each
Transactionvalue is lightweight (a few hundred bytes), but 10,000 transactions still add up. For analytics features that process large datasets, compute aggregates in a detachedTaskand discard the raw array promptly.
Apple Docs:
FinanceStore— FinanceKit
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Personal finance or budgeting app reading Apple Card data | FinanceKit — purpose-built, on-device, privacy-preserving |
| Aggregating transactions from non-Apple banks | Third-party aggregator (Plaid, MX) — FinanceKit only reads Wallet data |
| Processing payments or accepting Apple Pay | PassKit — FinanceKit is read-only |
| In-app purchase receipt validation | StoreKit 2 — different domain entirely |
| Displaying transaction data on a server dashboard | Not FinanceKit — the data never leaves the device by design |
| Expense reporting with receipt matching | FinanceKit for transactions + VisionKit for receipt OCR |
FinanceKit is deliberately narrow. It reads financial data from Apple Wallet — nothing more. If your feature requires writing data, triggering payments, or accessing non-Wallet accounts, you need a different tool. But for the growing category of personal finance apps that want to surface Apple Card spending without a third-party aggregator, FinanceKit is the cleanest path available.
Summary
- FinanceKit is an iOS 18 framework that provides read-only, on-device access to transaction data from Apple Card and participating financial institutions — no third-party aggregator required.
- Authorization uses a single
requestAuthorization()async call that presents a system permission sheet. Always checkisDataAvailable(.financialData)first to handle devices with no Wallet accounts. - Queries use
TransactionQuerywith Swift predicates and sort descriptors. Predicates execute inside the Wallet process, so push filtering as close to the query as possible for best performance. - Live observation via
transactionHistory(since:query:)returns anAsyncSequenceof change events, integrating naturally with structured concurrency andfor awaitloops. - Never persist raw transaction data to disk. FinanceKit’s privacy model assumes data stays in memory — store only computed aggregates if you need persistence.
For handling the monetary flows your app generates rather than reads, see StoreKit 2: In-App Purchases and Subscriptions. And if you are securing the financial data your app does store, Passkeys and AuthenticationServices covers the modern credential story.