Skip to content

Identify

Sessions are anonymous by design. When the visitor tells you who they are - signs in, subscribes, checks out - you can say so, and the session is linked to a contact.

signals.identify('[email protected]', { first_name: 'Jane', company: 'Acme' });

At least one of email or external_id is required. Returns a promise resolving to { success, contact_id, linked_to_session } or { success: false, error }.

  1. The contact is found by external_id if you sent one, otherwise by email (most recently seen first). If nothing matches, a contact is created.
  2. Traits you sent are written onto the contact. Existing values are updated; keys you did not send are left alone.
  3. The session gets the contact’s id, once. A session that is already linked to a contact keeps that link; a second identify with a different person updates the second person’s contact but does not re-link the session.
  4. Two events are recorded on the session: contact (created or updated, on every call) and identify (the first time the session is linked).

Calling identify twice with the same payload is a no-op the second time.

Traits are snake_case and map onto contact fields. Anything else goes under meta.

TraitNotes
first_name, last_name
phone, company, job_title, website
country, cityDefault from the session’s geo when not supplied
lifecycle_stageFree string. The app’s own pickers offer lead, subscriber, customer, user. Defaults to user on create.
metaObject of arbitrary key/value pairs, merged into the contact’s meta
signals.identify('[email protected]', {
first_name: 'Jane',
lifecycle_stage: 'customer',
meta: { plan: 'pro', seats: 5 },
});

Top-level keys that are not in the table are dropped. Put them in meta.

A browser sending Sec-GPC: 1 is never identified. The server answers { success: true, contact_id: null, linked_to_session: false, gpc: true } so your code sees a quiet no-op, the session stays anonymous, and analytics continue. See Privacy Controls.

Identified people are listed under Contacts in the property, with their sessions, lifecycle stage and state. Deleting a contact redacts it in place so the sessions it touched keep working. The full model, including the API and Automations paths for creating contacts, is on Contacts.

If sign-in happens on your backend, identify from there instead of shipping the email through the page:

  • POST /v2/property/contacts/identify on the API, with the session id from Session Handoff.
  • The PHP SDK can identify automatically on Laravel’s Login event.

Server-side identify accepts the same traits and follows the same set-once rule. It also works without a session id, in which case the contact is created or updated but nothing is linked.