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:

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. Voidhash stores the ID exactly as you pass it. An ID Voidhash cannot store throws a VoidhashException with the code INVALID_ARGUMENT and leaves the current identity unchanged, whether or not the device is online: an empty or whitespace-only ID, one longer than 255 characters, one containing a NUL character, or one starting with vh:anon:, the prefix of anonymous IDs.

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.

The switch happens on the device immediately. If Voidhash cannot be reached, returns an unreadable answer, or rejects the publishable key, identify() does not throw. It returns the person the SDK knows about and delivers the sign-in later, including after the app restarts. Until Voidhash confirms it, a customer who bought while anonymous keeps the access cached for the anonymous identity, marked stale. Use identifyWithStatus() to learn whether Voidhash has the sign-in yet:

when (voidhash.identifyWithStatus(user.id).status) {
    WriteStatus.CONFIRMED -> Unit // Voidhash has linked the identities
    WriteStatus.DEFERRED -> Unit  // applied on the device, delivered once Voidhash is reachable
}

identify() throws only when Voidhash refuses the sign-in itself: a 400 for an invalid request or a 404 for an unknown target. The SDK then restores the previous identity. See Errors.

When the anonymous identity already belongs to another account, Voidhash answers 409 and cannot merge it. identify() does not throw in that case. The device switches to the customer you identified, without the anonymous identity's history and purchases, which stay with the other account, and the SDK reports a PERSON_WRITE_REJECTED diagnostic. A sign-in delivered later behaves the same way.

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. A sign-in that is waiting on the network never delays initialize() or signOut(). 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.

If you already know the customer when you configure the SDK, you can pass their ID as the distinctId option instead of waiting to call identify(). When the device used an anonymous identity before, initialize() links it to that ID, so earlier purchases carry over. A distinctId that identify() would refuse, such as a blank one, is ignored.

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:

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:

voidhash.setPersonAttributes(
    mapOf(
        "email" to "ada@example.com",
        "name" to "Ada Lovelace",
        "plan_source" to "referral",
    )
)

Attribute values can be strings, numbers, booleans, or null. A map or a list throws a VoidhashException with the code INVALID_ARGUMENT before anything is sent, whether or not the device is online. Non-finite numbers are sent as null. An empty map sends nothing. email and name map to built-in person fields. Other keys become custom traits.

Like identify(), attribute writes never fail because Voidhash cannot be reached. They are queued and delivered later. Attributes set right after a sign-in that is still queued wait for it, so they always land on the identified person.

Read the distinct ID

Read the distinct ID of the current identity, anonymous or identified:

val distinctId = 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.

Next steps