Identify users
Link a device's anonymous history to your user IDs, set attributes, and sign out.
The SDK starts every install with an anonymous distinct ID, so purchases and events work before
anyone signs in. Call identify after sign-in to link the device to your user ID; purchases and
access then follow the person across devices and platforms. Distinct IDs, merging and attributes
are explained in People and identity.
The anonymous ID has the form vh_anon_<uuid> and is stored in UserDefaults, so it doesn't
survive reinstalling the app. A user who bought anonymously gets access back on a new install by
restoring purchases.
Identify after sign-in
Pass your stable user ID, and optionally an email and name:
try await voidhash.identify(externalUserId: "user_123", email: "ada@example.com", name: "Ada")Use an opaque, unique ID such as a UUID, not an email address or a sequential database ID. The ID
must contain a non-whitespace character, be at most 255 characters long, contain no NUL character,
and not start with vh_anon_ or vh:anon:. Otherwise identify throws INVALID_ARGUMENT and
keeps the current identity.
When an anonymous person signs in, Voidhash moves their purchases and entitlements to the identified
person. Events captured before the switch stay on the anonymous distinct ID. Calling identify
again with the same ID and no profile changes does nothing.
Offline and failures
The identity switches on the device at once, whether or not Voidhash is reachable. The SDK saves the link before sending it and keeps sending it until Voidhash applies it, across outages and launches. Queued links go before queued purchases, so a purchase made anonymously is recorded after the merge.
identify waits up to 2 seconds for Voidhash. Until Voidhash confirms the link, the person and
feature flags keep what the anonymous person had, marked stale, so a user who bought before signing
in keeps access. identifyState returns whether the link is confirmed or still deferred:
let result = try await voidhash.identifyState(externalUserId: "user_123")
if result.status == .deferred {
// Signed in on this device; Voidhash applies the link once it's reachable.
}identify throws only when Voidhash rejects the link for good within those 2 seconds; the SDK then
restores the previous identity. It doesn't throw for network failures or a rejected publishable key.
A rejection that arrives later keeps the user signed in, drops the link, and reports the
PERSON_WRITE_REJECTED diagnostic.
When the anonymous ID already belongs to another identified person, for example after restoring an
old device backup, identify doesn't throw either. The user is signed in as the new ID without the
anonymous ID's history, which stays with the other person, and the SDK reports
PERSON_WRITE_REJECTED.
Start with a known ID
If you know the user when the app starts, pass the ID as options.distinctId instead of calling
identify. The SDK adopts it without a request and links an anonymous ID the device used before in
the background. An invalid distinctId is ignored.
Follow your sign-in state
Wait until your authentication has finished loading, then identify a signed-in user or sign out a
signed-out one. Signing out while your session is still loading discards the identity before
authentication restores it. Sign out only an identified user: signOut() always starts a new
anonymous identity, which would leave an anonymous buyer's purchases behind.
func sync(signedInUserId: String?) async throws {
guard let voidhash = Voidhash.shared else { return }
if let userId = signedInUserId {
try await voidhash.identify(externalUserId: userId)
} else if !Voidhash.isAnonymousDistinctId(await voidhash.getDistinctId()) {
await voidhash.signOut()
}
}Switching directly from one user ID to another doesn't merge them. Events and queued purchases keep the identity they were captured under.
Sign out
Call signOut() after your own sign-out completes:
await voidhash.signOut()It records $sign_out (when lifecycle events and analytics consent are on), then starts a new
anonymous identity and analytics session, outside the user's groups. Queued
deliveries, analytics consent and the install record survive. reset() does the same.
Set person attributes
try await voidhash.setPersonAttributes([
"email": .string("ada@example.com"),
"plan_source": .string("referral"),
"seats": .number(3),
])Values can be strings, numbers, booleans or nil. email and name set the built-in fields; other
keys become custom attributes. An object or array value throws INVALID_ARGUMENT and nothing is
saved. A number that isn't finite is written as nil.
The call waits up to 2 seconds. When Voidhash can't confirm in time, the attributes are saved on the
device and sent later, and the call returns the cached person. Attributes Voidhash refuses later are
dropped with the PERSON_WRITE_REJECTED diagnostic. setPersonAttributesState(_:) returns
confirmed, deferred, or skipped when the user hasn't granted
analytics consent: attributes need it.
Read the distinct ID
let distinctId = await voidhash.getDistinctId()Use it in support logs, or to correlate a session with your backend. Voidhash.isAnonymousDistinctId(_:)
tells you whether it's anonymous; don't check the prefix yourself.
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.