Cortality
Documentation

Install Cortality in about ten minutes.

Two script tags, one attribute on your primary call-to-action, and a console message that tells you it worked. Every step below is self-verifying, and this page is written so you can forward it directly to whoever deploys your site.

Before you start: get your siteId and key

Cortality is a guided engagement, not self-serve. Every client gets a siteId, a submission key, and a dashboard login, provisioned by us at onboarding. If you don't have yours yet, email colton@cortality.com or reply to your Cortality contact; we typically provision the same day.

Pricing, engagement scope, and what you receive (reports, dashboard access, review calls) are agreed during onboarding rather than listed here; this page covers installation and technical behavior only. If you're still evaluating, the same email reaches the founder directly.

Quickstart

Paste this immediately before your page's closing </body> tag, using the siteId and submission key from your onboarding email:

<script src="https://cdn.cortality.com/c.js" async></script>
<script>
  window.addEventListener('load', function () {
    if (window.Cortality) {
      Cortality.init({
        siteId: 'op_yourcompany',
        secret: 'your-submission-key',  // publishable, submit-only — see note below
        endpoint: 'https://ingest.cortality.com/v1/sessions'  // stated explicitly so installs never depend on defaults
      });
    }
  });
</script>
About the field named secret: despite the name, it's a publishable, submission-only key, the same trust model as a Google Analytics measurement ID. It can push data into your site's bucket and nothing else; it cannot read anything. It's safe in your page source, and we can rotate it any time.

Install it exactly as written. Don't add lazy-loading, interaction-gating, or "load after the user does something" wrappers, and don't move it inside a component or useEffect. Deferred loading blinds the measurement to the first seconds on the page, which is exactly what we measure. The load hook in our block is fine and expected: the script loads async in parallel with your page, and the hook just guarantees it's ready before init. If your page is very media-heavy and you want capture armed even earlier, ask us for the early-boot variant (the same init call placed right after the script tag with a brief readiness poll, so it doesn't wait for the load event). A consent banner is fine if you run one (see disclosure duties).

If anything mangles the copy-paste, the two hostnames are cdn.cortality.com (script) and ingest.cortality.com (endpoint).

Where the tag goes, by stack

StackPlacement
Static HTMLIn each page you want measured, before </body>.
Astro / Next.js / layoutsIn the shared layout file so every page carries it, before </body>.
Vite / React / Vue SPAIn the root index.html, before </body>. If the same app serves logged-in areas, add the route allowlist.
WordPressAppearance → Theme File Editor → footer.php before </body>, or (easier and update-safe) a header/footer snippet plugin such as WPCode: paste into the "footer" slot.
WebflowSite settings → Custom Code → Footer Code, then publish.
ShopifyOnline Store → Themes → Edit code → layout/theme.liquid, before </body>. Note: this covers your storefront pages; Shopify's checkout pages aren't editable on standard plans, so tag an on-page action (add-to-cart, a lead form) as your conversion rather than the checkout button. See conversion tracking.
Google Tag ManagerSupported if you can't touch code at all: a Custom HTML tag firing on All Pages, containing the full Quickstart block. Direct install is preferred (GTM adds its own loading layer ahead of ours), but a GTM install measures correctly.
AI site builders (Lovable, v0, Bolt…)Paste the prompt below into the builder's chat.
Agency-built / not sureForward this page to whoever deploys your site; the Quickstart plus one row above covers them. If nobody can make the change, email us and we'll get on a screen-share with you or your agency.

Prompt for AI site builders

If your site is built and edited through an AI tool, paste this to it, with your real values substituted:

Add Cortality analytics. In index.html, immediately before the closing
body tag, add exactly this, and do not wrap it in a React component,
useEffect, or any loader:

<script src="https://cdn.cortality.com/c.js" async></script>
<script>window.addEventListener('load',function(){if(window.Cortality){
Cortality.init({siteId:'YOUR_SITE_ID',secret:'YOUR_KEY',
endpoint:'https://ingest.cortality.com/v1/sessions'});}});</script>

Then add the attribute data-cortality-conversion to our primary
call-to-action button, passing it through to the real DOM element.
If index.html or hosting config sets a Content-Security-Policy, add
https://cdn.cortality.com to script-src. Do not modify the snippet
in any other way.

Verify it works

  1. Open your site in a normal browser window.
  2. Right-click → Inspect → Console.
  3. Look for: [Cortality] session started
That message is the whole verification. If it appears, capture is live. Move the mouse for twenty seconds, click something, then switch tabs; your session becomes visible in your dashboard within about a minute.

If the message doesn't appear

  • A CSP error in the console mentioning cdn.cortality.com → see Content Security Policy.
  • No error at all → the tag likely isn't in the served HTML. View source and search for cortality; if it's missing, the change didn't deploy.
  • [Cortality] path not in allowedPaths → working as configured; you're on a route outside your allowlist.
  • Anything else → email colton@cortality.com; we answer same-day and will get on a screen-share if needed.

Conversion tracking

Add one attribute to the element that counts as the win on your page: the form submit button, the booking button, the sign-up link.

<button data-cortality-conversion>Request a Demo</button>

In React and similar frameworks, make sure the attribute reaches the real DOM element. On a plain <button> it passes through automatically; on a custom component, the component must spread extra props onto its underlying element. On visual builders (Webflow, WordPress page builders), use the element's custom-attribute setting; if your builder doesn't expose one, tell us which button is the win and we'll suggest the cleanest alternative for your setup.

Pick the truest win you have. A button that opens a booking dialog counts clicks on that button, which measures intent to book, not completed bookings. If the completed action has its own submit button (for example, inside the dialog), tag that instead. Tell us which element you tagged so your dashboard labels it correctly.

SPA route allowlist

Single-page applications serve everything from one index.html, so a root-level tag would execute on every route, including logged-in product areas. If your app has authenticated routes, restrict collection to your public marketing routes with allowedPaths:

Cortality.init({
  siteId: 'op_yourcompany',
  secret: 'your-submission-key',
  endpoint: 'https://ingest.cortality.com/v1/sessions',
  allowedPaths: ['/', '/pricing', '/about', '/blog/*']
});

The behavior is fail-closed in both directions:

  • Landing outside the list (a deep link into the app, a bookmark, a login page): the snippet initializes nothing. No session, no listeners, no collection.
  • Navigating from a listed route into an unlisted one (marketing page → logged-in app): the snippet sends what it collected on the marketing page, then permanently stops all collection for the rest of that browser tab's life. It does not re-arm if the visitor returns to a marketing route.

How it detects navigation: the snippet monitors the browser's history API (pushState, replaceState, and back/forward), which is how SPA routers change routes without reloading, so client-side route changes are caught without any integration work in your router.

Matching rules: entries are pathnames. Exact entries match exactly (trailing slashes tolerated). A trailing * matches a whole section: '/blog/*' covers /blog and every post under it. Anything not listed is out of scope by default, so new product routes are excluded without you maintaining a blocklist.

Know the limit: the allowlist matches URL paths, not login state. If a listed marketing route ever renders personalized or logged-in content without changing its path, that content is in scope; exclude it with data-cortality-ignore or call Cortality.disable() on login (below).

Programmatic stop

Belt and suspenders for auth boundaries or consent controls: calling Cortality.disable() immediately and permanently stops collection for the tab and sends what the current page has collected. Call it from your sign-in handler if you want a hard guarantee independent of routing:

onLogin() {
  window.Cortality && Cortality.disable && Cortality.disable();
}
Verify the boundary yourself: on a marketing route the console shows [Cortality] session started. Navigate into your app and it prints [Cortality] capture stopped: left allowed routes. Land directly on an app route and it prints [Cortality] path not in allowedPaths, capture off.

Excluding elements

Mark any element with data-cortality-ignore and it is excluded from observation, along with everything inside it:

<div data-cortality-ignore>
  <!-- live chat widget -->
</div>

Use this for anything that renders visitor-entered or personalized content outside a form field. Automatic masking covers form input values everywhere, but content your page renders elsewhere, a live-chat transcript, a "welcome back" banner, a quote-calculator result, is page content and will be captured unless you exclude it. Chat widgets are the most common case; we check for them at onboarding, but new widgets added later are on you to mark. See the masking model for exactly what is and isn't covered automatically.

Content Security Policy

If your site sends a Content-Security-Policy header, two directives matter:

DirectiveWhat to addWhen
script-srchttps://cdn.cortality.comAlways (loads the snippet)
connect-srchttps://ingest.cortality.comOnly if your policy doesn't already allow https:

One subtlety: the capture module fetches your page's images to include them in page captures. If your images are served from a different origin (a CDN or image host), that origin needs to be in connect-src too, unless https: is already allowed. If you're not sure where your CSP is configured, it's usually your hosting config or server middleware; send us the header and we'll tell you the exact change.

Your disclosure duties

Before or alongside install: disclose this technology in your privacy policy, and evaluate whether your jurisdictions require visitor consent. Some US state laws treat recording website interactions as requiring disclosure or consent, independent of cookies. If you gate on consent, call Cortality.disable() when a visitor declines.

The authoritative version of this guidance, including a starter policy clause you can hand your counsel and when to ship it, lives in the Trust Center: Your obligations as a client.

Your dashboard

Your dashboard lives at app.cortality.com. At onboarding we issue two credentials: an access code (your account identifier, like a username) and a password, scoped to your site(s). There's no self-signup, and no automated password recovery by design: if credentials are lost, email us from your registered address and we revoke the old login and reissue a new one, typically the same day. Sessions typically appear within about a minute of a visit ending. The dashboard shows your page section-by-section with attention scores, your conversion funnel, and the current recommendations; your written reports are built from the same data.

What gets collected

The short version: mouse, scroll, touch, and click behavior with page coordinates; periodic images of the page as the visitor's browser rendered it; a sanitized copy of the page's HTML; and basic session context. No cookies, no keystrokes, no form values, no cross-visit identification, no biometrics.

The complete, versioned statement, including the masking model and its limits, IP handling, retention schedules, subprocessors, and the deletion process, lives in the Trust Center. That page is the authoritative reference; this section is only a summary.

Removing the snippet

Delete the two script tags. That's the entire removal; no cookies were set, and nothing persists in visitors' browsers beyond the current visit's crash-protection buffer, which clears itself. To also delete collected data, see Retention & deletion.

FAQ

Will it slow my site down?

The loader is about 18 KB compressed and loads async, so it never blocks rendering. Capture work is throttled and deferred to idle time. A second module (about 10 KB) lazy-loads only when the first page capture is needed.

Is the key in the page source a security problem?

No. It's a submission-only key, like a GA measurement ID: it can push data into your site's bucket and nothing else. It cannot read data, and every submission is server-verified and rate-limited. If it's ever abused, we rotate it and the old key stops working immediately.

Can it break my page?

The snippet runs inside error isolation: any internal failure disables the snippet silently rather than affecting your page. It does not alter your page's visible content, styling, or user-facing behavior. To do its work it adds a small number of non-rendered elements of its own (for example, injecting its lazy-loaded capture module as a script tag), but it never edits, removes, or restyles anything of yours.

When do sessions show up?

Data ships during the visit and when the visitor leaves; sessions are typically queryable in your dashboard within about a minute of the visit ending.

How does this interact with consent banners and recording-consent laws?

Two separate questions. Cookie banners govern cookies, which this snippet doesn't set. Separately, some states' laws treat recording website interactions as requiring disclosure or consent regardless of cookies. We recommend disclosing the tool in your privacy policy and evaluating consent requirements with your counsel; see your disclosure duties. If you gate on consent, initialize Cortality only after acceptance, or call Cortality.disable() on decline.

Can we run it on multiple domains?

Yes; each domain gets its own siteId so data stays separated. Tell us the domains during onboarding and we'll provision them.

Anything we should avoid during a measurement window?

Keep the page layout stable during an agreed capture window; a mid-week redesign resets the comparison. Ship changes when we agree the window is done, and we'll timestamp the change to measure before versus after.

Mobile visitors?

Captured fully: touch, scroll, and page captures. Cursor-specific metrics (hover time) exist only on desktop, since phones have no cursor; scroll-reach and funnel numbers include all devices.