Documentation

Everything you need to get CWho tracking your sites.

Installation

Add the CWho tracking snippet to every page you want to track. Place it inside the <head> of your HTML. The snippet is under 1KB minified, loads asynchronously, and never blocks rendering.

Basic snippet

Go to your site's settings in the CWho dashboard to copy your unique snippet with your data-site key pre-filled:

<script
  defer
  data-site="YOUR_TRACKING_KEY"
  src="https://cwho.com/t/cwho.min.js"
></script>
Tip: Place the snippet in your <head>, not at the bottom of <body>. The defer attribute means it won't block page rendering regardless of placement.

WordPress

Add the snippet to your theme's header.php, or use a plugin like Insert Headers and Footers to add it site-wide without editing theme files.

Shopify

Go to Online Store → Themes → Edit Code and add the snippet to your theme.liquid file inside the <head> section.

Next.js / React

Add the snippet to your custom _document.js or use the <Script> component in your layout:

import Script from 'next/script';

export default function Layout({ children }) {
  return (
    <>
      <Script
        defer
        data-site="YOUR_TRACKING_KEY"
        src="https://cwho.com/t/cwho.min.js"
        strategy="afterInteractive"
      />
      {children}
    </>
  );
}

Verifying Installation

After installing the snippet, visit your site from a different browser or incognito window. Then check your CWho dashboard — you should see a real-time visitor within seconds.

You can also verify manually:

  1. Open your browser DevTools (F12) and go to the Network tab.
  2. Filter by collect.
  3. Reload your page. You should see a POST request to https://cwho.com/api/collect.
  4. Click the request — a 200 OK response confirms tracking is working.
No data showing up? Check that your data-site key matches the tracking key shown in the CWho dashboard. Keys are case-sensitive.

Single-Page Apps

CWho automatically detects client-side navigation via the popstate event and history.pushState / history.replaceState overrides. For most SPAs (React Router, Vue Router, Next.js), no extra configuration is needed.

If you're using a custom routing system, you can manually trigger a pageview:

// Trigger a manual pageview (e.g. after router navigation)
window.cwho.trackPageview();

// Or with custom URL and title
window.cwho.trackPageview({
  url: '/dashboard/reports',
  title: 'Reports'
});

Custom Events

Track any user action — button clicks, form submissions, purchases, video plays — with cwho.track().

Basic custom event

// Simple event with just a name
window.cwho.track('signup_completed');

Event with properties

// Event with a value property (used for goal tracking)
window.cwho.track('purchase', { value: '99.00' });

// Event with a descriptive value
window.cwho.track('plan_selected', { value: 'pro' });

Tracking a button click

<button onclick="window.cwho.track('cta_clicked', {value: 'hero_signup'})">
  Sign up free
</button>
Note: Custom events require the CWho snippet to be loaded first. If the snippet hasn't loaded yet (e.g., very early page load), the call is safely queued and sent once the snippet initialises.

Outbound Link Tracking

CWho automatically captures outbound link clicks (links to other domains) as outbound_click events. The destination URL is stored as the event value. No configuration required.

You can disable automatic outbound tracking with the data-track-outbound="false" snippet attribute (see Snippet Options).

Goals & Conversions

Goals let you define and track specific actions as conversions. Create goals in your site's settings under the Goals tab.

Three goal types are supported:

  • Pageview goal — triggered when a visitor views a specific URL (e.g. /thank-you). No code changes needed.
  • Event goal — triggered when a matching custom event is fired. Use cwho.track('event_name').
  • Outbound click goal — triggered when a visitor clicks an outbound link matching a pattern.

Snippet Options

Customise snippet behaviour with data-* attributes:

Attribute Default Description
data-site required Your unique tracking key. Found in site settings.
data-track-outbound "true" Set to "false" to disable automatic outbound link tracking.
data-exclude "false" Set to "true" to exclude the current browser from tracking (stores a localStorage flag).
data-api-host "https://cwho.com" Override the API host. Only needed for self-hosted deployments.

Excluding Yourself

To prevent your own visits from being counted, run this in your browser console on your site. It sets a localStorage flag that tells the snippet to ignore all tracking for that browser:

window.localStorage.setItem('cwho_exclude', '1');

To re-enable tracking in that browser:

window.localStorage.removeItem('cwho_exclude');

FAQ

Does CWho use cookies?

No. CWho is completely cookie-free. Visitor identification uses a daily-rotating hash of the IP address and User-Agent string. The hash is never stored with the IP address, and it changes every day, making long-term fingerprinting impossible. No consent banners are required.

Is CWho GDPR-compliant?

Yes. CWho does not collect any Personally Identifiable Information (PII). IP addresses are used only for GeoIP lookup and are never stored. You do not need to add CWho to your GDPR consent flow.

Will the snippet slow down my site?

No. The snippet is under 1KB minified, loaded with defer, and fires after the page has finished rendering. It has no measurable impact on Core Web Vitals or Lighthouse scores.

Can I track multiple domains with one account?

Yes. Each domain is a separate "site" in CWho, with its own tracking key. Add as many sites as your plan allows from the dashboard.

How do I track events before the snippet loads?

CWho creates a queue (window.cwho = window.cwho || []) before the script loads. Any calls to window.cwho.track() made before the snippet initialises are stored in the queue and flushed automatically once loaded.

Can I use CWho on a localhost / staging site?

Yes. Events from localhost, 127.0.0.1, and private IP ranges are accepted but tagged as internal in your dashboard so you can filter them out.

Where can I get help?

Email us at support@cwho.com. We typically respond within one business day.

REST API

CWho provides a REST API for programmatic access to your sites, analytics, and uptime data. The API is available on Pro and Agency plans.

Plan required: The REST API requires a Pro or Agency subscription. Free and Starter plan users will receive a 403 Forbidden response.

Base URL for all API requests:

https://cwho.com/api/v1

All responses are JSON. All timestamps are ISO 8601 in UTC.

Authentication

Authenticate by passing your API key in the X-API-Key request header. Find your API key under Settings → API Key in the dashboard. Keep your API key secret — it has read access to all your sites and data.

# Example: authenticated request with curl
curl https://cwho.com/api/v1/sites \
  -H "X-API-Key: YOUR_API_KEY"
Security tip: Never expose your API key in client-side JavaScript or public repositories. Rotate your key immediately from Settings if you suspect it has been compromised.

Error responses

All errors follow a consistent shape:

{
  "error": "Unauthorized",
  "message": "Invalid or missing API key.",
  "status": 401
}
HTTP StatusMeaning
200Success
400Bad request — missing or invalid parameter
401Unauthorized — missing or invalid API key
403Forbidden — plan does not include API access
404Resource not found (or not owned by your account)
429Rate limited — slow down requests
500Internal server error

Sites

List all sites

Returns all sites associated with your account.

GET /api/v1/sites
# Request
curl https://cwho.com/api/v1/sites \
  -H "X-API-Key: YOUR_API_KEY"

# Response 200
{
  "sites": [
    {
      "id": 42,
      "name": "My Blog",
      "domain": "example.com",
      "tracking_key": "abc123def456",
      "current_status": "up",
      "current_response_ms": 214,
      "visitors_today": 1082,
      "last_check_at": "2026-04-07T10:30:00Z",
      "created_at": "2025-11-15T09:00:00Z"
    }
  ],
  "total": 1
}

Get a single site

Returns details for one site by its ID.

GET /api/v1/sites/:id
# Request
curl https://cwho.com/api/v1/sites/42 \
  -H "X-API-Key: YOUR_API_KEY"

# Response 200
{
  "id": 42,
  "name": "My Blog",
  "domain": "example.com",
  "tracking_key": "abc123def456",
  "current_status": "up",
  "current_response_ms": 214,
  "visitors_today": 1082,
  "visitors_yesterday": 937,
  "ssl_expires_at": "2026-09-12T00:00:00Z",
  "domain_expires_at": "2027-03-01T00:00:00Z",
  "last_check_at": "2026-04-07T10:30:00Z",
  "created_at": "2025-11-15T09:00:00Z"
}

Analytics

Get analytics summary

Returns aggregated analytics data for a site over the specified number of days. Includes daily pageview counts, top pages, referrers, countries, and device breakdown.

GET /api/v1/sites/:id/analytics?days=7
ParameterTypeDefaultDescription
days integer 7 Number of days to look back. Allowed values: 1, 7, 30, 90. Limited by your plan's retention period.
# Request
curl "https://cwho.com/api/v1/sites/42/analytics?days=7" \
  -H "X-API-Key: YOUR_API_KEY"

# Response 200
{
  "site_id": 42,
  "period_days": 7,
  "summary": {
    "pageviews": 8241,
    "unique_visitors": 3107,
    "sessions": 4512,
    "bounce_rate_pct": 42.3,
    "avg_duration_ms": 94500
  },
  "pageviews_by_day": [
    { "date": "2026-04-01", "pageviews": 1102, "visitors": 410 },
    { "date": "2026-04-02", "pageviews": 987,  "visitors": 371 }
  ],
  "top_pages": [
    { "path": "/",           "pageviews": 2100 },
    { "path": "/blog",       "pageviews": 940  }
  ],
  "top_referrers": [
    { "domain": "google.com",  "visitors": 820  },
    { "domain": "twitter.com", "visitors": 214  }
  ],
  "top_countries": [
    { "country_code": "US", "visitors": 1540 },
    { "country_code": "GB", "visitors": 312  }
  ],
  "devices": {
    "desktop": 58.2,
    "mobile":  36.5,
    "tablet":  5.3
  }
}

Uptime

Get uptime summary

Returns uptime statistics, recent check results, and open incidents for a site.

GET /api/v1/sites/:id/uptime?days=30
ParameterTypeDefaultDescription
days integer 30 Number of days to look back. Allowed values: 1, 7, 30, 90.
# Request
curl "https://cwho.com/api/v1/sites/42/uptime?days=30" \
  -H "X-API-Key: YOUR_API_KEY"

# Response 200
{
  "site_id": 42,
  "period_days": 30,
  "monitors": [
    {
      "id": 7,
      "url": "https://example.com",
      "current_status": "up",
      "uptime_pct_30d": 99.87,
      "avg_response_ms": 218,
      "last_check_at": "2026-04-07T10:30:00Z"
    }
  ],
  "incidents": [
    {
      "id": 3,
      "monitor_id": 7,
      "started_at": "2026-03-22T14:05:00Z",
      "resolved_at": "2026-03-22T14:23:00Z",
      "duration_seconds": 1080,
      "cause": "Connection refused"
    }
  ],
  "recent_checks": [
    {
      "monitor_id": 7,
      "status": "up",
      "response_time_ms": 204,
      "status_code": 200,
      "checked_at": "2026-04-07T10:30:00Z"
    }
  ]
}
Rate limits: The REST API is limited to 600 requests per minute per API key. If you exceed this, you will receive a 429 Too Many Requests response. The Retry-After header indicates how many seconds to wait.