# Calmstats documentation

The complete Calmstats docs in one file, for AI assistants. Web version: https://calmstats.com/docs.

## Contents

- [Get started: Get started in 3 steps](https://calmstats.com/docs/get-started)
- [Get started: Install the script](https://calmstats.com/docs/install-script)
- [Get started: Install with an AI agent](https://calmstats.com/docs/ai-agents)
- [Get started: Connect payment providers](https://calmstats.com/docs/connect-payments)
- [Get started: Track custom events](https://calmstats.com/docs/custom-events)
- [Get started: Track goals](https://calmstats.com/docs/goals)
- [Get started: Track conversion funnels](https://calmstats.com/docs/funnels)
- [Get started: Track scroll depth](https://calmstats.com/docs/scroll-tracking)
- [Installation guides: Next.js](https://calmstats.com/docs/install-nextjs)
- [Installation guides: React and single-page apps](https://calmstats.com/docs/install-react)
- [Installation guides: WordPress](https://calmstats.com/docs/install-wordpress)
- [Installation guides: Webflow](https://calmstats.com/docs/install-webflow)
- [Installation guides: Framer](https://calmstats.com/docs/install-framer)
- [Installation guides: Shopify](https://calmstats.com/docs/install-shopify)
- [Installation guides: Google Tag Manager](https://calmstats.com/docs/install-gtm)
- [Revenue attribution: How attribution works](https://calmstats.com/docs/revenue-attribution)
- [Revenue attribution: Stripe](https://calmstats.com/docs/stripe)
- [Revenue attribution: Polar](https://calmstats.com/docs/polar)
- [Revenue attribution: Whop](https://calmstats.com/docs/whop)
- [Revenue attribution: Shopify](https://calmstats.com/docs/shopify)
- [Advanced features: Script options](https://calmstats.com/docs/script-options)
- [Advanced features: Track AI crawlers](https://calmstats.com/docs/ai-crawlers)
- [Advanced features: Connect Google Search Console](https://calmstats.com/docs/search-console)
- [Advanced features: Embeddable widgets](https://calmstats.com/docs/widgets)
- [Advanced features: Share a public dashboard](https://calmstats.com/docs/public-dashboard)
- [Troubleshooting: Visits are not showing](https://calmstats.com/docs/script-not-tracking)
- [Troubleshooting: Revenue is not showing](https://calmstats.com/docs/revenue-not-showing)

---

# Get started in 3 steps

Add your website, install one script, and connect payments to see which traffic brings in revenue.

Calmstats needs one script on your site. Payments are optional, but they are what turn visits into revenue per channel, campaign and page.

## 1. Add your website

[Create a free account](https://calmstats.com/signup), then add your website with its name and domain, such as `example.com`. Use the domain visitors see: Calmstats only counts visits sent from that domain (`www.` and the bare domain count as the same site), so a preview or `localhost` copy of your site never pollutes your stats.

## 2. Install the script

Paste this tag into the `<head>` of every page. Your dashboard shows it with your website ID filled in under **Settings → Tracking**.

```html title="index.html"
<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>
```

That one tag tracks pageviews, referrers, UTM campaigns, devices, countries, scroll depth and single-page app navigation. No cookies, no banner configuration, nothing else to add. Then click **Verify installation** in **Settings → Tracking** to confirm Calmstats sees it.

> [!TIP]
> Signed in? Pick a website in the header and every code sample on these pages uses its ID.

Using a site builder or framework? See the [installation guides](https://calmstats.com/docs/install-nextjs) for Next.js, React, WordPress, Webflow, Framer, Shopify and Google Tag Manager.

## 3. Connect your payments

Connect Stripe, Polar, Whop or Shopify in **Settings → Revenue**. Calmstats matches each payment to the visitor who made it, so every report shows revenue next to traffic: which channels pay, which pages sell, which campaigns are worth the money.

Start with [Connect payment providers](https://calmstats.com/docs/connect-payments).

## Next steps

- [Track custom events](https://calmstats.com/docs/custom-events) such as signups and button clicks.
- [Track goals](https://calmstats.com/docs/goals) and [conversion funnels](https://calmstats.com/docs/funnels) to see where visitors drop off.
- [Track scroll depth](https://calmstats.com/docs/scroll-tracking) to see how far people read.

Source: https://calmstats.com/docs/get-started

---

# Install the script

The one script tag Calmstats needs, where to put it, and the options it takes.

## Add the script

Paste this tag into the `<head>` of every page on your site:

```html title="index.html"
<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>
```

Replace `YOUR-SITE-ID` with your website ID. The tag with your ID is in **Settings → Tracking**, ready to copy.

The script is about 2 KB, loads with `defer` so it never blocks your page, and works with any site or framework.

## What it tracks

With nothing else added, the script records:

- **Pageviews**, including client-side navigation in single-page apps.
- **Sources**: referrers, UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`) and channels.
- **Visitors and sessions**: device, screen size and country. Sessions end after 30 minutes without activity.
- **Engagement**: time on site, bounces, outbound link clicks, scroll depth and who is online now.

It sets no cookies. Visitors get a site-scoped ID in `localStorage`, and IP addresses are only used in memory for a daily salted hash. See the [privacy notice](https://calmstats.com/privacy) for the details.

## Check the install

1. Open your site in a normal browser window, not one with an ad blocker that blocks analytics.
2. In Calmstats, go to **Settings → Tracking** and click **Verify installation**.
3. Your visit shows up in the **Realtime** report within a few seconds.

Nothing showing? See [Visits are not showing](https://calmstats.com/docs/script-not-tracking).

## Options

The tag takes a few optional attributes, such as respecting Do Not Track or excluding private pages:

| Attribute | Example | What it does |
| --- | --- | --- |
| `data-respect-dnt` | `data-respect-dnt="true"` | Skip visitors whose browser sends Do Not Track. |
| `data-spa-tracking` | `data-spa-tracking="false"` | Turn off pageviews on client-side URL changes (on by default for single-page apps). |
| `data-exclude` | `data-exclude="/app/**,/share/*"` | Stop tracking on matching paths; `*` is one path segment, `**` is anything. |
| `data-scroll-depth` | `data-scroll-depth="false"` | Turn off scroll depth (on by default). |
| `data-host-url` | `data-host-url="https://stats.example.com"` | Send events to a different origin than the one serving the script. |

More examples are in [Script options](https://calmstats.com/docs/script-options).

## Before the script loads

To send [custom events](https://calmstats.com/docs/custom-events) with `calmstats.track()` before the script has finished loading, add this optional queue script just before the main tag. Calls made while the page loads are sent once Calmstats loads.

```html
<script>window.calmstats=window.calmstats||{q:[]};["track","goal","identify"].forEach(function(m){window.calmstats[m]=window.calmstats[m]||function(){window.calmstats.q.push([m].concat([].slice.call(arguments)))}});</script>
```

Source: https://calmstats.com/docs/install-script

---

# Install with an AI agent

Copy-paste prompts and an Agent Skill that let Claude Code, Cursor, Codex or a WordPress or Shopify MCP agent install Calmstats, add events and track scrolling.

Let your coding agent do the install. Copy a prompt below into Claude Code, Cursor, Codex, Copilot or any other agent, or add the Calmstats skill to your project once and just ask for what you want, such as "track signups" or "track scrolling on the pricing page".

Every prompt includes your script tag and the rules the agent needs: one script only, no extra packages, and how events and section views work. Each prompt ends by telling you what changed and how to verify it.

> [!TIP]
> Signed in? Pick a website in the header and every prompt and the skill use its ID. You'll also find these prompts with your ID filled in under **Settings → Tracking**.

## Prompts

### Install the script

Works in Next.js (App Router or Pages Router), React, Vue, Svelte, Astro and plain HTML. The agent finds your layout and adds the tag once.

```text title="Prompt"
Add Calmstats analytics to this project.

This is the tag to load on every page:

<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>

- In Next.js, add it once to the root layout (app/layout.tsx, or pages/_app.tsx with the Pages Router) with next/script, strategy="afterInteractive", keeping the src and data-website-id attributes.
- In other frameworks and plain HTML, put it in the <head> of the shared layout or index.html.
- Calmstats needs exactly one script tag, loaded on every page. Before adding it, search the project for "script.js" and "data-website-id": if a Calmstats tag is already there, keep that one and only fix it.
- Pageviews, single-page app route changes, referrers, UTM campaigns, devices and scroll depth are tracked automatically. Do not add router listeners, manual pageview calls, or any other package.
- Visits are only counted from the website's own domain, so localhost and preview deployments send nothing. That is expected, not a broken install.
- Framework guides: https://calmstats.com/docs/install-nextjs.md, https://calmstats.com/docs/install-react.md, https://calmstats.com/docs/install-script.md.

When you're done, tell me which files or settings you changed, and remind me to deploy, then open Calmstats → Settings → Tracking and click "Verify installation". New events show up as suggestions when I create a goal or funnel.
```

### Track custom events

Replace the line in brackets with the events you want, in plain words.

```text title="Prompt"
Track these Calmstats custom events in this project:

- [describe each event, for example: "signup_completed when the signup form succeeds", "pricing_click on each plan button with the plan name"]

How Calmstats events work:

- No-code clicks: add data-calmstats-event="event_name" to a button or link. Other data-calmstats-* attributes on the same element are sent as properties (data-calmstats-plan="pro" sends { plan: "pro" }).
- Forms: put data-calmstats-event on the <form>; the event is sent on submit, not on clicks inside it.
- From code: call window.calmstats?.track("event_name", { key: "value" }) after the action succeeds. Use optional chaining so nothing breaks if the script is blocked.
- Event names: snake_case names for what happened, such as signup_completed or pricing_click, up to 100 characters. Use one name per action across the site. Property values are strings, numbers or booleans.
- Never send personal data such as emails, names or phone numbers as event names or properties.
- Prefer the data attributes for plain clicks and forms; use track() when the event depends on a result, such as a signup that succeeded on the server.
- The Calmstats script must already be on the page. If it isn't, add it first with this tag:

<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>

If track() can run before the deferred script loads (for example on page load), add this optional queue just before the main tag:

<script>window.calmstats=window.calmstats||{q:[]};["track","goal","identify"].forEach(function(m){window.calmstats[m]=window.calmstats[m]||function(){window.calmstats.q.push([m].concat([].slice.call(arguments)))}});</script>

Examples:

<button data-calmstats-event="pricing_click" data-calmstats-plan="pro">
  Start free trial
</button>

<form data-calmstats-event="signup_completed">
  ...
</form>

calmstats.track('signup_completed', { plan: 'pro' });

Reference: https://calmstats.com/docs/custom-events.md

When you're done, tell me which files or settings you changed, and remind me to deploy, then open Calmstats → Settings → Tracking and click "Verify installation". New events show up as suggestions when I create a goal or funnel.
```

### Track scrolling on a page

Scroll depth is already on for every page. This prompt adds section views, such as "saw the pricing table", to the sections you name.

```text title="Prompt"
Track how visitors scroll on [page path, for example /pricing] with Calmstats.

Send a section view for each of these sections: [list them, for example: the pricing table as viewed_pricing, the testimonials as read_testimonials]

How Calmstats scroll tracking works:

- Scroll depth per page is automatic. Do not write scroll listeners. To turn it off, add data-scroll-depth="false" to the script tag.
- Section views: add data-calmstats-scroll="event_name" to a section, and the event is sent the first time that element is seen on a page. Optional data-calmstats-scroll-threshold (0.1 to 1, default 0.5, how much must be visible) and data-calmstats-scroll-delay (milliseconds it must stay in view).
- Sections rendered later and client-side route changes are handled. Section views are custom events, usable in goals and funnels.
- The Calmstats script must already be on the page. If it isn't, add it first with this tag:

<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>

Examples:

<section data-calmstats-scroll="viewed_pricing">
  ...
</section>

<div data-calmstats-scroll="read_testimonials"
     data-calmstats-scroll-threshold="0.7"
     data-calmstats-scroll-delay="2000">
  ...
</div>

Reference: https://calmstats.com/docs/scroll-tracking.md

When you're done, tell me which files or settings you changed, and remind me to deploy, then open Calmstats → Settings → Tracking and click "Verify installation". New events show up as suggestions when I create a goal or funnel.
```

### WordPress, Shopify and site builders

For an agent connected to your site through its MCP server or admin API, such as a WordPress, Shopify or Webflow MCP server. It picks the right place for the script on each platform and tells you the settings path when it can't make the change itself.

```text title="Prompt"
Use the tools you have for my site (WordPress, Shopify, Webflow or another site builder, through its MCP server or admin API) to add Calmstats analytics.

This is the tag to load in the <head> of every page:

<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>

- Calmstats needs exactly one script tag, loaded on every page. Before adding it, search the project for "script.js" and "data-website-id": if a Calmstats tag is already there, keep that one and only fix it.
- Pageviews, single-page app route changes, referrers, UTM campaigns, devices and scroll depth are tracked automatically. Do not add router listeners, manual pageview calls, or any other package.
- WordPress: add it with a header code plugin such as WPCode if your tools can change its settings, otherwise with a wp_head action in a child theme's functions.php or a small must-use plugin. Never paste it into post or page content, and never edit a parent theme. Guide: https://calmstats.com/docs/install-wordpress.md
- Shopify: connecting the store in Calmstats → Settings → Revenue installs the script and imports orders, so check whether it's connected before touching the theme. Without the connection, add the tag before </head> in layout/theme.liquid of the live theme. Purchases are tracked from orders, not with events. Guide: https://calmstats.com/docs/install-shopify.md
- Webflow: add it as site-wide head code (custom code) and publish the site. Guide: https://calmstats.com/docs/install-webflow.md
- Framer, Wix, Squarespace and builders your tools can't edit: give me the exact settings path to paste it into myself. Framer guide: https://calmstats.com/docs/install-framer.md
- Events (optional): add data-calmstats-event="event_name" as a custom attribute on buttons, links or forms, with data-calmstats-* properties. When the builder can't set attributes, add a small footer script that calls window.calmstats?.track("event_name") on click of a CSS selector.
- Section views (optional): add data-calmstats-scroll="event_name" as a custom attribute on a section. Scroll depth needs nothing.
- Change only what this needs, and don't publish other unpublished edits without asking me.

When you're done, tell me which files or settings you changed, and remind me to deploy, then open Calmstats → Settings → Tracking and click "Verify installation". New events show up as suggestions when I create a goal or funnel.
```

## Add the Calmstats skill

The skill is one `SKILL.md` file in the open [Agent Skills](https://agentskills.io) format. With it in your project, your agent knows how to install Calmstats, add custom events and track scrolling whenever you ask, with no prompt to paste.

For Claude Code, run this in your project:

```bash
mkdir -p .claude/skills/calmstats && curl -fsSL https://calmstats.com/skill.md -o .claude/skills/calmstats/SKILL.md
```

For other agents that support skills, save the same file in their skills folder, for example `.agents/skills/calmstats/SKILL.md`. For an agent without skills, save it anywhere in the repo and mention it in your `AGENTS.md` or rules file.

The downloaded file asks your agent to find the website ID in your code or ask you for it. To get a copy with your ID already filled in, copy it from the block below while signed in:

````markdown title="SKILL.md"
---
name: calmstats
description: Install Calmstats web analytics and add custom events or scroll tracking. Use when asked to add analytics or the Calmstats script, track a click, signup, form submit or other event, or track scrolling or section views on a page, in a codebase or through a WordPress, Shopify or Webflow MCP server.
---

# Calmstats analytics

Calmstats is cookieless web analytics with revenue attribution. Everything below uses one script tag; there is no npm package and nothing else to install.

## Install the script

If data-website-id below is still a placeholder rather than a UUID, reuse the data-website-id of a Calmstats tag already in the project, otherwise ask the user for it (Calmstats → Settings → Tracking). Never invent one.

```html
<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>
```

- Calmstats needs exactly one script tag, loaded on every page. Before adding it, search the project for "script.js" and "data-website-id": if a Calmstats tag is already there, keep that one and only fix it.
- Pageviews, single-page app route changes, referrers, UTM campaigns, devices and scroll depth are tracked automatically. Do not add router listeners, manual pageview calls, or any other package.
- Visits are only counted from the website's own domain, so localhost and preview deployments send nothing. That is expected, not a broken install.

### Next.js (App Router)

Add it once to the root layout. With the Pages Router, put the same `<Script>` in `pages/_app.tsx`.

```tsx
import Script from 'next/script';

// in app/layout.tsx, inside <body>, after {children}
<Script
  src="https://calmstats.com/script.js"
  data-website-id="YOUR-SITE-ID"
  strategy="afterInteractive"
/>
```

### Other frameworks and plain HTML

Put the tag in the `<head>` of the shared layout or `index.html` (Vite, React, Vue, Svelte, Astro, Remix, Nuxt, SvelteKit, Laravel, Rails, Django templates and so on).

### Script options

Optional attributes on the tag: `data-exclude="/app/**"` stops tracking on matching paths (`*` is one segment, `**` is anything), `data-respect-dnt="true"` skips Do Not Track browsers, `data-spa-tracking="false"` turns off route-change pageviews, `data-scroll-depth="false"` turns off scroll depth. Reference: https://calmstats.com/docs/script-options.md

## Custom events

- No-code clicks: add data-calmstats-event="event_name" to a button or link. Other data-calmstats-* attributes on the same element are sent as properties (data-calmstats-plan="pro" sends { plan: "pro" }).
- Forms: put data-calmstats-event on the <form>; the event is sent on submit, not on clicks inside it.
- From code: call window.calmstats?.track("event_name", { key: "value" }) after the action succeeds. Use optional chaining so nothing breaks if the script is blocked.
- Event names: snake_case names for what happened, such as signup_completed or pricing_click, up to 100 characters. Use one name per action across the site. Property values are strings, numbers or booleans.
- Never send personal data such as emails, names or phone numbers as event names or properties.
- Prefer the data attributes for plain clicks and forms; use track() when the event depends on a result, such as a signup that succeeded on the server.

```html
<button data-calmstats-event="pricing_click" data-calmstats-plan="pro">
  Start free trial
</button>

<form data-calmstats-event="signup_completed">
  ...
</form>
```

```js
window.calmstats?.track('signup_completed', { plan: 'pro' });
```

In TypeScript, declare the global once (for example in `types/calmstats.d.ts`):

```ts
type CalmstatsProps = Record<string, string | number | boolean>;

declare global {
  interface Window {
    calmstats?: {
      track: (event: string, props?: CalmstatsProps) => void;
      getVisitorId: () => string;
    };
  }
}

export {};
```

If `track()` can run before the deferred script has loaded (for example during page load), add this optional queue just before the main tag. Calls made while the page loads are sent once Calmstats loads:

```html
<script>window.calmstats=window.calmstats||{q:[]};["track","goal","identify"].forEach(function(m){window.calmstats[m]=window.calmstats[m]||function(){window.calmstats.q.push([m].concat([].slice.call(arguments)))}});</script>
```

Reference: https://calmstats.com/docs/custom-events.md

## Scroll tracking

- Scroll depth per page is automatic. Do not write scroll listeners. To turn it off, add data-scroll-depth="false" to the script tag.
- Section views: add data-calmstats-scroll="event_name" to a section, and the event is sent the first time that element is seen on a page. Optional data-calmstats-scroll-threshold (0.1 to 1, default 0.5, how much must be visible) and data-calmstats-scroll-delay (milliseconds it must stay in view).
- Sections rendered later and client-side route changes are handled. Section views are custom events, usable in goals and funnels.

```html
<section data-calmstats-scroll="viewed_pricing">
  ...
</section>

<div data-calmstats-scroll="read_testimonials"
     data-calmstats-scroll-threshold="0.7"
     data-calmstats-scroll-delay="2000">
  ...
</div>
```

In React and other JSX, data attributes go on the element as written: `<section data-calmstats-scroll="viewed_pricing">`.

Reference: https://calmstats.com/docs/scroll-tracking.md

## Sites managed through an MCP server or admin API

- WordPress: use a header code plugin such as WPCode if the tools can change its settings, otherwise a wp_head action in a child theme's functions.php or a must-use plugin. Never paste the tag into post or page content or edit a parent theme. https://calmstats.com/docs/install-wordpress.md
- Shopify: connecting the store in Calmstats → Settings → Revenue installs the script and imports orders. Without it, add the tag before </head> in layout/theme.liquid of the live theme. Purchases come from orders; don't add purchase events. https://calmstats.com/docs/install-shopify.md
- Webflow: add the tag as site-wide head code and publish. Events and section views are custom attributes on elements. https://calmstats.com/docs/install-webflow.md
- Builders the tools cannot edit (Framer, Wix, Squarespace): tell the user the settings path to paste the tag into.
- When a builder cannot set data attributes, add a small footer script that calls window.calmstats?.track("event_name") on click of a CSS selector.
- Change only what the task needs, and ask before publishing other unpublished edits.

## Finish

Tell the user which files or settings changed. Remind them that visits only count on the live domain: deploy, then click "Verify installation" in Calmstats → Settings → Tracking. New event names show up as suggestions when they create a goal or funnel step.

All docs as Markdown: https://calmstats.com/docs.md
````

## After the agent is done

Visits are only counted on your website's own domain, so localhost and preview deployments send nothing. Deploy, then click **Verify installation** in **Settings → Tracking**. New event names appear as suggestions when you create a [goal](https://calmstats.com/docs/goals) or [funnel](https://calmstats.com/docs/funnels) step.

Want the agent to read the docs itself? Point it at [/docs.md](https://calmstats.com/docs.md), every page in one Markdown file.

Source: https://calmstats.com/docs/ai-agents

---

# Connect payment providers

Connect Stripe, Polar, Whop or Shopify so every payment is matched to the visit that earned it.

Connecting a payment provider puts revenue on top of your web analytics: every payment is matched to the visitor who made it and credited to the channel, campaign, referrer and page that brought them. It is what makes [revenue attribution](https://calmstats.com/docs/revenue-attribution) work.

> [!NOTE]
> **Shopify stores**: connecting your store also installs the Calmstats script for you. See the [Shopify guide](https://calmstats.com/docs/shopify).

## Connect a provider

1. In your dashboard, open your website and go to **Settings → Revenue**.
2. Choose your provider and paste a read-only key. Each provider guide says exactly which key and permissions to create.
3. Copy the webhook endpoint Calmstats shows and add it in your provider's dashboard, so payments arrive in real time. Shopify registers its webhooks for you.
4. Pass the visitor to your checkout, so Calmstats knows who paid (see below).

Calmstats imports the last 30 days of payments when you connect and then syncs every day, so a missed webhook never loses a payment.

| Provider | Guide |
| --- | --- |
| Stripe | [Connect Stripe](https://calmstats.com/docs/stripe) |
| Polar | [Connect Polar](https://calmstats.com/docs/polar) |
| Whop | [Connect Whop](https://calmstats.com/docs/whop) |
| Shopify | [Connect Shopify](https://calmstats.com/docs/shopify) |

## Pass the visitor to checkout

The script exposes the visitor's ID in the browser:

```js
const visitorId = window.calmstats?.getVisitorId();
```

Send it with the checkout you create on your server, as `calmstats_visitor_id` metadata. Each provider guide has the exact code. Two cases need no code at all:

- **Stripe Payment Links**: the script adds the visitor to `buy.stripe.com` links on your pages automatically.
- **Shopify**: the script saves the visitor on the cart.

A payment that arrives without a visitor ID is still recorded in **Revenue**, but Calmstats can't credit it to a channel, campaign or page.

## Next step

See revenue per channel and campaign in **Sources**, next to traffic on the **Overview**, and every payment with the visitor who made it in **Revenue**. Then [track goals](https://calmstats.com/docs/goals) for the steps before the payment.

Source: https://calmstats.com/docs/connect-payments

---

# Track custom events

Send clicks, form submits and app events, then use them in goals and funnels.

Custom events track what pageviews can't: button clicks, signups, form submits, video plays. Every custom event can be used as a [goal](https://calmstats.com/docs/goals) or a [funnel step](https://calmstats.com/docs/funnels), and it appears in each visitor's journey.

The main script handles custom events. There is nothing else to install.

## Track clicks without code

Add `data-calmstats-event` to any element. The event is sent when the element is clicked:

```html
<button data-calmstats-event="pricing_click" data-calmstats-plan="pro">
  Start free trial
</button>
```

Other `data-calmstats-*` attributes on the same element are sent as properties, so this button sends `pricing_click` with `{ plan: "pro" }`.

## Track form submits

On a `<form>`, the event is sent when the form is submitted, not on each click inside it:

```html
<form data-calmstats-event="signup_completed">
  ...
</form>
```

## Track from JavaScript

Call `calmstats.track()` from your code, for example after a signup succeeds on the server and your app gets the response:

```js
calmstats.track('signup_completed', { plan: 'pro' });
```

Properties can be strings, numbers or booleans.

### Calls before the script loads

The script loads with `defer`, so a call made very early in the page can run before `window.calmstats` exists. To queue those calls, add this optional script just before the main tag. Calls made while the page loads are sent once Calmstats loads.

```html
<script>window.calmstats=window.calmstats||{q:[]};["track","goal","identify"].forEach(function(m){window.calmstats[m]=window.calmstats[m]||function(){window.calmstats.q.push([m].concat([].slice.call(arguments)))}});</script>
```

## Naming events

- Event names are up to 100 characters. Longer names are cut.
- Use `snake_case` verbs that describe what happened, such as `signup_completed` or `trial_started`.
- Keep one name per action: `signup` from the button and `signup` from the form count as the same goal.

## See your events

Events show up in **Goals** once you create a goal for them, in **Funnels** as steps, and in **Journeys** for each visitor. When you type an event name in a goal or funnel, Calmstats suggests the names it has already received.

Source: https://calmstats.com/docs/custom-events

---

# Track goals

Count the conversions that matter, from a page visit or a custom event.

A goal counts the visitors who did something that matters: reached the thank-you page, signed up, started a trial. Goals show conversions, conversion rate and the revenue those visitors brought in.

## Create a goal

1. Open your website and go to **Goals**.
2. Click **Create Goal** and give it a name.
3. Choose what counts as a conversion:
   - **Custom Event**: a [custom event](https://calmstats.com/docs/custom-events), such as `signup_completed`.
   - **Page URL**: a path, such as `/thank-you`. The path must match exactly; query strings and `#` fragments are ignored.
4. Save. Conversions are counted from the data already collected, not only from now on.

## Read the report

Each goal shows its conversions over the last days, its conversion rate, and the revenue from the visitors who converted. Use the filters at the top of the dashboard to compare goals by channel, campaign, page, country or device.

## Next step

Chain goals and pages into a [conversion funnel](https://calmstats.com/docs/funnels) to see where visitors drop off on the way.

Source: https://calmstats.com/docs/goals

---

# Track conversion funnels

See where visitors drop off between landing and paying, step by step.

A funnel shows how many visitors move through a series of steps, such as landing page → pricing → signup, and where the rest drop off.

## Create a funnel

1. Open your website and go to **Funnels**.
2. Click **New Funnel** and name it.
3. Add between 2 and 6 steps. Each step is either:
   - **Page URL Match**: a path, such as `/pricing`.
   - **Custom Event**: a [custom event](https://calmstats.com/docs/custom-events), such as `signup_completed`.
4. Save. The funnel is computed from data you already have.

## How steps are counted

- Steps count in order, within one session: a visitor reaches step 3 only after steps 1 and 2.
- A visitor who repeats a step is counted once.
- The conversion rate is the share of visitors who started the funnel and reached the last step.

## Read the report

- **Flow**: each step with how many visitors reached it and how many dropped off. Select a drop-off to see where those visitors went instead: the page they left from, or the next pages they opened.
- **What stands out**: plain sentences on the biggest drop-off and changes from the previous period.
- **Segments**: conversion by channel, device and country, to compare who converts.
- **Time to convert**: how long visitors take from the first step to the last.

> [!TIP]
> Steps that are custom events usually make sturdier funnels than page paths: they keep working when you rename a page.

Source: https://calmstats.com/docs/funnels

---

# Track scroll depth

How far visitors read each page, and which sections they actually see.

## Scroll depth

Scroll depth needs no setup. The main script records how far down each page a visitor scrolled and sends it with signals it already sends, so it adds no requests and uses none of your monthly events.

- **Top Pages** shows each page's average depth and the share of views that reached 25, 50, 75 and 100% of the page.
- **Journeys** shows the depth of every page a visitor saw.

Pages that fit on one screen are not measured. To turn scroll depth off, add `data-scroll-depth="false"` to the script tag.

## Section views

To know whether visitors actually saw a section, such as your pricing table, add `data-calmstats-scroll` with an event name. The event is sent the first time the element is seen on a page:

```html
<section data-calmstats-scroll="viewed_pricing">
  ...
</section>
```

Section views are custom events: use them in [goals](https://calmstats.com/docs/goals) and [funnels](https://calmstats.com/docs/funnels) like any other, and see them in **Journeys**.

### Threshold and delay

```html
<div data-calmstats-scroll="read_testimonials"
     data-calmstats-scroll-threshold="0.7"
     data-calmstats-scroll-delay="2000">
  ...
</div>
```

- `data-calmstats-scroll-threshold` (0.1 to 1, default 0.5) is how much of the element must be visible, or how much of the screen it must fill when it is taller than the screen.
- `data-calmstats-scroll-delay` is how long, in milliseconds, it must stay in view.

Sections added after the page loads, and pages in single-page apps, are handled. `data-fast-scroll` attributes from DataFast work unchanged.

Source: https://calmstats.com/docs/scroll-tracking

---

# Next.js

Add Calmstats to a Next.js app with the App Router or the Pages Router.

## App Router

Add the script to your root layout with `next/script`:

```tsx title="app/layout.tsx"
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://calmstats.com/script.js"
          data-website-id="YOUR-SITE-ID"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}
```

## Pages Router

Add the same `<Script>` to `pages/_app.tsx`, next to your `<Component {...pageProps} />`.

## Notes

- Client-side navigation is tracked automatically, with no router events to wire up.
- To keep a signed-in area out of your stats, add `data-exclude="/dashboard/**"`. See [Script options](https://calmstats.com/docs/script-options).
- To count AI crawlers, which never run JavaScript, see [Track AI crawlers](https://calmstats.com/docs/ai-crawlers).

Source: https://calmstats.com/docs/install-nextjs

---

# React and single-page apps

Add Calmstats to Vite, Create React App, Vue, Svelte and other single-page apps.

For Vite, Create React App, Vue, Svelte, Angular and other single-page apps, add the script to the `<head>` of your `index.html`:

```html title="index.html"
<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>
```

Client-side route changes are tracked automatically: the script records a pageview each time the URL path changes, whether your router uses `pushState` or the back and forward buttons. Hash changes alone (`#section`) don't count as new pageviews.

To turn route tracking off, for example when you send pageviews yourself, add `data-spa-tracking="false"`.

## Send events from your components

```tsx
function UpgradeButton() {
  return (
    <button onClick={() => window.calmstats?.track('upgrade_click', { plan: 'pro' })}>
      Upgrade
    </button>
  );
}
```

See [Track custom events](https://calmstats.com/docs/custom-events) for more.

Source: https://calmstats.com/docs/install-react

---

# WordPress

Add Calmstats to a WordPress site with a header plugin or your theme.

## With a plugin

1. In WordPress, go to **Plugins → Add New** and install a header and footer plugin, such as **WPCode**.
2. Open the plugin's header section and paste the script:

```html title="index.html"
<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>
```

3. Save. The script now loads on every page.

## In your theme

If you edit your theme, paste the script just before `</head>` in `header.php`, or add it from your child theme's `functions.php`:

```php title="functions.php"
add_action('wp_head', function () {
  echo '<script defer data-website-id="YOUR-SITE-ID" src="https://calmstats.com/script.js"></script>';
});
```

> [!WARNING]
> Caching and optimization plugins can delay or combine scripts. If visits don't show up, exclude `script.js` from minification and delayed loading.

Source: https://calmstats.com/docs/install-wordpress

---

# Webflow

Add Calmstats to a Webflow site in Project settings.

1. In Webflow, open **Project settings → Custom code**.
2. Paste the script into **Head code**:

```html title="index.html"
<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>
```

3. Click **Save changes**, then **Publish** your site. Custom code only runs on the published site, not in the Designer.

Make sure the domain you added in Calmstats is the one your Webflow site is published on.

Source: https://calmstats.com/docs/install-webflow

---

# Framer

Add Calmstats to a Framer site in Site settings.

1. In Framer, open **Site settings → General → Custom code**.
2. Paste the script into **End of `<head>` tag**:

```html title="index.html"
<script defer
  data-website-id="YOUR-SITE-ID"
  src="https://calmstats.com/script.js"></script>
```

3. Publish your site.

Framer sites are single-page apps; page changes are tracked automatically.

Source: https://calmstats.com/docs/install-framer

---

# Shopify

Connect your Shopify store and Calmstats installs the script for you.

You don't need to paste a script on Shopify. Connecting your store in **Settings → Revenue** installs the Calmstats script on your storefront for you and imports your orders, so you get visitors, revenue and attribution from one step.

Follow the [Shopify revenue guide](https://calmstats.com/docs/shopify) to connect your store.

> [!NOTE]
> If you already added the script to your theme by hand, you can remove it after connecting: the connection installs it, and visits aren't counted twice either way.

Source: https://calmstats.com/docs/install-shopify

---

# Google Tag Manager

Load Calmstats from a Custom HTML tag in Google Tag Manager.

1. In Google Tag Manager, create a new tag of type **Custom HTML**.
2. Paste the script. In Tag Manager, put the website ID in the script URL, which works where attributes may be dropped:

```html
<script defer src="https://calmstats.com/script.js?website_id=YOUR-SITE-ID"></script>
```

3. Set the trigger to **Initialization - All Pages** (or **All Pages**).
4. Save and **Submit** to publish the container.

> [!TIP]
> Loading the script directly in your site's `<head>` is faster and isn't blocked when a visitor's browser blocks Tag Manager. Use Tag Manager only when you can't edit your site's code.

Source: https://calmstats.com/docs/install-gtm

---

# How attribution works

How Calmstats matches a payment to a visitor and credits the channel, campaign and page.

Revenue attribution answers one question: which traffic brings in paying customers? Calmstats connects the visit to the payment, so every report can show revenue next to visitors.

## How a payment is matched

1. The script gives each visitor an ID, stored in your site's own `localStorage` (no cookies).
2. At checkout, your site passes that ID to your payment provider as `calmstats_visitor_id` metadata. Stripe Payment Links and Shopify need no code.
3. When the payment arrives, by webhook or by the daily sync, Calmstats looks up the visitor and every session that led to the payment.
4. The payment is credited to the channel, campaign, referrer, landing page and country of the session it came from.

## Attribution models

Revenue reports credit each payment to the source of the session it came from (**last touch**). In **Settings → Attribution** you can already choose **first touch** (the source that first brought the visitor) or **linear** (split across every session in the journey); reports for those models are on the way.

## Refunds and currencies

Refunds are subtracted from the payment they belong to, so revenue is always net. Each currency is kept separately and is never converted.

## Where to see it

- **Overview**: revenue, revenue per visitor and conversion rate next to traffic.
- **Sources**: revenue per channel, referrer and campaign.
- **Revenue**: every payment, with the visitor who made it. Open the visitor in **Visitors** or **Journeys** to see the path that led to it.
- **Goals** and **Funnels**: revenue from the visitors who converted.

Connect a provider to start: [Stripe](https://calmstats.com/docs/stripe), [Polar](https://calmstats.com/docs/polar), [Whop](https://calmstats.com/docs/whop) or [Shopify](https://calmstats.com/docs/shopify).

Source: https://calmstats.com/docs/revenue-attribution

---

# Stripe

Attribute Stripe Checkout, Payment Links and renewals to the visits that earned them.

## 1. Create a key

In **Settings → Revenue**, choose **Stripe** and paste:

**Restricted API key**: Stripe → Developers → API keys → Create restricted key, with Read on Charges, Checkout Sessions and Refunds.

Calmstats only reads payments. It can't charge, refund or change anything in your account.

## 2. Add the webhook

Copy the webhook endpoint shown in **Settings → Revenue** and add it in Stripe with these events:

`charge.succeeded`, `charge.refunded`, `checkout.session.completed`

Webhooks make payments appear right away. Without them, payments still arrive with the daily sync.

## 3. Pass the visitor to checkout

```js
const visitorId = window.calmstats?.getVisitorId();

// Payment Links need nothing: the Calmstats script adds the visitor to
// buy.stripe.com links as client_reference_id.

// Stripe Checkout from your server:
const session = await stripe.checkout.sessions.create({
  // ...line_items, mode, success_url
  client_reference_id: visitorId,
  metadata: { calmstats_visitor_id: visitorId },
});
```

Using Stripe Payment Links? There's nothing to add: the script appends the visitor to every `buy.stripe.com` link on your pages as `client_reference_id`.

Source: https://calmstats.com/docs/stripe

---

# Polar

Attribute Polar orders and subscriptions to the visits that earned them.

## 1. Create a key

In **Settings → Revenue**, choose **Polar** and paste:

**Organization access token**: Polar → Settings → Developers → New token, with the orders:read scope.

**Organization ID**: Optional for organization tokens.

Calmstats only reads payments. It can't charge, refund or change anything in your account.

## 2. Add the webhook

Copy the webhook endpoint shown in **Settings → Revenue** and add it in Polar with these events:

`order.paid`, `order.refunded`, `order.updated`

Webhooks make payments appear right away. Without them, payments still arrive with the daily sync.

## 3. Pass the visitor to checkout

```js
const visitorId = window.calmstats?.getVisitorId();

// Server: create the checkout with the visitor from the browser
const checkout = await polar.checkouts.create({
  products: [productId],
  metadata: { calmstats_visitor_id: visitorId },
});
```

Source: https://calmstats.com/docs/polar

---

# Whop

Attribute Whop memberships and one-off sales to the visits that earned them.

## 1. Create a key

In **Settings → Revenue**, choose **Whop** and paste:

**Company API key**: Whop → Developer → API keys, with payment:basic:read and member:email:read.

**Company ID**: Your biz_… ID. Optional when the key belongs to one company.

Calmstats only reads payments. It can't charge, refund or change anything in your account.

## 2. Add the webhook

Copy the webhook endpoint shown in **Settings → Revenue** and add it in Whop with these events:

`payment.succeeded`, `refund.created`, `refund.updated`

Webhooks make payments appear right away. Without them, payments still arrive with the daily sync.

## 3. Pass the visitor to checkout

```js
const visitorId = window.calmstats?.getVisitorId();

// Server: create the checkout configuration with the visitor
await fetch('https://api.whop.com/api/v1/checkout_configurations', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WHOP_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ plan_id: planId, metadata: { calmstats_visitor_id: visitorId } }),
});
```

Source: https://calmstats.com/docs/whop

---

# Shopify

Attribute Shopify orders and refunds to the visits that earned them.

Shopify works differently from other providers: you create a small private app for your store, and Calmstats does the rest. It installs the script on your storefront, imports the last 30 days of paid orders, registers order and refund webhooks, and syncs every day.

## Connect your store

1. Open dev.shopify.com (Shopify Dev Dashboard), signed in as the store owner, and click Create app. Name it Calmstats.
2. Under Access → Scopes, add read_orders and write_script_tags, then click Release.
3. On the app's Home, click Install app, pick your store and approve the install.
4. In the app's Settings, copy the Client ID and Client secret and paste them in **Settings → Revenue** with your .myshopify.com domain.
5. If you added the Calmstats script to your theme by hand, you can remove it: the connection installs it for you.

Use your store's `.myshopify.com` domain, not a custom domain.

## What happens next

- The visitor is saved on the Shopify cart, so orders are attributed with no checkout code.
- Refunds are subtracted automatically, and test orders are skipped.
- Disconnecting removes the webhooks and the script from your store.

Source: https://calmstats.com/docs/shopify

---

# Script options

Every attribute the script tag takes: Do Not Track, excluded paths, single-page apps and more.

The script tag takes these optional attributes:

| Attribute | Example | What it does |
| --- | --- | --- |
| `data-respect-dnt` | `data-respect-dnt="true"` | Skip visitors whose browser sends Do Not Track. |
| `data-spa-tracking` | `data-spa-tracking="false"` | Turn off pageviews on client-side URL changes (on by default for single-page apps). |
| `data-exclude` | `data-exclude="/app/**,/share/*"` | Stop tracking on matching paths; `*` is one path segment, `**` is anything. |
| `data-scroll-depth` | `data-scroll-depth="false"` | Turn off scroll depth (on by default). |
| `data-host-url` | `data-host-url="https://stats.example.com"` | Send events to a different origin than the one serving the script. |

## Exclude pages

`data-exclude` takes a comma-separated list of paths. `*` matches one path segment and `**` matches anything:

```html
<script defer
  data-website-id="YOUR-SITE-ID"
  data-exclude="/app/**,/admin/**,/preview/*"
  src="https://calmstats.com/script.js"></script>
```

Excluded pages send nothing, including events, and moving in and out of an excluded area in a single-page app is handled.

## Respect Do Not Track

`data-respect-dnt="true"` skips visitors whose browser sends the Do Not Track signal. You can also switch it on in **Settings → Tracking**.

## Settings in the script URL

Where you can't add attributes, as in some tag managers, put settings in the URL with underscores instead of dashes:

```html
<script defer src="https://calmstats.com/script.js?website_id=YOUR-SITE-ID&respect_dnt=true"></script>
```

Source: https://calmstats.com/docs/script-options

---

# Track AI crawlers

Count GPTBot, ClaudeBot, PerplexityBot and other AI crawlers from your server.

AI crawlers such as GPTBot and ClaudeBot don't run JavaScript, so the script never sees them. A small snippet on your server checks each request's user agent and reports AI crawler hits to Calmstats, so the **AI Traffic** report shows which crawlers read which pages.

You'll need an API key from **Settings → API keys**. Only the site owner's key can report hits.

## Next.js

1. Set the API key as CALMSTATS_API_KEY in your app's environment.
2. Save this as proxy.ts next to your app folder, or merge it into the one you have.
3. Deploy.

```ts title="proxy.ts"
import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server';

const AI_CRAWLERS = /GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|meta-externalagent|meta-externalfetcher|Bytespider|Amazonbot|CCBot|cohere-training-data-crawler|cohere-ai|MistralAI-User|DuckAssistBot|YouBot/i;

export function proxy(request: NextRequest, event: NextFetchEvent) {
  const userAgent = request.headers.get('user-agent') ?? '';
  if (AI_CRAWLERS.test(userAgent) && process.env.CALMSTATS_API_KEY) {
    event.waitUntil(
      fetch('https://calmstats.com/api/ai-bots', {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          authorization: `Bearer ${process.env.CALMSTATS_API_KEY}`,
        },
        body: JSON.stringify({
          website_id: 'YOUR-SITE-ID',
          hits: [{
            user_agent: userAgent,
            path: request.nextUrl.pathname,
            ip: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim(),
            timestamp: new Date().toISOString(),
          }],
        }),
      }).catch(() => {}),
    );
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
```

## WordPress

1. Add define('CALMSTATS_API_KEY', 'your key'); to wp-config.php.
2. Save this as wp-content/mu-plugins/calmstats-ai-crawlers.php (create the mu-plugins folder if it is missing). It loads automatically.
3. If a page cache such as Cloudflare, WP Rocket or your host's cache serves crawlers before WordPress runs, use the Cloudflare Worker instead.

```php title="calmstats-ai-crawlers.php"
<?php
/**
 * Plugin Name: Calmstats AI crawlers
 * Description: Reports AI crawler visits to Calmstats. Crawlers don't run JavaScript.
 */

add_action('shutdown', function () {
    if (!defined('CALMSTATS_API_KEY') || empty($_SERVER['HTTP_USER_AGENT'])) {
        return;
    }
    $user_agent = (string) $_SERVER['HTTP_USER_AGENT'];
    if (!preg_match('/GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|meta-externalagent|meta-externalfetcher|Bytespider|Amazonbot|CCBot|cohere-training-data-crawler|cohere-ai|MistralAI-User|DuckAssistBot|YouBot/i', $user_agent)) {
        return;
    }
    wp_remote_post('https://calmstats.com/api/ai-bots', array(
        'blocking' => false,
        'timeout'  => 2,
        'headers'  => array(
            'Content-Type'  => 'application/json',
            'Authorization' => 'Bearer ' . CALMSTATS_API_KEY,
        ),
        'body' => wp_json_encode(array(
            'website_id' => 'YOUR-SITE-ID',
            'hits' => array(array(
                'user_agent'  => substr($user_agent, 0, 512),
                'path'        => substr(strtok($_SERVER['REQUEST_URI'] ?? '/', '?'), 0, 2048),
                'status_code' => http_response_code() ?: 200,
                'ip'          => $_SERVER['REMOTE_ADDR'] ?? null,
                'timestamp'   => gmdate('c'),
            )),
        )),
    ));
});
```

## Cloudflare

1. For any site whose domain is proxied by your own Cloudflare account, whatever it is built with.
2. Create a Worker with this code and add CALMSTATS_API_KEY as a secret (Settings → Variables and Secrets).
3. Add a route for your domain, such as example.com/*, so the Worker runs in front of the site.

```js title="worker.js"
const AI_CRAWLERS = /GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|meta-externalagent|meta-externalfetcher|Bytespider|Amazonbot|CCBot|cohere-training-data-crawler|cohere-ai|MistralAI-User|DuckAssistBot|YouBot/i;

export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    const userAgent = request.headers.get('user-agent') || '';
    if (AI_CRAWLERS.test(userAgent) && env.CALMSTATS_API_KEY) {
      ctx.waitUntil(
        fetch('https://calmstats.com/api/ai-bots', {
          method: 'POST',
          headers: {
            'content-type': 'application/json',
            authorization: `Bearer ${env.CALMSTATS_API_KEY}`,
          },
          body: JSON.stringify({
            website_id: 'YOUR-SITE-ID',
            hits: [{
              user_agent: userAgent,
              path: new URL(request.url).pathname,
              status_code: response.status,
              ip: request.headers.get('cf-connecting-ip') || undefined,
              timestamp: new Date().toISOString(),
            }],
          }),
        }).catch(() => {}),
      );
    }
    return response;
  },
};
```

## Node.js

1. Needs Node 18 or newer (built-in fetch). Set CALMSTATS_API_KEY in the server environment.
2. Add the middleware with app.use(calmstatsAiCrawlers) before your routes.
3. Deploy.

```js title="calmstats-ai-crawlers.js"
const AI_CRAWLERS = /GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|meta-externalagent|meta-externalfetcher|Bytespider|Amazonbot|CCBot|cohere-training-data-crawler|cohere-ai|MistralAI-User|DuckAssistBot|YouBot/i;

// app.use(calmstatsAiCrawlers) before your routes.
function calmstatsAiCrawlers(req, res, next) {
  const userAgent = req.headers['user-agent'] || '';
  if (AI_CRAWLERS.test(userAgent) && process.env.CALMSTATS_API_KEY) {
    res.on('finish', () => {
      fetch('https://calmstats.com/api/ai-bots', {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          authorization: `Bearer ${process.env.CALMSTATS_API_KEY}`,
        },
        body: JSON.stringify({
          website_id: 'YOUR-SITE-ID',
          hits: [{
            user_agent: userAgent,
            path: req.path || req.url.split('?')[0],
            status_code: res.statusCode,
            ip: (req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket.remoteAddress,
            timestamp: new Date().toISOString(),
          }],
        }),
      }).catch(() => {});
    });
  }
  next();
}

module.exports = { calmstatsAiCrawlers };
```

## Recognised crawlers

| Crawler | Operator | Purpose |
| --- | --- | --- |
| GPTBot | OpenAI | training |
| OAI-SearchBot | OpenAI | search |
| ChatGPT-User | OpenAI | user |
| ClaudeBot | Anthropic | training |
| Claude-SearchBot | Anthropic | search |
| Claude-User | Anthropic | user |
| anthropic-ai | Anthropic | training |
| PerplexityBot | Perplexity | search |
| Perplexity-User | Perplexity | user |
| Meta-ExternalAgent | Meta | training |
| Meta-ExternalFetcher | Meta | user |
| Bytespider | ByteDance | training |
| Amazonbot | Amazon | search |
| CCBot | Common Crawl | training |
| Cohere crawler | Cohere | training |
| cohere-ai | Cohere | user |
| MistralAI-User | Mistral | user |
| DuckAssistBot | DuckDuckGo | user |
| YouBot | You.com | search |

Shopify, Wix and Squarespace don't run your own server code, so they can't report crawlers unless your domain runs through your own Cloudflare account.

Source: https://calmstats.com/docs/ai-crawlers

---

# Connect Google Search Console

See the searches that bring visitors, and the revenue each keyword leads to.

Connect Google Search Console to see the searches that bring visitors, with clicks, impressions and position, next to the visits, conversions and revenue each keyword led to.

1. Go to **Settings → Google Search**.
2. Click **Connect with Google** and sign in with the Google account that has access to your Search Console property.
3. Choose the property for this website.

Calmstats imports up to 16 months of history and then syncs daily. Access is read-only. See the [privacy notice](https://calmstats.com/privacy#google-search-console) for what is stored.

Source: https://calmstats.com/docs/search-console

---

# Embeddable widgets

Show live visitors or a traffic chart on your own site.

Widgets put live Calmstats data on any website in an `<iframe>`: a **Realtime** visitor count, a **Preview** of your stats, or a traffic **Chart**.

1. Go to **Settings → Widgets** and switch **Widgets** on. They are off by default.
2. Pick a widget and a theme (system, light or dark).
3. Copy the embed code as HTML or React and paste it into your page.

Anyone who can see that page sees the numbers. Revenue appears only in the Preview widget, and only if you switch on **Show revenue in the Preview widget**.

Source: https://calmstats.com/docs/widgets

---

# Share a public dashboard

Share a read-only link to your dashboard.

Share a read-only link to your dashboard, for your team, investors or an open startup page.

1. Go to **Settings → Public sharing**.
2. Switch on **Public dashboard** and copy the link.

Anyone with the link can view reports. Revenue credentials, customer emails and settings stay private. Switch it off at any time to stop the link working.

Source: https://calmstats.com/docs/public-dashboard

---

# Visits are not showing

Check the install when the dashboard shows no visits.

## Check the domain

Calmstats only counts visits sent from the domain you added for the website (`www.` and the bare domain count as the same). Visits from `localhost`, preview deployments or another domain are ignored. Check the domain in **Settings → General**.

## Check the script

1. Open your site and view the page source. The script tag should be in the `<head>` with your website ID.
2. Click **Verify installation** in **Settings → Tracking**.
3. Open your browser's developer tools, go to the **Network** tab, and reload. You should see a request to `/api/collect`.

## Common causes

- **Ad blockers**: some block analytics scripts. Test in a private window with extensions off.
- **Excluded paths**: a `data-exclude` pattern may match the page.
- **Do Not Track**: with `data-respect-dnt="true"`, browsers that send DNT aren't counted.
- **Caching or optimization plugins**: they can delay, combine or drop scripts. Exclude `script.js`.
- **Paused website or monthly limit**: a paused site, or a Free plan past its monthly events, stops collecting until it's resumed or the month resets.

Source: https://calmstats.com/docs/script-not-tracking

---

# Revenue is not showing

Check the payment connection when payments are missing or not attributed.

## No payments at all

- Check the key in **Settings → Revenue**: it needs the read permissions listed in your provider's guide.
- Check the webhook endpoint and its events in your provider's dashboard. Without webhooks, payments still arrive with the daily sync.
- Calmstats imports the last 30 days of payments when you connect; older payments aren't imported.

## Payments show up but aren't attributed

The payment arrived without a visitor ID. Make sure your checkout passes `window.calmstats.getVisitorId()` as `calmstats_visitor_id` metadata; see [Connect payment providers](https://calmstats.com/docs/connect-payments). The visitor must have visited your site with the script before paying.

## Amounts look wrong

Revenue is net of refunds, and each currency is shown separately, never converted.

Source: https://calmstats.com/docs/revenue-not-showing
