Identify customers
Link anonymous purchase history to accounts in your authentication system.
Voidhash creates an anonymous distinct ID on first launch and persists it, so purchases and events
work before anyone signs in. If your app has accounts, call identify() after authentication.
Access then follows the customer across devices.
Identify after sign-in
Pass a stable identifier from your backend, plus optional profile fields:
try await voidhash.identify(externalUserId: user.id, email: user.email, name: user.name)Use an opaque, unique ID such as a UUID. Do not use an email address or a sequential database ID as the primary identifier.
The ID can contain non-Latin scripts. It must contain a non-whitespace character, be at most 255
characters long, contain no NUL character and not start with vh:anon:, which marks the SDK's
anonymous IDs. For any other ID, identify() throws an error with the code INVALID_ARGUMENT and
keeps the current identity. A distinctId option breaking these rules is ignored in the same way,
and the SDK keeps its anonymous identity.
When an anonymous customer signs in, Voidhash moves their purchase history and entitlements to the identified person. Events captured before the switch stay attributed to the anonymous identity.
Signing in while offline
The identity switches on the device right away, whether or not Voidhash is reachable. The link
from the anonymous ID to the account is saved on the device before the SDK sends it, and the SDK
keeps sending it until Voidhash applies it: after an outage, after a relaunch, and even when the
task that called identify() is cancelled once the switch happened. Queued links are sent before
queued purchases, so a purchase made anonymously is recorded after the merge.
Until Voidhash confirms the link, getCurrentPerson() keeps returning what the anonymous customer
had, marked stale, so a customer who bought before signing in keeps their access. Use
identifyState(externalUserId:email:name:) to learn whether the link was confirmed or is still
deferred:
let result = try await voidhash.identifyState(externalUserId: user.id)
if result.status == .deferred {
// Signed in on this device; Voidhash applies the link when it is reachable again.
}identify() throws only when Voidhash rejects the link for good, for example for an ID it refuses.
The SDK then restores the previous identity. It does not throw for network failures or a rejected
publishable key.
When the anonymous ID already belongs to a different account, for example after restoring an older
device backup, identify() does not throw. The customer is signed in as the new ID without the
anonymous ID's history and purchases, which stay with the other account, and the SDK reports the
PERSON_WRITE_REJECTED diagnostic.
Start with a known ID
If you already know the account when the app starts, you can pass it as the distinctId option
instead of calling identify(). The SDK adopts it without a request. If the device used an
anonymous ID before, the SDK links that ID to the account in the background, the same way
identify() does, so earlier purchases follow the customer.
Follow authentication state
Wait until your authentication system has finished loading. Then call identify(user.id) for a
signed-in user, or signOut() once sign-out is confirmed. A loading session is not a signed-out
session; resetting during startup discards the persisted identity before authentication restores it.
Identity changes are ordered by the SDK, including calls during initialization. Repeating
identify() for the same ID without profile changes leaves the session and cached state intact.
Switching directly between accounts does not merge them. Events and queued transactions retain
the identity that captured them, and purchases retain the identity that started them.
No declarative identity prop, additional provider, or app-side identity queue is needed.
Sign out
Sign out of Voidhash after your own sign-out completes:
await voidhash.signOut()This records a sign-out event when automatic lifecycle events are enabled, then starts a fresh
anonymous identity and session. Shared caches and pending deliveries survive. reset() remains
available with the same behavior.
Set customer attributes
Write profile traits for the current person:
try await voidhash.setPersonAttributes([
"email": .string("ada@example.com"),
"name": .string("Ada Lovelace"),
"plan_source": .string("referral"),
])The SDK writes the traits server-side and returns the updated person. When Voidhash cannot confirm
them now, or a sign-in for the same customer is still waiting to be sent, the traits are saved on
the device and sent after it, and the call returns the cached person instead of throwing.
setPersonAttributesState(_:) tells you whether the traits were confirmed or deferred.
Attribute values can be strings, numbers, booleans, or nil. email and name map to built-in
person fields. Other keys become custom traits. A value that is an object or an array throws an
error with the code INVALID_ARGUMENT, online and offline alike, and nothing is saved. A number
that is not finite, such as the result of dividing by zero, is written as nil.
Read the distinct ID
Read the distinct ID of the current identity, anonymous or identified:
let distinctId = await voidhash.getDistinctId()This is useful in support logs, and when you correlate a client session with your backend.
Anonymous or identified?
Which setup fits depends on how your app handles accounts.
- No accounts. Keep the automatically generated anonymous identity.
- Optional accounts. Let purchases work anonymously, then identify after sign-in.
- Account required. Identify immediately after restoring your app session.