Skip to content
Pathbound DOCS

Tracker — Identity & fingerprinting

The tracker resolves “is this the same visitor I saw before?” through three layers, each more durable than the last:

  1. The pathbound_visitor_id cookie.
  2. Device fingerprint recovery when the cookie is missing.
  3. External contact ID when your application has authenticated the user.

Plus a session cookie that scopes events to a single browsing session, and a Google Analytics user ID capture that lets you join Pathbound events with GA reports.

CookieLifetimeScopePurpose
pathbound_visitor_id365 dayspath=/; SameSite=Strict; SecureStable per-browser identifier.
pathbound_session_id1 day, slidingpath=/; SameSite=Strict; Secure; HttpOnlyActive browsing session. Refreshes on activity.
_sellably_visitor_id (mirror)365 dayssame as visitor_idBackward-compat duplicate (legacy SaaS name).

The session cookie is HttpOnly so it’s not readable from JS; the visitor cookie is intentionally JS-readable so SPA code can fire identified events.

Cookies the tracker reads (but does not set)

Section titled “Cookies the tracker reads (but does not set)”
CookiePurpose
pathbound_external_contact_idSet by your application when the user has authenticated. The tracker forwards this on every event so the backend can resolve it to a Pathbound contact.
_sellably_external_contact_idBackward-compat fallback for the same.
_gaGoogle Analytics client ID — captured into the event as ga_user_id so you can correlate Pathbound events with GA.
pathbound_dntIf =1, the tracker exits at the very top of the script and does nothing.

A session is a single browsing window. The session cookie stores <id>|<last-activity-ms>. On every event:

  • If the cookie is missing, a new session id is minted and a 1-day cookie is set.
  • If the last activity was more than 30 minutes ago, the old session ends and a new one begins.
  • Otherwise the cookie’s last-activity timestamp is bumped, extending the session.

This is the same heuristic GA4 uses: a session is what a human would call a “visit,” not what the cookie engine treats as a single connection.

When a visitor arrives without a pathbound_visitor_id cookie, the tracker attempts to recover their canonical visitor ID from the device fingerprint before minting a new one.

The fingerprint is a SHA-256 hash of:

  • navigator.userAgent, navigator.language, navigator.platform
  • navigator.cookieEnabled, navigator.doNotTrack, navigator.hardwareConcurrency
  • screen.width × height, screen.colorDepth, window.devicePixelRatio
  • Timezone offset (new Date().getTimezoneOffset())
  • A canvas rendering of “Device fingerprint” text in 14 px Arial — the canonical browser-fingerprint canvas trick
  • The WebGL UNMASKED_VENDOR + UNMASKED_RENDERER strings (when WEBGL_debug_renderer_info is exposed)
  • An OfflineAudioContext rendering — sums the absolute amplitudes of the audio output, which depends on the device’s audio stack

If the browser doesn’t support crypto.subtle.digest, the tracker falls back to a 64-bit DJB2-style hash.

  1. The tracker computes the fingerprint hash.
  2. Checks if pathbound_visitor_id is already set — if so, ship a _fingerprint_update event to refresh the server-side mapping and continue.
  3. If no cookie, POST /recover-visitor with { fingerprint }.
  4. If the server returns a known visitorId, set it as the cookie.
  5. Otherwise, mint a fresh UUID and treat this as a new visitor — fire first_visit.

This means a user clearing cookies or switching browsers on the same machine generally lands on the same visitor_id, with all of their prior history intact.

  • It does not identify the user by name, email, or any PII — the fingerprint is one-way.
  • It does not persist data outside the cookie + the server-side fingerprint mapping.
  • It does not ship raw fingerprint signals — only the SHA-256 hash leaves the browser.
  • It does not override the pathbound_dnt opt-out — DNT visitors don’t fingerprint.

When your application authenticates a user, set a cookie:

// On login (or any time you know the user's Pathbound contact ID):
document.cookie =
'pathbound_external_contact_id=' + contact.externalId +
'; path=/; max-age=31536000; SameSite=Strict; Secure';

The tracker reads this on every event and includes it in the payload. The backend resolves it against the contact’s stored external_id (or your custom property mapping) and stitches the event timeline together — even across visitor cookies and devices.

This is the most reliable identification mechanism. Use it whenever you can.

When an event arrives, the backend tries to identify the visitor in this order:

  1. external_contact_id in the event payload → matches a Pathbound contact directly.
  2. Form submission email → matches a contact by properties.email, creating one if the form is configured for upsert.
  3. visitor_id that has a prior identified event → joins to that contact via the events collection.
  4. No identification → event is recorded with internal_contact_id: null. It’s still queryable (set identified_only=false in /v1/events), and will retroactively join to a contact on the next identification.

Anonymous events are not lost; they sit waiting until a future identification event reveals who they belonged to all along.