Check access

Gate features using the current customer's active perk grants.

Use the current person's active perk grants to decide what a customer can access in your app. The person snapshot also carries subscription state and purchase history, but perk grants are what you gate on.

The SDK decides what to show on the device. It cannot protect anything a customer can reach by calling your API directly. Those checks belong on your server. See Check access from your backend.

Gate a feature

A perk is available when the person holds a grant for it with status active. hasActivePerk(_:) takes the perk slug you set in Studio and checks exactly that. It does not compare expiresAt with the device clock: Voidhash keeps a subscription's grant active through the App Store's billing retry and grace period, and the next snapshot ends it when access really ends.

func hasPerk(_ slug: String) async throws -> Bool {
    let person = try await voidhash.getCurrentPerson()
    return person?.hasActivePerk(slug) ?? false
}

Wrap the check once in a helper like this so your screens stay simple.

Gate on perks, not subscription status

Subscription status cannot tell you which features a product unlocks. It also misses access that comes from one-time purchases or manual grants. Active perk grants are the source of truth for access control.

Offline and failure behavior

A failed refresh, whether the device is offline or the server returned an error, is not proof that the customer has no access.

While Voidhash is unreachable, getCurrentPerson() serves the last snapshot the device has. When it has none, it returns nil without that meaning the customer has no access. Use getCurrentPersonState() to tell the two apart: a nil value marked stale means the SDK could not ask.

let state = try await voidhash.getCurrentPersonState()
if state.value == nil && state.isStale {
    // Unknown. Retry later. Never lock by default.
} else {
    let hasAccess = state.value?.hasActivePerk("premium") ?? false
    // Route to premium content or the upgrade prompt based on hasAccess.
}

Read the current person

When you need more than a single perk check, read the full snapshot:

let person = try await voidhash.getCurrentPerson()

The call returns nil when no person exists yet for this identity. The snapshot contains entitlements, subscription state, and purchase history. activePerkSlugs lists the slugs of every perk the person can use now. Filter entitlements.grants yourself when you need more than that.

Grant fields

Each entry in entitlements.grants has these fields.

FieldMeaning
perkSlugThe perk slug configured in Studio. nil if the perk was deleted.
perkIdThe perk's internal ID. It is not the slug, so do not compare it with one.
statusactive or expired.
sourcesubscription, purchase, or manual.
sourceIdThe subscription, purchase, or manual grant that created it.
expiresAtExpiration time, or nil for access without an expiry.

Use subscriptions.current for account UI, such as the current plan or its renewal state. Use purchases.history when you need to show past transactions.

Refresh behavior

The SDK refreshes the person after purchases, restores, and identity changes. restorePurchases() and syncPurchases() wait for that refresh before they return, so the person you read right after them already includes what they restored. Between refreshes the SDK serves a cached snapshot. The snapshot is cached for two days and is served stale after five minutes. Pass forceFetch: true when you need a network round trip:

try await voidhash.getCurrentPerson(forceFetch: true)

A forced read waits for Voidhash to answer, up to the request timeout. If it gets no answer, it returns the cached snapshot, and getCurrentPersonState(forceFetch: true) marks that value stale.

Next steps