pex

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" });
ParameterTypeDescription
userIdrequiredstringYour 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).
traitsRecord<string, unknown>Key-value pairs describing the user — email, name, plan, company, etc. Persisted on the Contact so segments can target them.
traits.visitorIdstringThe 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" },
  ],
});
ParameterTypeDescription
account.idrequiredstringStable company/org id in your app. Required for account rollup — Apex never infers it from a shared email domain.
account.*traitsname, domain, plan, seat_count, industry, owner_email, mrr, arr — see the Apex Spec account attributes.
groups[].idrequiredstringStable group id (workspace/team/project id in your app).
groups[].kindstringVocab label: "workspace", "team", "project". Drives the section heading in the dashboard.
groups[].parent_idstringExternal 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[].rolestringThe 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 clientapex.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:

  1. Visitor lands on your site — Apex assigns an anonymous visitor ID via the tracking snippet or SDK
  2. Visitor browses pagestrack("page_view", ...) calls are recorded under the anonymous ID
  3. Visitor submits a form or logs in — you call identify("jane@acme.co")
  4. Apex stitches — all prior anonymous events are now attributed to jane@acme.co
  5. 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