Swift SDK
Client configuration and API reference for the Voidhash iOS SDK.
The Voidhash iOS SDK adds entitlements, feature flags, analytics, and observer-mode transaction reporting to native Swift apps.
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
| Requirement | Version |
|---|---|
| Platform | iOS 15+ (paywalls require UIKit) |
| Toolchain | Swift 6, builds with the Swift 5.9 language mode |
| StoreKit | StoreKit 2 |
The package ships two library products:
Voidhash— the SDK you integrate against.VoidhashCore— the shared native core (StoreKit engine, paywall bridge and presenter, API client, identity, caching, schema). Also consumed by the React Native SDK's native layer.
CocoaPods is supported through the npm package @voidhash/ios; declare both pods because the local
Voidhash pod cannot resolve its local VoidhashCore dependency alone:
pod "VoidhashCore", :path => "../node_modules/@voidhash/ios"
pod "Voidhash", :path => "../node_modules/@voidhash/ios"Create the client
Create one client for the lifetime of the app:
import Voidhash
let voidhash = Voidhash.configure(publishableKey: "vh_pk_...")The publishable key is safe to include in the app. Never ship vh_sk_... secret keys.
configure returns the client, also reachable as Voidhash.shared. There is no schema argument:
the schema lives on the server and is fetched during initialization.
Initialization — store connection, schema resolution, reconciliation of transactions observed while the app was away — runs in the background and is awaited implicitly by the first call that needs it. Await it explicitly when you need to gate UI on it:
try await voidhash.waitForInitialization()A failed initialization is retried on the next call that needs it.
Client options
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 + marks requests as from a debug build
options.distinctId = nil // Seed the initial customer identity; usually omit
options.enabled = true // false makes every call inert (no network at all)
options.readOnly = true // Forced on in the initial observer-only release
options.onWarning = { message in } // Diagnostics never raised to the caller
let voidhash = Voidhash.configure(publishableKey: "vh_pk_...", options: options)| Option | Default | Use |
|---|---|---|
baseUrl | https://api.voidhash.com | Override the API origin for self-hosted deployments. |
ingestUrl | Same origin as baseUrl | Override only the analytics origin. |
debug | false | Enable diagnostics and mark requests as debug-build traffic. |
distinctId | Generated anonymous ID | Seed the initial customer identity. Usually omit it and call identify(). |
enabled | true | Set 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. |
Disabled clients
With enabled: false, every method is inert: no requests are made, no store connection opens,
getProducts() returns an empty list, and getCurrentPerson() returns nil.
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
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:) is retained for a later commerce launch but currently throws
READ_ONLY_PURCHASE_NOT_ALLOWED before StoreKit is touched. SDK-owned store sheets are inert.
People and entitlements
let person = try await voidhash.getCurrentPerson()
let isPro = person?.entitlements.grants.contains {
$0.perkId == "pro" && $0.status == "active"
} ?? 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 person snapshot is cached for two days and served stale after five minutes; pass
getCurrentPerson(forceFetch: true) to bypass the cache. The SDK refreshes it after purchases,
restores, and identity changes.
See Check access and Identify customers for the underlying concepts.
Feature flags
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
await voidhash.capture("checkout_started", properties: ["plan": .string("pro")])
await voidhash.flush()Events are batched (20 per request, flushed every 5 seconds) and retried with exponential backoff;
flush() sends everything queued right now. Names beginning with $ are reserved for Voidhash
events. See Capture analytics.
Paywalls
Hosted paywalls are temporarily unavailable. presentPaywall(...) returns .notAssigned, paywall
resolution returns nil, and no network or presentation work is performed.