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

RequirementVersion
PlatformiOS 15+ (paywalls require UIKit)
ToolchainSwift 6, builds with the Swift 5.9 language mode
StoreKitStoreKit 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)
OptionDefaultUse
baseUrlhttps://api.voidhash.comOverride the API origin for self-hosted deployments.
ingestUrlSame origin as baseUrlOverride only the analytics origin.
debugfalseEnable diagnostics and mark requests as debug-build traffic.
distinctIdGenerated anonymous IDSeed the initial customer identity. Usually omit it and call identify().
enabledtrueSet false to ship the SDK fully inert.
readOnlytrueForced on while commerce features are unavailable.
onWarningUnified logReceives 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 response

The 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 == true

Pass 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.