Custom Events

The Apex snippet tracks several event types automatically and exposes an API for sending custom events. All events are sent to the events endpoint as JSON, using navigator.sendBeacon for reliable delivery during page unload.

POST/api/events

Receives all tracking events from the snippet

Accepts a JSON body with workspace key, event type, visitor ID, URL, timestamp, and a data object containing event-specific fields plus attribution context.

Automatic Events

These events are sent by the snippet without any code on your part:

Event TypeWhen It FiresKey Data
pageviewEvery page loadreferrer, isLanding, experiment assignments
form_submitAny <form> submission (not on product paths)formAction, email (unless form reading is off), assignments, attribution
engagementVisitor leaves the pageduration (seconds), scrollDepth (0–100%)
heartbeatOn page load, then every 30 seconds while the page is in view (you can choose 15 seconds to 5 minutes)snippetVersion, experiment and form status, screen/viewport size
clickOutbound link clickhref, link text (not on product pages)
goal_conversionExperiment conversion metgoalType, experimentId, variant, conversion-specific fields
rage_click3+ rapid clicks in same areax, y, selector, clicks
dead_clickClick with no URL change and no mutation on the clicked node after 500msx, y, selector
js_errorUnhandled JS error or rejection, up to 10 different errors per pagemessage, source, line, col

Choosing what's recorded

In Settings → Website snippet you can change:

  • Heartbeat: every 15 seconds, 30 seconds (the default), 1, 2 or 5 minutes. A heartbeat says the visitor is still there, so more often keeps your live view closer to who is on the site right now. Each heartbeat is an event on your bill.
  • Pick up names and emails from forms: on by default. On, the person Apex creates also keeps the other fields they filled in (labelled the way your form labels them), shown on their page and in your team's new-person email. Passwords, card and bank numbers, one-time codes, captchas and hidden fields are never read. Off, a form_submit still arrives (so conversions and tests keep counting) but carries nothing the visitor typed, and nobody is identified from it. Product paths are never read either way.
  • Clicks: outbound link clicks, rage clicks and dead clicks. Events you send with apex.track() are not affected.
  • JavaScript errors: error reports never count toward your bill.

Changes reach visitors on their next page load. Agents can set the same things with the set_snippet_capture tool.

Sending Custom Events

Use the global window.apex.track() method to send your own events:

window.apex.track("signup_started", {
  plan: "pro",
  source: "pricing_page"
});

The snippet automatically attaches the visitor ID, workspace key, current URL, timestamp, and attribution data. You only need to provide the event name and any custom properties.

Common Examples

// Video engagement
window.apex.track("video_played", {
  videoId: "intro-demo",
  duration: 45
});

// Pricing interaction
window.apex.track("pricing_toggle", {
  interval: "annual",
  plan: "growth"
});

// Feature activation
window.apex.track("feature_used", {
  feature: "export_csv",
  firstTime: true
});

Event Payload Format

Every event sent to /api/events follows this structure:

{
  "workspaceKey": "your-workspace-key",
  "type": "pageview",
  "visitorId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "url": "https://yoursite.com/pricing",
  "timestamp": "2025-07-15T10:30:00.000Z",
  "data": {
    "referrer": "https://google.com",
    "isLanding": true,
    "assignments": [
      {
        "experimentId": "exp-hero-copy",
        "variant": "variant_b",
        "selectorFound": true,
        "contentMatch": true
      }
    ],
    "attribution": {
      "utm_source": "google",
      "utm_medium": "cpc",
      "utm_campaign": "brand_q3",
      "gclid": "CjwKCAjw..."
    }
  }
}

The data.attribution object is automatically injected from the apex_attr cookie on every event, so your custom events always carry full attribution context. Every event also carries data.sessionAttribution — the same shape, but scoped to the current visit, which is what traffic reports group on.

Conversions the snippet watches

When you define a conversion on an experiment, the snippet tracks it automatically. The wire event is still named goal_conversion:

{
  "type": "goal_conversion",
  "data": {
    "goalType": "click",
    "selector": ".cta-primary",
    "label": "Primary CTA Click",
    "experimentId": "exp-hero-copy",
    "variant": "variant_b",
    "assignments": [{ "experimentId": "exp-hero-copy", "variant": "variant_b" }]
  }
}

Conversion types and their extra fields:

TypeExtra fields
clickselector, label
pageviewtargetUrl, label
file_downloadfileExtension, fileName, label
engaged_timeengagedSeconds, thresholdSeconds, label

Behavioral Events

The snippet captures UX signals automatically to surface usability issues:

Rage Clicks

Detected when a visitor clicks the same area 3 or more times within 800ms — a strong signal of user frustration (broken buttons, unresponsive UI).

Dead Clicks

Detected when a click produces no observable effect after a short wait: the URL did not change and the clicked node did not mutate. Table rows and cards that then navigate are not dead clicks. Frequent true dead clicks often indicate confusing visual affordances.

JS Errors

Both window.onerror and unhandledrejection events are captured and forwarded. Useful for correlating JavaScript errors with conversion drops.

Helper Methods

The snippet exposes a few utility methods on window.apex:

// Get the current visitor's anonymous ID
const visitorId = window.apex.getVisitorId();

// Identify a visitor by email (with optional metadata)
window.apex.identify("user@example.com", { name: "Jane Doe" });

// Named in-product control — one event, not one name per button
window.apex.track("ui_action", {
  action: "click",
  name: "Get started",
  element_type: "button",
  location: "hero",
});

Info

The window.apex object is available as soon as the snippet loads. If you need to call it before the snippet runs, initialize it first: window.apex = window.apex || {};

Next Steps

  • Form Tracking — how form submissions are captured automatically
  • Experiments — how experiment assignments flow into events
  • Installation — verify events are reaching the Apex API