Events — Identity & fingerprinting
The snippet resolves “is this the same visitor I saw before?” through three layers, each more durable than the last:
- The
pathbound_visitor_idcookie. - Device fingerprint recovery when the cookie is missing.
- 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.
Cookies the snippet sets
Section titled “Cookies the snippet sets”| Cookie | Lifetime | Scope | Purpose |
|---|---|---|---|
pathbound_visitor_id | 365 days | path=/; SameSite=Strict; Secure | Stable per-browser identifier. |
pathbound_session_id | 1 day, sliding | path=/; SameSite=Strict; Secure | Active browsing session. Refreshes on activity. |
_sellably_visitor_id (mirror) | 365 days | same as visitor_id | Backward-compat duplicate (legacy SaaS name). |
Both cookies are set client-side by the snippet, so both are readable from JS; the visitor cookie in particular is read by SPA code to fire identified events.
Cookies the snippet reads
Section titled “Cookies the snippet reads”| Cookie | Purpose |
|---|---|
pathbound_external_contact_id | The logged-in user’s external ID. Set by window.pathbound.identify() (or by your application directly); cleared by window.pathbound.reset(). The snippet forwards this on every event so the backend can resolve it to a Pathbound contact. |
_sellably_external_contact_id | Backward-compat fallback for the same. |
_ga | Google Analytics client ID — captured into the event as ga_user_id so you can correlate Pathbound events with GA. |
pathbound_dnt | If =1, the snippet exits at the very top of the script and does nothing. |
Sessions
Section titled “Sessions”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.
Device fingerprinting
Section titled “Device fingerprinting”When a visitor arrives without a pathbound_visitor_id cookie, the snippet 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.platformnavigator.cookieEnabled,navigator.doNotTrack,navigator.hardwareConcurrencyscreen.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
OfflineAudioContextrendering — 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 snippet falls back to a 64-bit DJB2-style hash.
Recovery flow
Section titled “Recovery flow”- The snippet computes the fingerprint hash.
- Checks if
pathbound_visitor_idis already set — if so, ship a_fingerprint_updateevent to refresh the server-side mapping and continue. - If no cookie,
POST /recover-visitorwith{ fingerprint }. - If the server returns a known
visitorId, set it as the cookie. - 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.
What it does NOT do
Section titled “What it does NOT do”- 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_dntopt-out — DNT visitors don’t fingerprint.
External contact IDs
Section titled “External contact IDs”When your application authenticates a user, tell the snippet:
// On login (or any page load where you know who the user is):window.pathbound = window.pathbound || [];window.pathbound.push(['identify', user.externalId, { email: user.email }]);
// On logout:window.pathbound.push(['reset']);identify() sets the pathbound_external_contact_id cookie (and creates or links the contact — see the identify quickstart); reset() clears it and starts a fresh session. If you’d rather manage the cookie yourself, setting it directly still works:
document.cookie = 'pathbound_external_contact_id=' + contact.externalId + '; path=/; max-age=31536000; SameSite=Strict; Secure';The snippet reads this cookie on every event and includes it in the payload. The backend resolves it against the contact’s stored external_contact_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. Placeholder values ("undefined", "null", "anonymous", and similar) are rejected on every path — client and server — so an instrumentation bug can’t fold unrelated visitors into one junk contact.
Choose an unguessable external ID
Section titled “Choose an unguessable external ID”The external_contact_id is a soft identity assertion — the cookie path is not authenticated, so treat the value as a join key, not a secret, and make it opaque and unguessable (a UUID or random token). Never use a sequential integer, an email address, or anything an attacker could enumerate: a logged-in user can edit their own cookie, and a guessable ID lets them attribute their browsing to someone else’s contact. Pathbound never returns contact data to the browser, so a forged ID can’t read another profile — but an unguessable ID closes the write-side (timeline-poisoning) gap entirely.
For a tamper-proof binding — authenticated, trait-carrying, and able to create a profile on the spot — use the server-side POST /v1/identify call. That’s the recommended path; see the Identify logged-in users quickstart for the full setup (cookie + API, logout handling, and the one-device-one-contact rule).
Identification cascade
Section titled “Identification cascade”When an event arrives, the backend tries to identify the visitor in this order:
external_contact_idin the event payload → matches a Pathbound contact directly.- Form submission email → matches a contact by
properties.email, creating one if no contact matches. visitor_idthat has a prior identified event → joins to that contact via the events collection.- No identification → event is recorded with
internal_contact_id: null. It’s still queryable (setidentified_only=falsein/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.
Two guard rules apply throughout the cascade:
- One device → one contact. A
visitor_idbinds to at most one contact; a second user’s bind on a shared browser is refused (and logged), never forked. - Identified contacts are never re-keyed implicitly. If an attribute match (e.g. by email) lands on a contact that already carries a different
external_contact_id, the mapping is refused and logged (/v1/identifyreturns409). Re-keying is an explicit contact update, not an ingestion side effect.