Skip to content
Pathbound DOCS

Events — Event types

The snippet fires events automatically with no configuration. Each event is wrapped in a standard envelope on the wire and lands in your tenant’s events collection ready for GET /v1/events.

Every event arrives at the backend with this shape (the snippet batches up to 10 events per request):

{
"client_id": "3f2b8c1e-9a4d-4e7f-8b21-6c0d5e9fa713",
"event": "page_view",
"data": { /* event-specific payload */ },
"timestamp": "2026-04-29T11:23:45.000Z",
"url": "https://example.com/pricing",
"domain": "example.com",
"visitorId": "vis_abc",
"sessionId": "sess_xyz",
"external_contact_id": null,
"ga_user_id": "GA1.1.1234.5678",
"userAgent": "Mozilla/5.0 …",
"referrer": "https://google.com/",
"screenResolution": "1920x1080"
}

client_id is the snippet’s own ID for the event, the same every time its batch is sent, so a batch that arrives twice is stored once. It isn’t returned by GET /v1/events.

The backend derives extra fields from the userAgent, referrer and url every event already carries — no extra snippet config. They’re returned alongside each event by GET /v1/events and GET /v1/events/:id, by the agent tools that read those (list_events with fields="full", get_event), and platform, os and device_type are in the webhook payload:

  • browser, browser_version, os, device_type (Desktop / Mobile / Tablet) — derived from userAgent.

  • platform (web / ios / android) — where the event was captured: a browser, or a native app’s webview. It answers what os can’t: Safari on an iPhone and your iOS app both report os: "iOS", and only the app is platform: "ios". Derived from the event’s url and userAgent:

    • ios — the page runs from a capacitor:// URL, which is how Capacitor serves an iOS app. This holds on an iPad too, where the webview’s userAgent is a desktop Mac’s.
    • android — the userAgent is an Android WebView: the page is running inside an Android app. The in-app browsers we recognize (Facebook, Instagram, Threads, LinkedIn, X, TikTok, Snapchat, Pinterest, Line, WeChat, KakaoTalk, Naver, Bing and the Google app) stay web. Any other app’s webview reports android, so a page of yours embedded in someone else’s Android app counts too.
    • web — everything else, phone browsers included. An iOS app that loads its pages over https:// also reports web.

    Events that did not come from the snippet (integration events, and custom events sent through the API) have no platform. Snippet events stored before the field was introduced have been filled in with the same rule.

  • referring_domain — the host of referrer, or "direct" when there’s no referrer.

  • is_bot — present (and true) only when the userAgent matches a known bot/crawler. Bot events are flagged, not dropped, so you can include or exclude them at read time (filter is_bot == true to find them, or is_bot != true to exclude). Absent on human traffic.

device_type, browser, os, platform, referring_domain, and is_bot — along with visitor_id, session_id, and the element data fields (text, element_type, element_id, class) — are available as group_by dimensions and filters in aggregate_data and GET /v1/events (e.g. event counts by device type, or distinct visitors per button label).

The first referrer / referring domain / landing page / UTMs seen for a visitor are also persisted to their contact as first-touch attribution — creation_referrer, creation_referring_domain, creation_landing_page, creation_utms (surfaced grouped under first_touch by get_contact_details). They are written once and never overwritten by later visits.

Values are stored as the snippet sent them. Nothing is rewritten or re-encoded. Only these rules apply on the way in:

  • Keys starting with $ and the keys __proto__, constructor and prototype are dropped, and control characters are removed from keys.
  • Credential and email values inside captured URLs are replaced with [redacted] (see Privacy).
  • Size caps: url and referrer 2,048 characters, userAgent 2,000, strings inside data 1,000; at most 50 keys per object and 100 items per array; objects nested more than 5 levels deep inside data become null.

Because values arrive from browsers, treat them as untrusted input: escape them when you render HTML, and check a url’s scheme before you use it as a link.

The fields below describe the data payload for each event.

Fires on the initial document load and on every client-side route change. For single-page apps the snippet patches history.pushState/replaceState and listens for popstate/hashchange; a route counts as a new view when pathname + search + hash changes (an identical repeated URL is ignored).

  • navigationType is "load" on the first paint and "spa" for in-app navigations.
  • previousPath is present on "spa" views and holds the path the visitor came from.
  • url is the full URL of the page the view is for, query string included (credential and email values are masked; see Privacy).
  • utmParams also captures ad-click IDs when present in the URL: gclid, fbclid, msclkid, li_fat_id, ttclid, gbraid, wbraid. The first set seen for a visitor is persisted to their contact as first-touch attribution.
{
"title": "Pricing — Example",
"path": "/pricing",
"url": "https://example.com/pricing?plan=team",
"navigationType": "spa",
"previousPath": "/",
"utmParams": {
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "spring-2026"
},
"userAgent": "…",
"referrer": "…",
"screenResolution": "1920x1080",
"pageLoadTime": 412
}

On the very first load this event carries navigationType: "load" and no previousPath.

Fires for the page a visitor is leaving — on each SPA route change (for the page being navigated away from) and once on exit (pagehide / beforeunload). timeOnPage is the dwell time on that page in milliseconds. url and path always describe the page being left, never the next one.

{
"title": "Pricing — Example",
"path": "/pricing",
"url": "https://example.com/pricing?plan=team",
"timeOnPage": 8423
}

Fires once per browser, the first time the snippet assigns a fresh pathbound_visitor_id (i.e. no recoverable visitor existed for the device fingerprint).

{
"landingPage": "https://example.com/pricing",
"landingPath": "/pricing",
"initialReferrer": "https://google.com/",
"utmParams": { /* same as page_view */ },
"title": "Pricing — Example",
"userAgent": "…",
"screenResolution": "1920x1080",
"pageLoadTime": 410
}

Fires when a click resolves to a <button>, <input type="submit|button">, or any element with role="button" (including <a role="button"> — an explicit role wins over the tag) — including clicks that land on a child element (an icon or <span> inside the button). The captured text/class/id are the actionable element’s, not the child’s.

{
"text": "Get a demo",
"class": "btn btn-primary",
"id": "demo-cta",
"elementType": "button"
}

Filter and aggregate these interactions by any data field via GET /v1/events and aggregate_data: text (exact) / text_contains, element_type, element_id, and class_contains. For elements with no visible text — icon buttons, where text and often id are empty — class_contains with a stable class token (e.g. MuiIconButton-root, not a hashed css-* / jss* token) is the way to target them. To list the contacts who clicked a given element, use the matching has_event_* filters on GET /v1/contacts.

Fires when a click resolves to an <a> (without role="button") or any element with role="link", including clicks on a child element inside the link. isExternal is true if the link has an href whose hostname differs from the current page’s hostname.

{
"text": "Read the docs",
"href": "https://pathbound.ai/docs",
"class": "footer-link",
"id": "",
"elementType": "link",
"isExternal": true
}

Fires on every native <form> submit. Form field values are not captured here — post the form to the public form endpoint for that. This event is the snippet noticing a submit; it is not form_submission, which the API writes when a submission is stored.

{
"formId": "demo-form",
"formClass": "contact-form",
"action": "/submit",
"method": "post"
}

Fires when your code calls window.pathbound.identify(externalId, traits) — the only tracked event you trigger explicitly. It marks the login moment on the visitor’s timeline and carries the asserted identity; the actual contact create-or-link happens via the public identify endpoint the same call makes.

{
"external_contact_id": "usr_8f3a2c9e-…",
"traits": {
"email": "ada@example.com"
}
}

traits is present only when passed. Calls with placeholder ids ("undefined", "null", "anonymous", …) are ignored client-side and fire nothing.

Auto-detected when a <wistia-player> embed is on the page (or added later via DOM mutation).

Fires when playback begins or resumes.

{
"provider": "wistia",
"mediaId": "abc123def",
"videoName": "Product overview",
"duration": 187,
"currentTime": 0,
"pageTitle": "Home — Example",
"pagePath": "/",
"wistiaVisitorKey": "v1-…"
}

Fires on pause, including when the user navigates away.

{
"provider": "wistia",
"mediaId": "abc123def",
"videoName": "Product overview",
"duration": 187,
"currentTime": 64,
"secondsWatched": 58,
"pageTitle": "Home — Example",
"pagePath": "/"
}

Fires when the video reaches its natural end.

{
"provider": "wistia",
"mediaId": "abc123def",
"videoName": "Product overview",
"duration": 187,
"secondsWatched": 187,
"pageTitle": "Home — Example",
"pagePath": "/"
}

Fires once each at 25%, 50%, 75%, and 100% of duration. The 100% milestone may fire alongside video_complete.

{
"provider": "wistia",
"mediaId": "abc123def",
"videoName": "Product overview",
"milestone": 50,
"currentTime": 94,
"duration": 187,
"pageTitle": "Home — Example",
"pagePath": "/"
}

Fires when an in-video email-capture form is submitted.

{
"provider": "wistia",
"mediaId": "abc123def",
"videoName": "Product overview",
"duration": 187,
"currentTime": 32,
"pageTitle": "Home — Example",
"pagePath": "/",
"conversionType": "email_only",
"email": "ada@example.com",
"firstName": "Ada",
"lastName": "Lovelace"
}

The submitted email is the most reliable identifier the snippet captures from in-page interactions. If the visitor isn’t already linked to a contact, video_conversion is what flips them from anonymous to identified.

Not fired by the snippet. The API writes it when a form posts to the public form endpoint and the submission is stored. It is linked to the contact the submission created or updated, so it shows on that contact’s timeline, and it is delivered to HTTPS event streams like any other event.

{
"form_id": "playbook",
"submission_id": "6703f1c2a4b5c6d7e8f90123",
"contact_id": "6703f1c2a4b5c6d7e8f90456",
"company_id": "6703f1c2a4b5c6d7e8f90789",
"contact_created": true,
"company_created": false,
"source_domain": "www.example.com"
}
  • form_id is the id in the URL the form posted to. submission_id is the id of the stored submission.
  • contact_id and company_id are the records the submission created or updated. Each is missing or null when the submission carried no contact_data or no usable company_data.
  • contact_created and company_created are true when the submission created the record and false when it updated one.
  • source_domain is the host of the page the form posted from.

Those seven keys are the whole payload. It never holds the submitted field values, and it holds no page URL. On the envelope, domain is the same host, and url, visitorId, sessionId, userAgent, referrer, screenResolution and platform are absent, because no browser event was recorded.

The event is written only for a form id that fits the form id rule. An email automation is triggered by the stored submission, never by this event, so an event with this name that arrives another way sends nothing.

Internal — fired when the snippet computes a fresh device fingerprint. Used by the server to maintain the fingerprint → visitor ID mapping. Most consumers can ignore this event type when reading /v1/events.