Identity
Associate tracked events with known users and stitch anonymous visitor sessions to real identities.
identify()
Call identify() when you learn who a visitor is — at login, signup, or form submission. All subsequent track() calls are attributed to this user.
import { identify } from "@apex-inc/sdk";
identify("user_8x92k", { email: "jane@acme.co", plan: "pro" });
| Parameter | Type | Description |
|---|---|---|
userIdrequired | string | Your stable, unique user id — a database ID or your auth provider's id (Cognito sub, Auth0 user_id, Firebase uid, Supabase id). An email works but is weaker (emails change and get shared). |
traits | Record<string, unknown> | Key-value pairs describing the user — email, name, plan, company, etc. Persisted on the Contact so segments can target them. |
traits.visitorId | string | The browser's apex_vid cookie value. REQUIRED from a backend to stitch the anonymous visitor to this user — without it the server can't link the pre-login session. In the browser the SDK reads it automatically; on the server, pass it explicitly (e.g. req.cookies['apex_vid']). |
Tip
Always identify with your own user id, not just an email. Apex treats the
userId you pass as the highest-precedence identifier (internal_user_id,
limit one per Contact) — it's what makes anonymous → known reconciliation
deterministic. When you call identify("cognito-sub-123", { email }), the
SDK forwards cognito-sub-123 as your internal_user_id and email as a
secondary identifier. See Connect your users for
per-provider recipes (Cognito, Auth0, Firebase, Supabase, generic).
Profile attributes
Traits map onto the canonical Apex Spec, so a Contact gets a clean profile you can use across segments, templates, and the dashboard. One high-value attribute is avatar_url — a profile photo URL. When you send it, the customer's photo appears in the Customers list, on their detail page, and in the Live Customers widget. (Apex validates it on write — https URLs only — and falls back to colored initials when it's absent, so the UI always looks complete.)
identify(user.id, {
email: user.email,
name: user.name,
avatar_url: user.photoURL, // photoUrl / image / profile_image also map
});
Tip
If you authenticate with Cognito, Auth0, Firebase, or Supabase, you already
have a profile photo — the OIDC picture claim (photoURL in Firebase). Map
it to avatar_url in your identity connector and
end-user photos appear across Apex with no extra work.
B2B accounts and groups
If you sell to companies, revenue belongs to the account (the company), not the
individual who happened to hold the card. Pass a nested account object on
identify() and Apex creates one Account, links the user as a member, and rolls
every member's revenue into a single account-level LTV — no double counting.
identify(user.id, {
email: user.email,
account: {
id: "acct_acme", // stable company id in your app (required)
name: "Acme Inc",
plan: "enterprise",
seat_count: 25,
domain: "acme.inc",
},
});
Many B2B products also have a layer between the company and the person —
workspaces, teams, projects. Assert those with groups[]. Each group becomes a
sub-account of its parent_id (defaulting to the primary account.id), and the
user is linked to it, so "which workspaces is this user in?" is one lookup.
identify(user.id, {
email: user.email,
account: { id: "acct_acme", name: "Acme Inc" },
groups: [
{ id: "ws_prod", name: "Production", kind: "workspace", role: "admin" },
{ id: "team_growth", name: "Growth Team", kind: "team", parent_id: "acct_acme" },
],
});
| Parameter | Type | Description |
|---|---|---|
account.idrequired | string | Stable company/org id in your app. Required for account rollup — Apex never infers it from a shared email domain. |
account.* | traits | name, domain, plan, seat_count, industry, owner_email, mrr, arr — see the Apex Spec account attributes. |
groups[].idrequired | string | Stable group id (workspace/team/project id in your app). |
groups[].kind | string | Vocab label: "workspace", "team", "project". Drives the section heading in the dashboard. |
groups[].parent_id | string | External id of the parent account/group. Defaults to the identify's account.id. A group only attaches under an account the user is already anchored to. |
groups[].role | string | The user's role in the group (owner / admin / member). |
Info
Account and group membership only comes from a verified identify() (a
real login/signup on your server or the browser SDK) — never from an anonymous
auto-stitch. Revenue always lands on the billed account; a parent account's LTV
is its own counters, not the sum of its children.
Read it back programmatically with the management client
— apex.accounts.list(), apex.accounts.get(id), apex.accounts.subaccounts(id),
apex.accounts.people(id), and apex.accounts.contactGroups(contactId) — or via
the MCP tools list_accounts, list_sub_accounts, and get_contact_groups.
Identity Stitching
When the userId looks like an email address (contains @), the SDK automatically calls POST /api/identity/stitch to merge the anonymous visitor session with the known user profile. This also triggers if traits.email is set.
This means every event the visitor triggered before identifying themselves — pageviews, clicks, experiment exposures — gets linked to the real user.
Info
Identity stitching is automatic. You don't need to call a separate API.
Just pass an email as the userId, or include email in the traits object.
Anonymous → Known User Flow
Here's the typical lifecycle:
- Visitor lands on your site — Apex assigns an anonymous visitor ID via the tracking snippet or SDK
- Visitor browses pages —
track("page_view", ...)calls are recorded under the anonymous ID - Visitor submits a form or logs in — you call
identify("jane@acme.co") - Apex stitches — all prior anonymous events are now attributed to
jane@acme.co - Future events — every
track()call automatically includes the identified user
Examples
At Login
async function onLogin(credentials) {
const user = await api.login(credentials);
identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan,
});
}
At Signup
async function onSignup(formData) {
const user = await api.createAccount(formData);
identify(user.email, {
name: formData.name,
company: formData.company,
source: "signup",
});
}
With trackForm()
The trackForm() helper calls identify() automatically, so a single call handles both the event and the identity:
import { trackForm } from "@apex-inc/sdk";
trackForm({
email: "jane@acme.co",
formId: "contact-us",
fields: { message: "Interested in the enterprise plan" },
});
Warning
Call init() before identify(). If the SDK isn't initialized, the call is
ignored and a warning is logged to the console.
Tip
You can call identify() multiple times. The SDK uses the most recent
userId. Traits are sent with each call, so include any updated values.
Next Steps
- Track events — record pageviews, clicks, and conversions
- Installation reference — configuration options and environment setup
- Quickstart — end-to-end setup in under 5 minutes