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, email and name as the last time sends no request and changes nothing, also
after a relaunch. A changed email or name is sent.
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 tell the SDK what you found, on every
launch and whenever the session changes: identify when there is a user, signOut() when there is
none. Signing out while your session is still loading discards the identity before authentication
restores it.
func sync(signedInUserId: String?) async throws {
guard let voidhash = Voidhash.shared else { return }
if let userId = signedInUserId {
try await voidhash.identify(externalUserId: userId)
} else {
await voidhash.signOut()
}
}You don't need to remember what you called before. A repeated identify sends nothing, and
signOut() does nothing while the identity is anonymous, so an anonymous buyer keeps the identity
that holds their purchases. If your users belong to companies or workspaces, pass the full set to
setGroups in the same place.
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. While the identity is anonymous,
signOut() does nothing: no event, no new anonymous ID, and the groups stay.
reset() does exactly the same for a signed-in user. The two differ only when nobody is signed
in: signOut() does nothing, and reset() starts a fresh anonymous identity, with a new ID, no groups
and a new session, without recording $sign_out. Use reset() on a shared device, for a guest's
"start over", and in tests.
A consent that identify granted with
grantConsentOnIdentify lasts as long as the
sign-in: both calls return it to .pending.
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, as strings, set the person's
own email and name, as identify does; 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 whether
the write is confirmed or deferred. Attributes are sent whatever the
analytics consent, as identify is. The one exception is
agreeing to ad data sharing, which needs consent: a write holding nothing else returns skipped.
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.