VisitorType

Server-side tracking

Most AI crawlers — GPTBot, ClaudeBot, PerplexityBot, CCBot — request your HTML and leave without executing JavaScript, so a browser snippet alone can never see them. Server-side tracking closes the gap: your backend (or edge) reports every request it serves, and AI Tag Manager classifies it. Events arrive with source: server in analytics.

Which layer catches what

The two collection layers see different halves of your traffic — run both:

Browser snippet: humans (with engagement) and agentic browsers — AI driving a real browser looks human to a server, and is caught by client-side signals (webdriver flags, headless tells) only the snippet can read. Server-side: crawlers and assistant fetchers that never execute JavaScript. Turning the snippet off and reporting server-side with includeHumans: truecounts humans but classifies agentic browsers as human — that's the layer you switched off doing its job elsewhere. With both layers on, leave includeHumans off (the default) or humans are counted twice.

The endpoint

POST https://visitortype.ai/api/c/AITM-XXXXXXX/server-collect
Content-Type: application/json

{ "userAgent": "<the request's User-Agent>", "path": "/pricing", "referrer": "" }

// or batched (up to 500):
{ "events": [ { "userAgent": "...", "path": "/a" }, { "userAgent": "...", "path": "/b" } ] }
Human-classified events are not stored by this endpoint — the browser snippet already records humans, so storing them here would count every visit twice. Server-side tracking exists to catch the visitors that never run JavaScript (AI crawlers, assistants, monitors). Running server-side only, without the snippet? Add "includeHumans": true to the payload to keep human page views.

Next.js (middleware)

Copy integrations/nextjs-middleware.ts into your project. It reports via event.waitUntil, adding zero latency to responses:

export function middleware(req: NextRequest, event: NextFetchEvent) {
  event.waitUntil(
    fetch(`${VISITORTYPE_HOST}/api/c/${CONTAINER_ID}/server-collect`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        userAgent: req.headers.get("user-agent") ?? "",
        path: req.nextUrl.pathname,
        referrer: req.headers.get("referer") ?? "",
      }),
    }).catch(() => undefined)
  );
  return NextResponse.next();
}
Recent Next.js versions renamed middleware.ts to proxy.ts — the code is the same, only the filename changes.

Page caches & CDNs: know your capture point

Server-side adapters report the requests that reach your application. A full-page cache (WP Rocket, LiteSpeed, W3TC, a CDN's HTML cache) answers anonymous requests before your application runs — so a cached WordPress site records its logged-in admins (cache is bypassed for them) while the crawlers you actually want to see hit the cached copy and never reach PHP. If bots seem invisible but your own logged-in tests work, this is almost always why.

On cached sites, capture in front of the cache — the Cloudflare Worker below sees every request, cache hits included. And exclude tms.jsfrom any “delay JavaScript” page-speed optimization, or human visits that bounce before interacting are never recorded.

Cloudflare Workers

integrations/cloudflare-worker.js runs in front of any origin — no code changes to the site itself. It passes requests through and reports page-like URLs via ctx.waitUntil.

Express / Node

integrations/express-middleware.jsbuffers events and ships them in batches every 5 seconds, so busy sites don't pay one outbound request per page view:

const { aitmTrack } = require("./aitm-express");
app.use(aitmTrack);

Anything else

Any environment that can send an HTTP POST works — nginx mirror, a log shipper, a cron job over access logs. Skip static assets (images, CSS, JS) and send page-like requests only.