Skip to main content

Anonymous by default

From first launch the SDK mints a stable anonymous id (anon_ followed by 21 random characters) and stores it on the device. Everything works before anyone logs in: events queue and send, sessions are tracked, and the push subscription registers under that id. Nothing about the SDK requires a logged-in user. When you later call identify, the anonymous subscriber becomes the identified one. The push subscription, the topic preferences, the attributes and the whole event history come with it, so the events someone generated during onboarding are still on their timeline once they sign up. See Merging an anonymous user.

Identify

The signature is identify(_ externalId: String, email: String? = nil, identityHash: String? = nil, attributes: [String: JSONValue]? = nil, subscribe: [Channel: Bool] = [:]). It returns immediately and does its work on a serial queue, so identity calls never race each other. Four things happen. When the device was anonymous, that anonymous subscriber is merged into the identified one. The subscriber is then created or updated with the id, email and attributes you passed, along with the device context. If the identity actually changed, the queued events are flushed and the push subscription is re-registered under the new id. The email is saved on the subscriber as the email attribute whether you pass it as email: or inside attributes, and it is saved even before an email provider is connected to the tenant. Once one is, identify also subscribes the address, so the user can be emailed without another call. Pass subscribe: [.email: false] to keep an address on file without subscribing it, for example while it is still unverified. Call it at login, and on every launch where the user is already signed in. It is an idempotent upsert on your own id, so calling it repeatedly costs nothing.
externalId is your own user id. It is the same id your backend addresses in PUT /v1/subscribers/:externalId and in to on a send, which is what keeps the app and your server talking about the same person. See Subscribers.

Merging an anonymous user

The SDK remembers the anonymous id it minted and sends it as anonymousId on the first identify after a device was anonymous. The API merges that subscriber into the identified one, so signing up never loses what came before. The merge takes one of two shapes. When the id you identify with is new, which is what a first sign-up does, the subscriber is simply renamed: it keeps its row, its id, its actor and its event sequence, so nothing moves at all and it is the same subscriber under a new name. When that id already exists, for example a second device or a user signing back in, there are genuinely two subscribers, so the anonymous one is absorbed into the identified one: its subscriptions and preferences move, its retained events are copied onto the identified timeline, and it is retired. Either way a $subscriber.merged event lands on the timeline carrying the id it merged from, and the anonymous id is kept as an alias of the identified subscriber. Nothing is thrown away: GET /v1/subscribers/anon_9RmZ… still answers with the person that device turned out to be, and a client that is still holding the old id reaches the right subscriber instead of creating a new one.
Calling identify again with the same anonymous id does nothing the second time, so retries and every-launch calls are safe. Only an anonymous, never-verified subscriber can be merged; a request naming an identified subscriber is refused with merge_source_identified.
Absorbed events keep their original timestamps but join the end of the identified subscriber’s sequence, so they appear on the timeline at the point of the merge rather than interleaved by date. The rename path, which is what a first sign-up does, has no such seam.

Attributes

Attributes are custom key-values on the subscriber and they drive segments and workflow conditions. They take [String: JSONValue], and JSONValue is expressible by string, integer, float, boolean, nil, array and dictionary literals, so ordinary Swift literals work without any wrapping. The server merges what the device sends into what is already on the subscriber. Adding and changing keys from the app is safe, and nothing your backend set is wiped by a call from the device. The SDK maintains one attribute of its own, the device’s notification permission, refreshed on launch and on every change. Segments read it like any other attribute.

Logout

Logout does three things in order. It deletes this device’s push subscription for the user who is signing out, so the next person to use the phone never receives the previous user’s notifications. It clears the stored external id and identity hash and generates a brand new anonymous id, not the one used before the first login. Then it re-registers the device under that fresh anonymous identity, so push still works for whoever is holding the phone now.

Identity verification

A client key is embedded in your binary, so on its own it lets any caller claim any external id. Verification closes that: your backend computes an HMAC of the user’s id under the tenant’s identity secret and hands the result to the app at login.
Fetch the identity secret once from the tenant’s settings in the dashboard and keep it on your server. It must never ship in the app.
Pass the hash to identify and the subscriber is marked verified. The SDK stores it and attaches it to every call it makes from then on, including events, subscription updates and preference changes, for as long as the user stays identified. logout discards it along with the id.
Turning on required verification for a tenant rejects every unverified client call outright. Ship the hash from every client first and confirm your subscribers are showing as verified, then enforce it. Tenant settings and the identity secret are covered under Tenants.