Swift SDK
Configure the Voidhash iOS SDK and look up its client options and methods.
The Swift SDK adds entitlements, feature flags, analytics, and observer-mode transaction reporting to native iOS apps. Use this page to create the client, set its options, and find the methods you need. Each feature also has its own guide, linked from the section that introduces it.
The initial release is observer-only
SDK-started purchases and hosted paywalls are temporarily unavailable. StoreKit transactions are still observed and submitted to Voidhash for revenue analytics, but the SDK never finishes them.
Compatibility
The SDK has three requirements.
- iOS 15 or later. Paywalls require UIKit.
- Swift 6, building with the Swift 5.9 language mode.
- StoreKit 2.
The package ships two library products. Voidhash is the SDK you integrate against. VoidhashCore
is the shared native core: the StoreKit engine, the paywall bridge and presenter, the API client,
identity, caching, and the schema. The React Native SDK's native layer uses VoidhashCore as well.
Install the package with Swift Package Manager from https://github.com/voidhashcom/voidhash, as
the quickstart shows. The repository has no release tags yet, so depend
on the main branch or pin a commit.
You can also install the SDK with CocoaPods through the npm package @voidhash/ios; the pods are
not published to the CocoaPods trunk. Declare both pods, because the local Voidhash pod cannot
resolve its local VoidhashCore dependency on its own.
pod "VoidhashCore", :path => "../node_modules/@voidhash/ios"
pod "Voidhash", :path => "../node_modules/@voidhash/ios"The sources also build for visionOS and for app extensions such as widgets. An extension keeps its
own identity, queues and caches and shares none of them with your app, because the SDK does not
use App Groups. To read the signed-in user's access in an extension, configure it with
options.distinctId set to the ID your app passes to identify, and turn off
automaticLifecycleEvents so the extension does not report its own app launches.
Privacy manifest
VoidhashCore ships a privacy manifest, which Xcode includes in your app's privacy report. It
declares no tracking, the UserDefaults API (reason CA92.1) for the SDK's own state, and the system
boot time API (reason 35F9.1) for measuring elapsed time. It lists the data the SDK sends to
Voidhash, all linked to the user and none used for tracking.
- User ID and device ID, from the anonymous or identified distinct ID, for app functionality and analytics.
- Email address and name, when you pass them to
identify, for app functionality. - Purchase history, from the store transactions the SDK reports, for app functionality and analytics.
- Product interaction, from captured events, screens and app lifecycle events, for analytics.
Declare anything else you send through the SDK, such as person attributes or event properties that hold personal data, in your app's own privacy details.
Create the client
Configure one client while the app launches and keep it for the lifetime of the app. In a SwiftUI
app, do it in the App initializer. In a UIKit app, do it in
application(_:didFinishLaunchingWithOptions:).
import SwiftUI
import Voidhash
@main
struct MyApp: App {
init() {
Voidhash.configure(publishableKey: "vh_pk_...")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}The publishable key is safe to include in the app. Never ship vh_sk_... secret keys.
configure returns the client, and the same client is reachable everywhere else as
Voidhash.shared. The examples on this page call it voidhash.
guard let voidhash = Voidhash.shared else { return }Avoid configuring the client in a let at file scope. Swift creates such a global the first time
code reads it, not at launch, so the SDK would not observe transactions or record app launches
until then. There is no schema argument, because the schema lives on the server and the SDK
fetches it during initialization.
Initialization runs in the background. It connects to the store, resolves the schema, and reconciles transactions that were observed while the app was away. The first call that needs initialization waits for it implicitly. When you want to gate UI on it, await it explicitly.
try await voidhash.waitForInitialization()If initialization fails, the SDK retries it on the next call that needs it.
Client options
Pass a VoidhashOptions value to configure to change the defaults.
var options = VoidhashOptions()
options.baseUrl = URL(string: "https://api.voidhash.com")! // API origin
options.ingestUrl = nil // Analytics ingest origin; defaults to baseUrl
options.debug = false // SDK logging; also marks requests as from a debug build
options.distinctId = nil // Seed the initial customer identity; usually omit
options.enabled = true // false makes the SDK fully inert
options.readOnly = true // Forced on in the initial observer-only release
options.onWarning = { message in } // Diagnostics never raised to the caller
Voidhash.configure(publishableKey: "vh_pk_...", options: options)Each option is described below.
| Option | Default | Description |
|---|---|---|
baseUrl | https://api.voidhash.com | Overrides the API origin for self-hosted deployments. |
ingestUrl | Same origin as baseUrl | Overrides only the analytics origin. |
debug | false | Enables diagnostics. Debug builds and dev mark requests as debug builds too. |
distinctId | Generated anonymous ID | Seeds the initial customer identity and links an earlier anonymous ID to it. |
enabled | true | Set it to false to ship the SDK fully inert. |
readOnly | true | Forced on while commerce features are unavailable. |
onWarning | Unified log | Receives background failures that are not surfaced as thrown errors. |
The SDK keeps its on-device state separately for each publishable key and API origin. A new key or
baseUrl starts with a new anonymous identity and empty queues, and data still queued under the
previous configuration is not sent. Keep both stable across app updates.
Disabled clients
With enabled: false, every method is inert. The SDK makes no requests, opens no store
connection, starts no timers or lifecycle observers, installs no screen-tracking hook and writes
nothing to storage. getProducts() returns an empty list, getCurrentPerson() returns nil, and
getDistinctId() returns an anonymous ID that is never stored or sent.
Reconfiguring
Calling configure again replaces the shared client. The previous client stops its store
observer and background work and hands its queued events and transactions to the new client. A
reference you kept to the old client stays safe to call: writes such as capture, identify and
reportTransaction go to the new client when it uses the same publishable key and API origin,
and reads return the same values as a disabled client.
Launches before the first unlock
iOS can launch an app in the background after a reboot, before the device was first unlocked.
The SDK cannot read its stored state then, so it waits for the unlock instead of starting a new
identity. Until then, reads return empty, stale values without waiting. Captured events,
identify calls and reported transactions are kept in memory and applied after the unlock.
Observer mode
The initial release always uses observer mode. Passing readOnly: false or calling
setReadOnly(false) cannot transfer StoreKit ownership to Voidhash yet.
Products and transaction reporting
Load the product catalog and restore the customer's existing purchases.
let products = try await voidhash.getProducts()
if let product = products.first(where: { $0.slug == "pro-monthly" }) {
// Prices are already formatted for the customer's storefront.
print(product.displayPrice, product.interval ?? "one-time")
}
try await voidhash.restorePurchases()Initialization and restore submit observed StoreKit transactions to Voidhash without finishing
them. purchase(product:) remains in the SDK for a later commerce launch, but it currently throws
READ_ONLY_PURCHASE_NOT_ALLOWED before it touches StoreKit. SDK-owned store sheets are inert.
After a successful host purchase, call reportTransaction(...) with its store transaction values.
A valid report is persisted before it returns and delivered in the background, so a network or
service outage delays reporting without failing the purchase flow. Use syncPurchases() for
callbacks without transaction values and for restore recovery. Store observers and scans cannot
recover a consumable already finished or consumed by the host.
See Report and restore purchases for callback examples, error
handling, and scan limits.
People and entitlements
Read the current person, identify them when they sign in, and reset the client when they sign out.
let person = try await voidhash.getCurrentPerson()
let isPro = person?.hasActivePerk("pro") ?? false
try await voidhash.identify(externalUserId: "user_123", email: "ada@example.com", name: "Ada")
try await voidhash.setPersonAttributes(["plan": .string("pro"), "seats": .number(3)])
let distinctId = await voidhash.getDistinctId()
await voidhash.reset() // Sign out: clears the identity and every cached responseThe SDK caches the person snapshot for two days. After five minutes it treats the snapshot as stale
but still serves it from the cache. Pass getCurrentPerson(forceFetch: true) to bypass the cache.
The SDK refreshes the snapshot on its own after purchases, restores, and identity changes.
See Check access and Identify customers for the underlying concepts.
Feature flags
Evaluate the flags you need by key.
let flags = try await voidhash.getFeatureFlags(["new-onboarding"])
let enabled = flags.first { $0.key == "new-onboarding" }?.enabled == truePass nil to evaluate every flag. See Evaluate feature flags for variants and
identity behavior.
Analytics
Capture an event, and call flush() when you need the queue sent right away.
await voidhash.capture("checkout_started", properties: ["plan": .string("pro")])
await voidhash.flush()The SDK batches events, sending up to 20 per request and flushing every 5 seconds while the app is
in the foreground. Failed requests are retried with exponential backoff, so the wait grows after
each failure. flush() sends everything queued right now, including reported purchases, without
waiting for the next retry. A reported purchase Voidhash refused for good waits for the next
launch. Event names beginning with $ are reserved for Voidhash events. See
Capture analytics.
Paywalls
presentFlow(placement:) presents the paywall or flow assigned to a placement and returns how it
ended: .purchased, .restored, .finished(result:variables:), .closed, or .failed with a
reason. Paywalls are drawn by the VoidhashUI library, which you add next to Voidhash. See
Display a paywall.