Introduction

Convrs is a lightweight website analytics platform built for modern SaaS products.

Add a single script to your website and start tracking visitors, pageviews, conversions, custom events, and revenue attribution in real time.

See the Quickstart guide to get set up in under two minutes.

Quick Start

Get started with Convrs in under two minutes.

1. Create a Website

Create a website in your Convrs dashboard and copy your Website ID.

2. Add the Script

Paste the script into your website's <head> or before the closing </body> tag.

1
2
3
4
5
<script
  async
  src="https://convrs.dev/script.js"
  data-website-id="YOUR_WEBSITE_ID">
</script>

3. Open Your Dashboard

Visit your Convrs dashboard and start exploring your analytics.

Data typically appears within a few seconds of the first pageview.

Replace YOUR_WEBSITE_ID with the Website ID from your Convrs dashboard.

HTML & Vanilla JS

The simplest installation — add the Convrs tracking script anywhere in your HTML.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
  <head>
    <title>My Site</title>

    <!-- Convrs Analytics -->
    <script
      async
      src="https://convrs.dev/script.js"
      data-website-id="YOUR_WEBSITE_ID">
    </script>
  </head>

  <body>
    <!-- Your content -->
  </body>
</html>

Using async ensures the script never blocks page rendering.

Next.js

Use the built-in Next.js Script component so Convrs loads with the correct strategy.

App Router (Next.js 13+)

Add this to your app/layout.tsx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Script from 'next/script';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}

        <Script
          src="https://convrs.dev/script.js"
          data-website-id="YOUR_WEBSITE_ID"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Pages Router

Add this to pages/_app.tsx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Script from 'next/script';
import type { AppProps } from 'next/app';

export default function App({
  Component,
  pageProps,
}: AppProps) {
  return (
    <>
      <Component {...pageProps} />

      <Script
        src="https://convrs.dev/script.js"
        data-website-id="YOUR_WEBSITE_ID"
        strategy="afterInteractive"
      />
    </>
  );
}

Convrs automatically tracks client-side route changes in Next.js.

React (Vite / CRA)

Install Convrs in your React application to start tracking pageviews, visitors, and custom events automatically.

The simplest approach is to add the Convrs script directly to your index.html file.

Vite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My App</title>
  </head>

  <body>
    <div id="root"></div>

    <script
      async
      src="https://convrs.dev/script.js"
      data-website-id="YOUR_WEBSITE_ID">
    </script>

    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Create React App

1
2
3
4
5
6
7
8
9
10
11
<!-- public/index.html -->

<body>
  <div id="root"></div>

  <script
    async
    src="https://convrs.dev/script.js"
    data-website-id="YOUR_WEBSITE_ID">
  </script>
</body>

Option B — Load via useEffect

If you prefer to load Convrs dynamically, add the script once when your application mounts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useEffect } from 'react';

export default function App() {
  useEffect(() => {
    const script = document.createElement('script');

    script.src = 'https://convrs.dev/script.js';
    script.async = true;

    script.setAttribute(
      'data-website-id',
      'YOUR_WEBSITE_ID'
    );

    document.head.appendChild(script);

    return () => {
      document.head.removeChild(script);
    };
  }, []);

  return <div>Your App</div>;
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
<body>
  <div id="app"></div>

  <script
    async
    src="https://convrs.dev/script.js"
    data-website-id="YOUR_WEBSITE_ID">
  </script>

  <script type="module" src="/src/main.ts"></script>
</body>

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
import { onMounted } from 'vue';

onMounted(() => {
  const script = document.createElement('script');

  script.src = 'https://convrs.dev/script.js';
  script.async = true;

  script.setAttribute(
    'data-website-id',
    'YOUR_WEBSITE_ID'
  );

  document.head.appendChild(script);
});
</script>

<template>
  <RouterView />
</template>

Convrs automatically tracks Vue Router navigation.

Nuxt.js

Use Nuxt's built-in head management to load Convrs on every page.

1
2
3
4
5
6
7
8
9
10
11
12
13
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: 'https://convrs.dev/script.js',
          'data-website-id': 'YOUR_WEBSITE_ID',
          async: true,
        },
      ],
    },
  },
});

app.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
useHead({
  script: [
    {
      src: 'https://convrs.dev/script.js',
      'data-website-id': 'YOUR_WEBSITE_ID',
      async: true,
    },
  ],
});
</script>

<template>
  <NuxtPage />
</template>

Convrs automatically tracks Nuxt route changes.

SvelteKit

Add the Convrs script to your root layout.

1
2
3
4
5
6
7
8
9
10
11
<!-- src/routes/+layout.svelte -->

<svelte:head>
  <script
    async
    src="https://convrs.dev/script.js"
    data-website-id="YOUR_WEBSITE_ID">
  </script>
</svelte:head>

<slot />

Convrs automatically tracks client-side navigation and page changes.

Remix

Add Convrs to your root application layout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />

        <script
          async
          src="https://convrs.dev/script.js"
          data-website-id="YOUR_WEBSITE_ID"
        />
      </head>

      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Convrs automatically tracks route transitions in Remix applications.

Astro

Add Convrs to your base layout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
---
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />

    <script
      async
      src="https://convrs.dev/script.js"
      data-website-id="YOUR_WEBSITE_ID">
    </script>

    <slot name="head" />
  </head>

  <body>
    <slot />
  </body>
</html>

If you're using Astro View Transitions, add transition:persist to the script tag to prevent reinitialization.

Gatsby

Install Convrs globally using Gatsby SSR APIs.

gatsby-ssr.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";

export const onRenderBody = ({
  setHeadComponents,
}) => {
  setHeadComponents([
    <script
      key="convrs"
      async
      src="https://convrs.dev/script.js"
      data-website-id="YOUR_WEBSITE_ID"
    />,
  ]);
};

gatsby-browser.js

1
2
// Convrs automatically tracks route changes.
// No additional setup is required.

Gatsby uses the History API, which Convrs tracks automatically.

WordPress

There are two ways to install Convrs on WordPress.

Option A — functions.php

1
2
3
4
5
function convrs_analytics_script() {
    echo '<script async src="https://convrs.dev/script.js" data-website-id="YOUR_WEBSITE_ID"></script>';
}

add_action('wp_head', 'convrs_analytics_script');

Option B — Plugin

Install a plugin such as:

  • Insert Headers and Footers
  • WPCode

Then paste:

1
2
3
4
5
<script
  async
  src="https://convrs.dev/script.js"
  data-website-id="YOUR_WEBSITE_ID">
</script>

into the Header section.

If you're using caching plugins such as WP Rocket or W3 Total Cache, ensure the script remains present in cached pages.

Webflow

Webflow allows custom code to be added globally across your site.

Installation

  1. Open your Webflow project.
  2. Click Project Settings.
  3. Open the Custom Code tab.
  4. Paste the following into Head Code:
1
2
3
4
5
<script
  async
  src="https://convrs.dev/script.js"
  data-website-id="YOUR_WEBSITE_ID">
</script>
  1. Save changes.
  2. Republish your website.

Convrs will immediately begin tracking pageviews once your site is published.

Shopify

Install Convrs across your entire Shopify storefront.

Installation

  1. Open your Shopify Admin.
  2. Navigate to Online Store → Themes.
  3. Select Actions → Edit Code.
  4. Open layout/theme.liquid.
  5. Paste the following before the closing </head> tag:
1
2
3
4
5
<script
  async
  src="https://convrs.dev/script.js"
  data-website-id="YOUR_WEBSITE_ID">
</script>
  1. Save the file.

Convrs automatically tracks storefront pages including product pages, collection pages, cart pages, and landing pages.

Convrs does not track activity inside the Shopify Admin dashboard.

NPM SDK

Install Convrs as an npm package for type-safe analytics tracking in JavaScript and TypeScript applications.

Perfect for React, Next.js, Vue, Nuxt, SvelteKit, Remix, Astro, and other modern web frameworks.

Why use the SDK?

  • TypeScript support
  • Framework agnostic
  • Automatic pageview tracking
  • User identification
  • Offline-ready

Installation

1
npm install @convrs/sdk

Quick Start

1
2
3
4
5
import { initConvrs } from "@convrs/sdk";

const convrs = await initConvrs({
  websiteId: "YOUR_WEBSITE_ID",
});

Track Events

1
2
3
convrs.track("signup", {
  source: "homepage",
});

Identify Users

1
2
3
4
convrs.identify("user_123", {
  email: "user@example.com",
  plan: "pro",
});

React / Next.js Example

lib/analytics.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { initConvrs } from "@convrs/sdk";

let convrs: any = null;

export async function getAnalytics() {
  if (!convrs) {
    convrs = await initConvrs({
      websiteId:
        process.env.NEXT_PUBLIC_CONVRS_WEBSITE_ID!,
      autoCapturePageviews: true,
    });
  }

  return convrs;
}

Automatic Pageview Tracking

1
2
3
4
const convrs = await initConvrs({
  websiteId: "YOUR_WEBSITE_ID",
  autoCapturePageviews: true,
});

Configuration Options

OptionTypeDescription
websiteIdstringYour Convrs website ID
domainstringOverride the current hostname
apiUrlstringCustom ingestion endpoint
debugbooleanEnable debug logging
allowLocalhostbooleanEnable localhost tracking
allowIframebooleanEnable tracking inside iframes
allowedHostnamesstring[]Domains used for cross-domain tracking

Example Configuration

1
2
3
4
5
6
7
8
9
const convrs = await initConvrs({
  websiteId: "YOUR_WEBSITE_ID",
  debug: true,
  allowLocalhost: true,

  autoCapturePageviews: {
    trackHashChanges: true,
  },
});

See the API Reference for track(), identify(), trackPageview(), flush(), reset(), and cross-domain tracking.

Script Options

The Convrs tracking script accepts several optional data-* attributes to customize its behavior.

AttributeRequiredDescription
data-website-idYesYour unique Website ID from the Convrs dashboard.
data-apiNoOverride the default tracking endpoint. Useful for custom domains, proxies, or self-hosted ingestion.
data-debugNoEnable debug logging in the browser console.
data-allow-localhostNoEnable tracking while running on localhost.

Example

1
2
3
4
5
6
7
8
9
<script
  async
  src="https://convrs.dev/script.js"
  data-website-id="YOUR_WEBSITE_ID"
  data-api="https://analytics.yourdomain.com/api/track"
  data-debug="true"
  data-allow-localhost="true"
  data-allow-file-protocol="true">
</script>

Only data-website-id is required. All other attributes are optional and should only be used when needed.

API Reference

The Convrs SDK exposes a simple API for tracking events, pageviews, and user identification.

track()

Track a custom event.

Syntax

1
convrs.track(eventName, properties?)

Parameters

ParameterTypeDescription
eventNamestringName of the event.
propertiesobjectOptional event metadata.

Examples

1
convrs.track("signup");
1
2
3
4
5
convrs.track("purchase", {
  amount: 99,
  currency: "USD",
  plan: "Pro",
});

identify()

Associate events with a user.

Syntax

1
convrs.identify(userId, traits?)

Parameters

ParameterTypeDescription
userIdstringUnique identifier for the user.
traitsobjectOptional user attributes.

Examples

1
convrs.identify("user_123");
1
2
3
4
5
convrs.identify("user_123", {
  email: "user@example.com",
  name: "John Doe",
  plan: "Pro",
});

trackPageview()

Manually track a pageview.

Most applications do not need this because pageviews are tracked automatically when autoCapturePageviews is enabled.

Track Current Page

1
convrs.trackPageview();

Track Custom Path

1
convrs.trackPageview("/pricing");

flush()

Immediately send all queued events.

Syntax

1
await convrs.flush();

Example

1
await convrs.flush();

Useful before:

  • Page unloads
  • Checkout redirects
  • Critical conversion events

reset()

Reset the current visitor and session.

Syntax

1
convrs.reset();

Example

1
2
3
function logout() {
  convrs.reset();
}

Useful when a user logs out.

getConvrsClient()

Access the initialized Convrs client anywhere in your application.

Syntax

1
2
3
import { getConvrsClient } from "@convrs/sdk";

const convrs = getConvrsClient();

Example

1
2
3
4
5
import { getConvrsClient } from "@convrs/sdk";

const convrs = getConvrsClient();

convrs.track("feature_used");

buildCrossDomainUrl()

Generate a URL containing Convrs tracking parameters.

Syntax

1
convrs.buildCrossDomainUrl(url);

Example

1
2
3
const url = convrs.buildCrossDomainUrl(
  "https://app.example.com/signup"
);

Result:

1
https://app.example.com/signup?_cv_vid=xxx&_cv_sid=xxx

getTrackingParams()

Retrieve the current visitor and session identifiers.

Syntax

1
const params = convrs.getTrackingParams();

Example

1
2
3
const params = convrs.getTrackingParams();

console.log(params);

Result:

1
2
3
4
{
  visitorId: "visitor_123",
  sessionId: "session_456"
}

Complete Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { initConvrs } from "@convrs/sdk";

const convrs = await initConvrs({
  websiteId: "YOUR_WEBSITE_ID",
});

convrs.identify("user_123", {
  plan: "Pro",
});

convrs.track("signup");

convrs.track("purchase", {
  amount: 99,
  currency: "USD",
});

await convrs.flush();

Tracking Events

Beyond automatic pageview tracking, Convrs lets you send custom events to measure important user actions throughout your application.

Custom events can be used to track:

  • Signups
  • Purchases
  • Feature usage
  • Form submissions
  • Button clicks
  • Checkout flow events
  • Marketing conversions

All custom events appear in your Convrs dashboard and can be used for funnels, conversion tracking, and revenue attribution.

Syntax

1
window.convrs(eventName, properties)
ParameterTypeDescription
eventNamestringName of the event to track.
propertiesobjectOptional metadata associated with the event.

Event names should be descriptive and consistent. Examples: signup, purchase, invite_sent, checkout_started.

Event Examples

Button Click

1
2
3
<button onclick="window.convrs('cta_clicked', { location: 'hero' })">
  Get Started
</button>

Form Submission

1
2
3
4
5
document
  .querySelector('#signup-form')
  .addEventListener('submit', () => {
    window.convrs('signup_submitted');
  });

React — Button Click

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function UpgradeButton() {
  function handleClick() {
    window.convrs('upgrade_clicked', {
      plan: 'pro',
      source: 'pricing_page',
    });

    // Open checkout
  }

  return (
    <button onClick={handleClick}>
      Upgrade to Pro
    </button>
  );
}

E-commerce — Add to Cart

1
2
3
4
5
6
window.convrs('add_to_cart', {
  product_id: 'sku_1234',
  product_name: 'Wireless Headphones',
  price: 79.99,
  currency: 'USD',
});

Purchase Completed

1
2
3
4
5
6
window.convrs('purchase', {
  order_id: 'order_123',
  amount: 99,
  currency: 'USD',
  plan: 'pro',
});

Video Played

1
2
3
4
5
6
7
document
  .querySelector('#hero-video')
  .addEventListener('play', () => {
    window.convrs('video_played', {
      video: 'hero_demo',
    });
  });

Checkout Started

1
2
3
4
window.convrs('checkout_started', {
  plan: 'pro',
  billing_cycle: 'monthly',
});

Invite Sent

1
2
3
window.convrs('invite_sent', {
  team_size: 5,
});

Event Properties

Properties provide additional context for an event.

1
2
3
4
5
window.convrs('signup', {
  plan: 'pro',
  source: 'pricing_page',
  campaign: 'summer_launch',
});

These properties can be used to better understand user behavior and conversion patterns.

Best Practices

  • Use lowercase event names.
  • Use snake_case for event names.
  • Keep event names consistent across your application.
  • Attach meaningful properties to important events.
  • Avoid sending sensitive user information.

Good examples:

1
2
3
4
window.convrs('signup');
window.convrs('checkout_started');
window.convrs('purchase');
window.convrs('invite_sent');

Avoid:

1
2
3
window.convrs('Clicked Button');
window.convrs('Event 1');
window.convrs('test');

Convrs automatically tracks pageviews and route changes. Custom events should be used for actions that represent meaningful user behavior.

Modern AI tools read your site before most humans do. ChatGPT fetches your pricing page to answer a question about it, Googlebot re-indexes your docs, GPTBot pulls your blog for training data — Bot traffic tracking shows you all of it, broken down by who's asking and why.

Bot traffic tracking is included in your Convrs plan. It isn't a separate add-on.

Get started

Install the server-side package, add one call to your backend, deploy. This runs separately from Convrs's normal browser tracking script — bots skip your frontend JavaScript, so this has to run server-side.

Install @convrs/ai-bot-sdk on npm

1. Install

1
npm install @convrs/ai-bot-sdk

2. Add it to your server

Most common setup — Next.js middleware:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// middleware.ts
import { createBotTrackingMiddleware } from "@convrs/ai-bot-sdk";

const trackBots = createBotTrackingMiddleware({
  siteId: "YOUR_SITE_ID",
});

export function middleware(
  request: Request,
  context: { waitUntil: (p: Promise<unknown>) => void }
) {
  trackBots(request, context);
}

export const config = {
  // Keep crawler-facing files reachable: robots.txt, llms.txt, sitemaps.
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Don't await trackBots(...). Call it, let it run in the background, and return your response immediately. Passing context lets the SDK use waitUntil internally so tracking never adds latency to the real response.

3. Deploy and check your dashboard

After deploying, open your Convrs dashboard and look for the Bot traffic card. Filter by AI Answers, Indexing, or Training, and switch between the timeline and the per-provider breakdown.

If nothing shows up right away, that's expected — you're waiting for a real crawler to visit, not a human pageview. Try curl-ing your own site with a known bot's user-agent to confirm the pipeline end to end (see Testing your setup below).

What gets tracked

Every detected bot is classified into one of four categories:

CategoryExampleWhat it tells you
AI answers (answer_agent)A user asks ChatGPT about your product; ChatGPT-User fetches your pricing page to answer accurately.Which pages AI assistants pull in real time to answer user questions.
Indexing (index_crawler)Googlebot or PerplexityBot requests a page to refresh a search or answer index.Who's discovering and re-indexing your content.
Training (training_crawler)GPTBot, ClaudeBot, Applebot, or Bytespider requests public pages.Who's collecting your content for model training or large datasets.
Other (other)Anything recognized as bot traffic that doesn't cleanly fit the three buckets above.Catch-all so nothing silently disappears from the dashboard.

Classification covers 20+ vendors out of the box — OpenAI, Anthropic, Google, Perplexity, Microsoft, Apple, Amazon, Meta, xAI, Mistral, Baidu, Alibaba, ByteDance, DeepSeek, Cohere, Common Crawl, and more. New vendors get added to the SDK's registry over time with no config change needed on your end.

If a crawler keeps requesting a page that doesn't exist — /pricing-old, /docs/quickstart — that's a signal worth acting on. It usually means users or agents expect that page to exist somewhere.

Crawler-facing files

These are always tracked when a known bot requests them, even though they'd normally be filtered out as "not a real page":

  • /robots.txt
  • /llms.txt and /llms-full.txt
  • /sitemap.xml and other sitemap files
  • Any path ending in .xml under /sitemap/ or /sitemaps/

Everything else under your default-ignored prefixes (/api, /_next, /static, /assets, and similar) and default-ignored file extensions (.css, .js, .png, etc.) is skipped, since those are near-never what a person actually means by "did a bot visit my site."

How it works

On every request, the SDK checks the method, the User-Agent, and the path — in that order, bailing out as early as possible — before deciding whether to send anything:

  1. Is this a GET/HEAD request? (configurable)
  2. Does the User-Agent match a known bot? If not, stop here — nothing is sent.
  3. Is the path a static asset, an ignored prefix, or over the length limit?
  4. Send a small event in the background and return immediately.

The event itself only carries the raw User-Agent, IP, URL, and status code — vendor and category are always re-derived on the server, not trusted from the client. That keeps a caller from spoofing, say, a training crawler as an answer agent, and means crawler definitions can be updated on Convrs's side without every integration needing a package bump.

Tracking is best-effort. If the ingest endpoint is slow or unreachable, the SDK gives up quietly after a short timeout — it will never delay or break your actual response.

Platform examples

Next.js middleware

See Get started above.

Express

1
2
3
4
5
6
import express from "express";
import { createExpressBotMiddleware } from "@convrs/ai-bot-sdk";

const app = express();

app.use(createExpressBotMiddleware({ siteId: "YOUR_SITE_ID" }));

The Express adapter calls next() immediately, then attaches a finish listener and sends the event after your response has already gone out — your request handler never waits on it.

Generic Request/Response handler

If your framework gives you a request object up front and a response object later:

1
2
3
4
5
6
7
8
import { withBotTracking } from "@convrs/ai-bot-sdk";

export const GET = withBotTracking(
  async (request: Request) => {
    return new Response("ok");
  },
  { siteId: "YOUR_SITE_ID" }
);

If you only have the request and not the eventual response, request-only tracking still captures which page the bot hit — it just won't include a status code:

1
2
3
4
5
import { trackBotRequest } from "@convrs/ai-bot-sdk";

export function middleware(request: Request, context: { waitUntil: Function }) {
  trackBotRequest(request, context, { siteId: "YOUR_SITE_ID" });
}

Custom hosts behind an internal hostname

If your runtime hands the SDK an internal address (localhost, 0.0.0.0) instead of your public one, set it explicitly:

1
2
3
4
trackBotRequest(request, context, {
  siteId: "YOUR_SITE_ID",
  publicOrigin: "https://example.com",
});

Convrs still validates the resulting hostname against your site configuration and preserves the original path and query string.

Use without Node.js

Any backend that can send an HTTPS POST can report crawler traffic directly — no npm package required.

1
2
3
POST https://ingest.convrs.dev/api/ai-crawls
Content-Type: application/json
Authorization: Bearer cvbot_...    (only if you've enabled required auth)

Request body:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "siteId": "YOUR_SITE_ID",
  "domain": "example.com",
  "url": "https://example.com/docs/get-started",
  "referrer": null,
  "bot": {
    "userAgent": "Mozilla/5.0 ... ChatGPT-User/1.0",
    "ip": "203.0.113.10",
    "statusCode": 200,
    "source": "server_middleware"
  }
}
FieldRequiredNotes
siteIdYesYour project token. websiteId is accepted as an alias.
urlYesAbsolute URL the crawler requested. Hostname must belong to your site.
domainRecommendedHostname that received the request. Falls back to the URL's own hostname if omitted.
bot.userAgentYesFull raw User-Agent string. Classification happens from this alone.
bot.ipRecommendedCrawler's source IP as seen by your server.
bot.statusCodeOptionalYour response status, 100-599. Omit if unavailable.
bot.sourceOptionalFree-form label for where this came from, e.g. server_middleware.

Don't send vendor, agentName, or category — even if you compute them yourself. They're always re-derived server-side and any value you send is ignored, by design.

A tracked-or-safely-ignored request returns 202 with a body indicating whether it was actually recognized as a bot:

1
{ "success": true, "tracked": true, "category": "training_crawler" }

or, for a user-agent that isn't a known bot:

1
{ "success": true, "tracked": false, "reason": "not_a_bot" }

Optional request authentication

Add a site-specific token without touching your existing integration:

  1. Open your Bot traffic card settings and generate a token.
  2. Add it to your SDK config or raw HTTP header.
  3. Once deployed, flip on Reject unauthenticated requests.
1
2
3
4
createBotTrackingMiddleware({
  siteId: "YOUR_SITE_ID",
  authToken: process.env.CONVRS_BOT_TOKEN,
});

Keep the cvbot_... token server-side only — never in frontend code, a public repo, logs, or a URL. Enforcement is off until you explicitly enable it, so adding a token doesn't change existing behavior on its own. Rotating or deleting the token takes effect immediately.

Filtering categories

By default every category is tracked. Turn individual ones off if you only care about specific crawler types:

1
2
3
4
5
6
7
createBotTrackingMiddleware({
  siteId: "YOUR_SITE_ID",
  skipAnswerAgents: true,
  skipIndexCrawlers: true,
  skipTrainingCrawlers: true,
  skipOtherBots: true,
});

Most sites should leave the defaults on and filter later in the dashboard — turning a category off here means that traffic is never sent at all, not just hidden.

Privacy checklist

  • Call the ingest endpoint (directly or via the SDK) only from your backend. Never from browser JavaScript, and never for regular human traffic.
  • The SDK only ever sends the fields listed above — it doesn't forward cookies, auth headers, request bodies, or other visitor data.
  • Prefer URLs without query parameters where you can. If query params carry anything sensitive, strip them before the URL reaches the SDK.
  • Treat any IP you pass as untrusted unless it's coming through a proxy you control that strips client-supplied forwarding headers.

Testing your setup

1
2
3
curl -X POST https://ingest.convrs.dev/api/ai-crawls \
  -H "Content-Type: application/json" \
  -d '{"siteId":"YOUR_SITE_ID","url":"https://example.com/","bot":{"userAgent":"GPTBot/1.1","source":"manual-test"}}'

Expect 202 with tracked: true. Then check your dashboard's Bot traffic card for the new entry — it should land within a few seconds.

Where to find the data

Open your dashboard and look for the Bot traffic card. Switch between AI Answers, Indexing, and Training tabs, see the timeline for the selected category, and check the provider breakdown on the side for exact counts per vendor.

Track social mentions on your analytics chart

See when your brand is mentioned on X directly on your main analytics chart — no more guessing whether a traffic spike came from social media or somewhere else.

Requires a Convrs Growth plan.

Why track mentions?

Knowing when — and where — people are talking about your product helps you:

  • Connect social buzz to revenue — see which mentions actually turned into traffic and sales, not just noise.
  • Catch viral moments early — spot a spike in mentions while it's happening, not two days later when you check analytics.
  • Time your launches — ship a campaign when conversation about your brand is already warm.

How it works

When someone mentions one of your tracked keywords on X, Convrs picks it up automatically and overlays it on your main analytics chart as social activity.

Hovering a day with activity shows:

  • The author's avatar, stacked alongside anyone else who mentioned you that day
  • A short preview of the post content, right in the chart tooltip
  • How that day's mentions line up against your visitor and revenue numbers

If there's more activity than fits in the tooltip preview, a View all button opens the full list for that day — every mention, with the full post content, author, and a link out to the original post.

X mentions

Convrs searches X for your tracked keywords and your domain, and surfaces any tweet that mentions your brand.

Set up mention tracking

  1. Go to Settings → Integrations.
  2. Find the Twitter / X card.
  3. Click the Mentions tab.
  4. Add the keywords you want tracked.

Keyword rules

  • 4–15 characters per keyword
  • Matching is case-insensitive
  • Include the @ symbol to track handle mentions (e.g. @convrsdev)
  • Avoid generic single words — they'll mostly surface noise, not signal

Your workspace's domain is tracked automatically — you don't need to add it as a keyword yourself.

A note on connection status

The Twitter / X card shows whether your workspace currently has an active X connection. You can still add keywords and link-attribution accounts before connecting — they'll just sit ready and start surfacing mentions as soon as the connection is active.

Best practices

  • Track your brand and product names — the highest-signal keywords you can add.
  • Track campaign hashtags — add a hashtag right when a campaign launches so you catch the conversation from day one.
  • Skip generic terms — words like "analytics" or "software" will flood your mention feed with unrelated noise.

Go further

Mentions tell you that people are talking about you. Pair this with X link attribution to see exactly which tweets are turning that conversation into visitors and revenue.

Stripe Revenue Attribution

Connect Stripe with Convrs to attribute revenue back to the campaigns, traffic sources, and visitors that generated it.

Once connected, Convrs automatically links successful payments to the original visitor and session, allowing you to see which channels, campaigns, and pages generate revenue.

How It Works

  1. A visitor lands on your website.
  2. Convrs assigns a unique visitor and session ID.
  3. You pass those IDs to Stripe when creating a checkout session or payment intent.
  4. Stripe sends the payment data to Convrs.
  5. Convrs attributes revenue to the original visitor, campaign, and traffic source.

Required Metadata

When creating a payment, include the following metadata:

KeyDescription
convrs_visitor_idUnique visitor identifier assigned by Convrs.
convrs_session_idSession identifier assigned by Convrs.

These values allow Convrs to connect a Stripe payment to the visitor who originally started the journey.

Stripe Checkout

If you're using Stripe Checkout, include the Convrs identifiers in the checkout session metadata.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const session = await stripe.checkout.sessions.create({
  line_items: [
    {
      price: "price_123",
      quantity: 1,
    },
  ],

  mode: "payment",

  success_url: "https://yourapp.com/success",
  cancel_url: "https://yourapp.com/cancel",

  metadata: {
    convrs_visitor_id: visitorId,
    convrs_session_id: sessionId,
  },
});

Payment Intents

For custom checkout flows, pass the identifiers when creating the payment intent.

1
2
3
4
5
6
7
8
9
10
const paymentIntent =
  await stripe.paymentIntents.create({
    amount: 5000,
    currency: "usd",

    metadata: {
      convrs_visitor_id: visitorId,
      convrs_session_id: sessionId,
    },
  });

Getting Visitor Information

If you're using the Convrs SDK, retrieve the tracking identifiers before creating the checkout session.

1
2
3
4
const tracking =
  convrs.getTrackingParams();

console.log(tracking);

Example response:

1
2
3
4
{
  visitorId: "visitor_123",
  sessionId: "session_456"
}
1
2
3
4
5
6
7
8
9
10
11
Visitor arrives
       ↓
Convrs creates visitor + session
       ↓
User starts checkout
       ↓
Pass IDs to Stripe metadata
       ↓
Payment succeeds
       ↓
Revenue appears in Convrs

Verify Revenue Attribution

After a successful payment, open your Convrs dashboard and navigate to:

Revenue Attribution → Payments

You should see:

  • Revenue amount
  • Customer information
  • Original traffic source
  • Campaign attribution
  • Landing page
  • Visitor journey

Revenue attribution works for one-time payments, subscriptions, upgrades, and renewals as long as the Convrs identifiers are included in the Stripe metadata.

API Reference

The Convrs Public API gives your scripts, CLI tools, backends, or AI agents read access to the same analytics and revenue data shown in your Convrs dashboard — computed by the exact same query layer, so the numbers always match what you see in the app.

This is the server-side REST API (/api/v1/...), authenticated with a Bearer token. It's a different thing from the client-side tracking SDK (convrs.track(), convrs.identify(), etc.) used to instrument your website.

Base URL

1
https://convrs.dev/api/v1

Every endpoint on this page and the following pages is relative to this base URL.

API Version

The current (and only) version is v1. Endpoints are versioned in the URL path (/api/v1/...). Breaking changes to response shapes or parameter semantics won't be made to v1 casually — a new version would be introduced instead.

Authentication

Every request requires an Authorization header with a Bearer token:

1
2
curl https://convrs.dev/api/v1/account \
  -H "Authorization: Bearer cvrs_..."

See Authentication for the full token format, how to create a token, and scopes.

Bearer Token Format

Tokens are created from Settings → API Tokens in your Convrs workspace and start with the cvrs_ prefix:

1
cvrs_xxxxxxxxxxxxxxxxxxxxxxxx

Never put a token in a URL or query string — it must be sent in the Authorization header.

API Token Scopes

Each token is granted one or more scopes that gate which endpoints it can call — for example analytics.read, goals.read, and payments.read. See Authentication for the full list and which scope each endpoint requires.

Workspace / Website Scope

A token is created inside one workspace and can only ever read that workspace's data — Convrs currently has no concept of a token spanning multiple websites. Every response is scoped to the token's own website automatically; you never need to (and cannot) select a different one. See Authentication for details.

Rate Limits

Requests are limited to 60 per minute per token. Exceeding the limit returns 429 rate_limit_exceeded. See Rate Limits.

Pagination

List endpoints return a consistent { data, pagination } envelope with page, limit, and hasMore. See Pagination for defaults, maximums, and per-endpoint notes.

Common Query Parameters

Most analytics endpoints share the same date-range, timezone, and website-scoping parameters:

ParameterTypeDescription
websiteIdstringDefaults to the website your token is scoped to. If provided, it must match that website or the request is rejected with 403 forbidden.
intervalstringRelative date range: 24h, 7d, 30d, 90d, 1y, mtd, qtd, ytd. Defaults to 30d. Ignored if startAt/endAt are set.
startAt / endAtstring (ISO 8601)Explicit date range. Takes precedence over interval. Maximum range: 366 days.
timezonestring (IANA)Timezone used to align day/hour/month buckets, e.g. America/New_York. Defaults to UTC.

Most analytics endpoints also accept a shared set of filters (country, device, referrer, UTM parameters, etc.) — see Analytics for the full list, since not every endpoint supports every filter (documented per-endpoint).

Error Handling

Every error response uses the same envelope:

1
2
3
4
5
6
{
  "error": {
    "code": "forbidden",
    "message": "This token does not have the required scope: analytics.read"
  }
}

See Errors for the full list of codes and when each occurs.

OpenAPI Reference

A machine-readable OpenAPI 3.1 specification is generated directly from the same validation schemas that power the API, so it can't drift from the real implementation:

1
https://convrs.dev/api/v1/openapi.json

Use it to import the API into Postman/Insomnia, generate a typed client, or drive AI-agent tool calling.

All API Resources

ResourceDescription
AuthenticationToken format, creating tokens, scopes, website scope
ErrorsError envelope and status codes
PaginationList response format, limits, defaults
Rate LimitsRequest limits and the 429 response
AccountVerify a token and see authenticated context
WebsitesThe website your token is scoped to
AnalyticsOverview, timeseries, breakdowns, realtime, bot traffic
GoalsGoal catalogue, completions over time, goal properties
CustomersCustomer list, detail, and event activity
PaymentsRedacted payment records
FunnelsFunnel definitions and step-by-step conversion analytics
RevenueRevenue overview, timeseries, and attribution breakdowns
ExamplesEnd-to-end cURL and JavaScript examples

Metadata Endpoint

1
GET /api/v1/meta

Returns the metrics, dimensions, and this website's real goal catalogue that the rest of the API actually supports — useful for building a UI or an AI-agent tool schema against the API without hardcoding assumptions.

Authentication: Bearer token · Scope: analytics.read

1
2
curl https://convrs.dev/api/v1/meta \
  -H "Authorization: Bearer cvrs_..."

Response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "data": {
    "intervals": ["24h", "7d", "30d", "90d", "1y", "mtd", "qtd", "ytd"],
    "analytics": {
      "metrics": ["visitors", "new_visitors", "returning_visitors", "bounce_rate", "avg_session_duration", "conversion_rate", "revenue_per_visitor", "revenue", "mrr", "new_revenue", "refund_amount"]
    },
    "breakdowns": {
      "pages": { "metrics": ["visitors", "conversions", "revenue", "revenue_per_visitor", "conversion_rate"], "dimensions": ["page", "hostname", "entrypage", "exitlink"] },
      "sources": { "metrics": ["visitors", "conversions", "revenue", "revenue_per_visitor", "conversion_rate"], "dimensions": ["referrer", "referrer_url", "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "campaign"] },
      "geo": { "metrics": ["visitors", "conversions", "revenue", "revenue_per_visitor", "conversion_rate"], "dimensions": ["country", "region", "city", "continent"] },
      "devices": { "metrics": ["visitors", "conversions", "revenue", "revenue_per_visitor", "conversion_rate"], "dimensions": ["device", "browser", "os"] }
    },
    "revenue": {
      "metrics": ["revenue", "mrr", "new_revenue", "refund_amount", "revenue_per_visitor", "conversion_rate"]
    },
    "bots": {
      "breakdowns": ["count", "timeseries", "providers", "top_pages", "categories"],
      "categories": ["answer_agent", "index_crawler", "training_crawler", "other"]
    },
    "goals": ["signup", "purchase"]
  }
}

goals is your website's actual, current goal catalogue (event names you've tracked), not a static list.

API Authentication

Bearer Tokens

Every request to /api/v1/... must include an Authorization header:

1
2
curl https://convrs.dev/api/v1/account \
  -H "Authorization: Bearer cvrs_..."
  • Tokens are Bearer tokens — never pass them as a query parameter or in the request body.
  • New tokens are prefixed cvrs_. Tokens created before the public API existed use a legacy bc_ prefix and continue to work.
  • A missing or malformed Authorization header, or an invalid token, returns 401 unauthorized.

Creating a Token

API tokens are created, named, and revoked from Settings → API Tokens inside your Convrs workspace. There is no way to create, list, or revoke a token through the API itself — token management is intentionally dashboard-only, so a leaked token can never be used to mint or revoke other tokens.

The full token value is shown exactly once, at creation time. Convrs stores only a one-way hash of it afterward — if you lose it, revoke it and create a new one.

Scopes

A token is granted one or more scopes when it's created. A request fails with 403 forbidden if the token doesn't have the scope an endpoint requires.

ScopeGrants access to
analytics.readAccount, websites, core analytics (overview/timeseries/pages/sources/geo/devices/realtime/bots), customers, funnels, revenue
goals.readGoal catalogue, goal completions over time, goal properties
payments.readThe redacted payments list
apis.readShorthand for every current and future read scope
apis.allFull access to all API scopes

Each endpoint's documentation states the exact scope it requires. apis.all satisfies any scope requirement; apis.read satisfies any scope ending in .read.

Website Scope

A token is created inside — and permanently bound to — exactly one workspace (Convrs's tenant/website boundary). There is currently no concept of an account-level token that can read multiple websites.

Some endpoints accept an optional websiteId query or path parameter for symmetry with other APIs. If you pass one, it must match the website your token is scoped to:

  • Omitted → the request is automatically scoped to your token's website.
  • Matches your token's website → works normally.
  • Doesn't match (a different, or made-up, website ID) → 403 forbidden. The mismatch is rejected outright; it is never silently ignored or used to widen access to another website's data.

Revoked and Expired Tokens

  • A revoked (deleted) token is rejected the same way an invalid token is: 401 unauthorized. There's no way to distinguish "token never existed" from "token was revoked" in the response — this is intentional, to avoid leaking information about token history.
  • An expired token (if an expiry date was set when it was created) is rejected with 401 unauthorized.

Insufficient Scope

If your token is valid but lacks the required scope for an endpoint, you'll get 403 forbidden with a message naming the missing scope:

1
2
3
4
5
6
{
  "error": {
    "code": "forbidden",
    "message": "This token does not have the required scope: payments.read"
  }
}

Security Guidance

  • Never expose a token in client-side code, a public repository, or a URL. Treat it as a credential, not a public identifier.
  • Never commit a token to source control. Store it in an environment variable or secrets manager.
  • Revoke a token immediately from Settings → API Tokens if it may have leaked — revocation takes effect immediately.
  • Grant the narrowest scope that does the job. A token that only needs to read analytics doesn't need payments.read.

API Errors

Error Envelope

Every error response — regardless of endpoint or status code — uses the same shape:

1
2
3
4
5
6
{
  "error": {
    "code": "not_found",
    "message": "Customer not found."
  }
}

code is a stable, machine-readable string. message is a human-readable explanation and may change wording over time — don't match on it, match on code.

Status Codes

These are the error codes the public API actually returns:

HTTP StatusCodeWhen it happens
400bad_requestInvalid query parameters — an unparseable date, an unknown metric/dimension, a limit outside its allowed range, an invalid interval, or a date range wider than 366 days.
401unauthorizedMissing/malformed Authorization header, an invalid token, an expired token, or a revoked token.
403forbiddenThe token doesn't have the required scope, or the request's websiteId doesn't match the token's own website.
404not_foundThe requested resource (a customer or a funnel) doesn't exist in your website.
429rate_limit_exceededYou've exceeded 60 requests/minute for this token. See Rate Limits.
500internal_server_errorAn unexpected server error. If this persists, it's worth reporting.

Notes

  • A 401 doesn't distinguish between "token never existed" and "token was revoked" — both return the same generic message, to avoid leaking information about token history.
  • A 404 for a customer or funnel ID from a different website looks identical to a 404 for an ID that never existed at all — the API never confirms or denies that a resource exists outside your own website.
  • 400 validation errors include a specific message naming the offending parameter, e.g. `startAt` must be before `endAt`. or Metric \mrr` is not available on this endpoint.`

Pagination

Response Format

List/breakdown endpoints return a data array alongside a pagination object:

1
2
3
4
5
6
7
8
{
  "data": [ ],
  "pagination": {
    "page": 1,
    "limit": 100,
    "hasMore": true
  }
}

Endpoints that return a single object (like Analytics overview or Account) just return { "data": { ... } } with no pagination key.

Parameters

ParameterTypeDefaultMaxDescription
pageinteger1—1-indexed page number.
limitinteger100500Rows per page. Requesting a limit above 500 (or below 1) returns 400 bad_request.

How hasMore Is Computed

To avoid running an expensive COUNT query on every request, most endpoints fetch limit + 1 rows and set hasMore: true if that extra row exists — hasMore tells you whether there's a next page, not the total row count. There is no totalCount/totalPages field.

Endpoints With Pagination

page/limit pagination applies to: Analytics breakdowns (pages, sources, geo, devices), bot-traffic breakdowns, Goals list, Goal properties, Customers list, Payments, and Funnels list.

One Exception: GET /api/v1/customers

GET /api/v1/customers only accepts a limit parameter (default 100, max 500) — there's no page parameter. Its hasMore is a heuristic (returned rows === limit), not derived from a limit + 1 fetch. If you need to page through more than 500 customers, this endpoint doesn't currently support that — see Customers for details.

Non-Paginated Endpoints

GET /api/v1/analytics/timeseries, GET /api/v1/analytics/revenue/timeseries, and GET /api/v1/analytics/goals return a plain array of time-bucketed points with no pagination — the number of points is bounded by your date range and interval, not by limit.

Rate Limits

Limit

Each API token is limited to 60 requests per minute, counted in a fixed 60-second window per token — one customer's script or agent can't starve another's.

The 429 Response

Exceeding the limit returns:

1
2
3
4
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
1
2
3
4
5
6
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Please slow down and retry later."
  }
}

Retry-After is in seconds until the current window resets. These headers are only present on a 429 response — successful responses don't currently include rate-limit headers, so don't rely on X-RateLimit-Remaining to preemptively back off; handle 429 and retry after the given delay instead.

Practical Guidance

  • Prefer fewer, broader requests over many narrow ones — e.g. request several metrics in one call to /api/v1/analytics instead of one request per metric.
  • If you're polling /api/v1/analytics/realtime, a 5–10 second interval is more than enough — the underlying data itself only updates on a similar cadence.
  • Cache responses where the data doesn't need to be second-fresh (most analytics and revenue breakdowns).

Account

Get Account

1
GET /api/v1/account

Verifies your token works and returns basic authenticated context: who created the token, which website it's scoped to, the token's own name and scopes, and the website's plan status. Useful as a first call to confirm a token is valid before wiring up anything else.

Authentication: Bearer token · Scope: none — any valid, non-expired token can call this regardless of which scopes it was granted.

Parameters

None.

cURL

1
2
curl https://convrs.dev/api/v1/account \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch("https://convrs.dev/api/v1/account", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();
console.log(data.website.name, data.plan.status);

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "data": {
    "user": {
      "id": "usr_...",
      "name": "Jane Doe",
      "email": "jane@example.com"
    },
    "website": {
      "id": "ws_...",
      "name": "Acme Inc",
      "domain": "acme.com"
    },
    "token": {
      "name": "My integration",
      "scopes": ["analytics.read", "goals.read"]
    },
    "plan": {
      "plan": "growth",
      "planFamily": "growth",
      "status": "active"
    }
  }
}
FieldDescription
user.id, user.name, user.emailThe user who created this token. user is null if that user record can't be resolved.
website.id, website.name, website.domainThe website this token is scoped to.
token.nameThe name given to this token when it was created.
token.scopesThe scopes granted to this token.
plan.plan, plan.planFamilyThe workspace's current plan.
plan.status"active" or "inactive", based on the workspace's billing entitlement.

Errors

401 unauthorized (missing/invalid/expired/revoked token). No scope is required, so 403 never occurs on this endpoint.

Websites

Convrs doesn't have a separate "website" object internally — a workspace is a website (one tracked domain per workspace). These endpoints reshape your workspace into that public "website" resource; every field below is a real workspace field, not invented data.

List Websites

1
GET /api/v1/websites

Lists the websites this token can access. Because a token is bound to exactly one workspace, this always returns a single-item array today — there's no multi-website token in Convrs (see Authentication).

Authentication: Bearer token · Scope: websites.read

cURL

1
2
curl https://convrs.dev/api/v1/websites \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
const res = await fetch("https://convrs.dev/api/v1/websites", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "data": [
    {
      "id": "ws_abc123",
      "name": "Acme Inc",
      "domain": "acme.com",
      "timezone": "America/New_York",
      "currency": "USD",
      "createdAt": "2026-01-15T00:00:00.000Z",
      "plan": "growth",
      "planFamily": "growth",
      "cookielessMode": false,
      "status": "active"
    }
  ]
}
FieldDescription
idThe website ID (ws_-prefixed). Use this as websiteId elsewhere in the API.
nameWorkspace/website name.
domainThe tracked domain.
timezoneIANA timezone used for this website's default analytics bucketing.
currencyISO 4217 display currency for revenue figures.
createdAtISO 8601 creation timestamp.
plan, planFamilyThe current billing plan.
cookielessModeWhether cookieless tracking mode is enabled.
status"active" or "inactive", based on billing entitlement.

Errors

401 unauthorized, 403 forbidden (missing websites.read scope).

Get Website

1
GET /api/v1/websites/:websiteId

Gets one website by ID. :websiteId must match the website your token is scoped to — a different or made-up ID returns 403 forbidden, never another website's data.

Authentication: Bearer token · Scope: websites.read

Path Parameters

ParameterDescription
websiteIdThe website ID, e.g. ws_abc123. Must match your token's own website.

cURL

1
2
curl https://convrs.dev/api/v1/websites/ws_abc123 \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
const res = await fetch("https://convrs.dev/api/v1/websites/ws_abc123", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();

Response

Same shape as one item from the list above, but as a single object (data is not an array).

Errors

401 unauthorized, 403 forbidden (missing scope, or websiteId doesn't match your token's website).

Analytics

Core visitor analytics: an overview snapshot, a timeseries, four dimension breakdowns, realtime, and Convrs's AI-crawler/bot-traffic detector. All of these are backed by the exact same query layer the Convrs dashboard uses — the numbers you get here always match the dashboard for the same website and date range.

Authentication: Bearer token · Scope: analytics.read for every endpoint on this page.

Metrics

MetricDescription
visitorsDistinct visitors. Convrs measures unique visitors, not raw pageview/session totals — there is no pageviews or sessions metric.
new_visitorsDistinct visitors seen for the first time in this bucket.
returning_visitorsDistinct visitors who had already been seen before this bucket.
bounce_ratePercentage of single-pageview sessions.
avg_session_durationAverage session duration, in seconds.
conversion_ratePercentage of sessions whose visitor completed a purchase.
revenue_per_visitorRevenue divided by distinct visitors.
revenueTotal revenue (in the website's display currency).
mrrMonthly Recurring Revenue, computed from active subscriptions — only available on the overview and timeseries endpoints below, not on the dimension breakdowns (see note under Revenue).
new_revenueRevenue from non-refunded charges.
refund_amountTotal refunded amount.
conversionsGoal completions — only available on the dimension breakdowns (pages/sources/geo/devices), not on the overview/timeseries.

Each endpoint below states exactly which of these it supports. Request a subset with ?metrics=visitors,revenue; omit metrics to get all of that endpoint's supported metrics (except mrr, which is opt-in only — see Revenue).

Filters

These filters are accepted by the overview, timeseries, and all four dimension-breakdown endpoints (not by realtime or bots — see those sections). Each accepts a single value, a comma-separated list, or an exclusion prefixed with -:

FilterExample
countrycountry=US or country=US,CA or country=-US
citycity=New York
regionregion=NY
continentcontinent=NA
devicedevice=Desktop
browserbrowser=Chrome
osos=Windows
refererreferer=google.com
refererUrlrefererUrl=https://google.com/search
hostnamehostname=blog.example.com
pagepage=/pricing
entrypageentrypage=/
exitlinkexitlink=/checkout
utm_source, utm_medium, utm_campaign, utm_term, utm_contentutm_source=newsletter
goalgoal=signup — restricts to sessions where this goal fired
saleTypenew or recurring

Plus the shared websiteId/interval/startAt/endAt/timezone parameters from the overview.

Overview

1
GET /api/v1/analytics

A single composite snapshot for one date range.

Query Parameters

ParameterTypeDefaultDescription
websiteIdstringyour token's websiteMust match your token's website.
intervalstring30d24h, 7d, 30d, 90d, 1y, mtd, qtd, ytd. Ignored if startAt is set.
startAt / endAtISO 8601—Explicit range, max 366 days.
timezoneIANA stringUTC
metricscomma listall except mrrAny of: visitors, new_visitors, returning_visitors, bounce_rate, avg_session_duration, conversion_rate, revenue_per_visitor, revenue, mrr, new_revenue, refund_amount.
(filters)——See Filters above.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics?interval=30d&metrics=visitors,revenue" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
6
const res = await fetch(
  "https://convrs.dev/api/v1/analytics?interval=30d&metrics=visitors,revenue",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
console.log(data.visitors, data.revenue);

Response

1
2
3
4
5
6
{
  "data": {
    "visitors": 4213,
    "revenue": 128500
  }
}

data contains exactly the metrics you requested (or all supported metrics, if metrics was omitted), keyed by metric name.

Errors

400 bad_request (invalid metric, invalid date range), 401 unauthorized, 403 forbidden (missing scope, or websiteId mismatch).

Timeseries

1
GET /api/v1/analytics/timeseries

The same composite metrics as the overview, grouped into a time series. Bucket granularity (hour/day/month) is chosen automatically from the date range's length, matching the dashboard.

Query Parameters

Same as Overview.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/timeseries?metrics=visitors,revenue&interval=day&startAt=2026-09-01&endAt=2026-09-15" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/timeseries?metrics=visitors,revenue&startAt=2026-09-01&endAt=2026-09-15",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
{
  "data": [
    { "date": "2026-09-01T00:00:00.000Z", "visitors": 120, "revenue": 4200 },
    { "date": "2026-09-02T00:00:00.000Z", "visitors": 98, "revenue": 3100 }
  ]
}

data is a plain array (no pagination) — one object per bucket, with date (ISO 8601 timestamp for the start of the bucket) plus each requested metric.

Errors

Same as Overview.

Pages

1
GET /api/v1/analytics/pages

Breakdown by page, hostname, entry page, or exit link.

Query Parameters

ParameterTypeDefaultDescription
dimensionstringpagepage, hostname, entrypage, or exitlink.
metricscomma listvisitors, conversions, revenue, revenue_per_visitor, conversion_rateSubset of those five.
page, limitinteger1, 100Pagination — see Pagination. limit max 500.
(date range, filters)——Same as Overview.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/pages?dimension=page&limit=50" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/pages?dimension=page&limit=50",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data, pagination } = await res.json();

Response

1
2
3
4
5
6
{
  "data": [
    { "page": "/pricing", "visitors": 812, "conversions": 40, "revenue": 9200, "revenue_per_visitor": 11.33, "conversion_rate": 4.9 }
  ],
  "pagination": { "page": 1, "limit": 50, "hasMore": false }
}

The dimension field's key matches whichever dimension you requested (page, hostname, entrypage, or exitlink).

Errors

400 bad_request (invalid dimension, invalid metrics, invalid limit), 401 unauthorized, 403 forbidden.

Sources

1
GET /api/v1/analytics/sources

Breakdown by referrer or UTM parameters.

Query Parameters

ParameterTypeDefaultDescription
dimensionstringreferrerreferrer, referrer_url, utm_source, utm_medium, utm_campaign, utm_term, utm_content, or campaign (the full attribution query string, e.g. ?utm_source=x&utm_medium=y).
metrics, page, limit——Same as Pages.
(date range, filters)——Same as Overview.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/sources?dimension=utm_source" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/sources?dimension=utm_source",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
{
  "data": [
    { "utmSource": "newsletter", "visitors": 340, "conversions": 12, "revenue": 2100, "revenue_per_visitor": 6.18, "conversion_rate": 3.5 }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}

The dimension key varies by dimension: referrer, referrerUrl, utmSource, utmMedium, utmCampaign, utmTerm, utmContent, or campaign.

Errors

Same as Pages.

Geo

1
GET /api/v1/analytics/geo

Breakdown by country, region, city, or continent.

Query Parameters

ParameterTypeDefaultDescription
dimensionstringcountrycountry, region, city, or continent.
metrics, page, limit——Same as Pages.
(date range, filters)——Same as Overview.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/geo?dimension=country" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/geo?dimension=country",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
{
  "data": [
    { "country": "US", "visitors": 1500, "conversions": 60, "revenue": 15000, "revenue_per_visitor": 10, "conversion_rate": 4 }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}

dimension=region rows also include country; dimension=city rows include both country and region.

Errors

Same as Pages.

Devices

1
GET /api/v1/analytics/devices

Breakdown by device type, browser, or OS.

Query Parameters

ParameterTypeDefaultDescription
dimensionstringdevicedevice, browser, or os.
metrics, page, limit——Same as Pages.
(date range, filters)——Same as Overview.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/devices?dimension=browser" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/devices?dimension=browser",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
{
  "data": [
    { "browser": "Chrome", "visitors": 2900, "conversions": 110, "revenue": 21000, "revenue_per_visitor": 7.24, "conversion_rate": 3.8 }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}

Errors

Same as Pages.

Realtime

1
GET /api/v1/analytics/realtime

Live visitor count, current pages, referrers, countries, and geo points for a realtime map. This endpoint reflects "right now" — it has no date-range parameters.

Query Parameters

None.

cURL

1
2
curl https://convrs.dev/api/v1/analytics/realtime \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
const res = await fetch("https://convrs.dev/api/v1/analytics/realtime", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();

Response

1
2
3
4
5
6
7
8
9
{
  "data": {
    "count": 8,
    "pages": [{ "page": "/pricing", "count": 3 }],
    "points": [{ "id": "abc-40.7128|-74.0060", "latitude": 40.7128, "longitude": -74.006, "value": 3 }],
    "referrers": [{ "source": "google.com", "count": 5 }],
    "countries": [{ "country": "United States", "code": "US", "count": 6 }]
  }
}
FieldDescription
countVisitors active in the last 30 seconds.
pagesCurrent pages being viewed: { page, count }.
pointsGeo points for a live map: { id, latitude, longitude, value }.
referrers{ source, count }.
countries{ country, code, count }.

Errors

401 unauthorized, 403 forbidden.

Bots

1
GET /api/v1/analytics/bots

Convrs's AI-crawler / bot-traffic detector: which AI bots and crawlers are hitting your site, broken down by vendor, category, or page.

Query Parameters

ParameterTypeDefaultDescription
breakdownstringcountcount (overview), timeseries, providers, top_pages, or categories.
domainstring—Filter to one tracked hostname.
categorystring—answer_agent, index_crawler, training_crawler, or other.
granularitystringautohour, day, or week — only used by breakdown=timeseries.
page, limitinteger1, 100Pagination — only applies to breakdown=providers/top_pages/categories.
(date range)——websiteId, interval, startAt/endAt, timezone — same as Overview. Bots does not accept the visitor filters listed under Filters.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/bots?breakdown=categories" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/bots?breakdown=categories",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

breakdown=count (default) — a single overview object:

1
2
3
4
5
6
7
8
9
10
{
  "data": {
    "total_requests": 4200,
    "ai_answers": 1800,
    "indexing": 1500,
    "training": 700,
    "other": 200,
    "unique_providers": 9
  }
}

breakdown=timeseries — a plain array, one object per bucket with one key per bot vendor seen:

1
2
3
4
5
{
  "data": [
    { "start": "2026-09-01T00:00:00.000Z", "gptbot": 40, "claudebot": 12 }
  ]
}

breakdown=providers / top_pages / categories — a paginated array, e.g. for providers:

1
2
3
4
{
  "data": [{ "vendor": "GPTBot", "category": "training_crawler", "requests": 900 }],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}

Errors

400 bad_request (invalid breakdown/category), 401 unauthorized, 403 forbidden.

Goals

The goal catalogue, completions over time, and custom event-property breakdowns for goal completions.

Authentication: Bearer token · Scope: goals.read for every endpoint on this page.

List Goals

1
GET /api/v1/goals

Your website's goal catalogue — the distinct goal-event names Convrs has recorded, with when each was first and most recently seen.

Query Parameters

ParameterTypeDefaultDescription
websiteIdstringyour token's websiteMust match your token's website.
page, limitinteger1, 100Pagination, max limit 500. See Pagination.

cURL

1
2
curl https://convrs.dev/api/v1/goals \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
const res = await fetch("https://convrs.dev/api/v1/goals", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();

Response

1
2
3
4
5
6
7
8
9
10
11
12
{
  "data": [
    {
      "eventName": "signup",
      "eventType": "goals",
      "trigger": "goal",
      "firstSeenAt": "2026-01-10T12:00:00.000Z",
      "lastSeenAt": "2026-09-14T08:00:00.000Z"
    }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}
FieldDescription
eventNameThe goal's name, as fired by the tracker (convrs.track("signup")) or via data-goal="signup".
eventTypeAlways "goals".
triggerHow the goal fires — currently always "goal".
firstSeenAt, lastSeenAtISO 8601 timestamps for when this goal was first/most recently recorded.

Errors

401 unauthorized, 403 forbidden (missing goals.read, or websiteId mismatch).

Goal Completions Over Time

1
GET /api/v1/analytics/goals

Goal completions, bucketed over time.

Query Parameters

ParameterTypeDefaultDescription
websiteId, interval, startAt/endAt, timezone——Same as Analytics overview.
goalscomma listall goalsGoal names to filter to (from List Goals above).

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/goals?goals=signup" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/goals?goals=signup",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
{
  "data": [
    { "date": "2026-09-01T00:00:00.000Z", "goal": "signup", "count": 14 }
  ]
}

A plain array (no pagination) — one row per goal per time bucket.

Errors

400 bad_request (invalid date range/interval), 401 unauthorized, 403 forbidden.

Goal Properties

1
GET /api/v1/analytics/goals/properties

Custom event-property values recorded alongside a goal completion (e.g. convrs.track("signup", { plan: "pro" })), grouped with counts.

This endpoint's filter support is narrower than other analytics endpoints. The underlying query only conditions on the website, the goal name, and the date range — it does not support the hostname/page/device/country/etc. filters documented under Analytics → Filters, because that filtering isn't implemented in the underlying data pipeline for this specific query. Only the parameters listed below actually take effect.

Query Parameters

ParameterTypeDefaultDescription
websiteIdstringyour token's websiteMust match your token's website.
goalstringall goalsThe goal name to analyze (from List Goals). If omitted, properties are aggregated across all goal events, which usually mixes unrelated property keys together — passing goal is recommended.
interval, startAt/endAt, timezone——Same as Analytics overview.
page, limitinteger1, 100Pagination, max limit 500.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/goals/properties?goal=signup" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/goals/properties?goal=signup",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
7
{
  "data": [
    { "propertyKey": "plan", "propertyValue": "pro", "count": 42 },
    { "propertyKey": "plan", "propertyValue": "free", "count": 118 }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}
FieldDescription
propertyKeyThe custom property's key, e.g. "plan".
propertyValueThe value recorded for that key, e.g. "pro".
countDistinct visitors who completed the goal with that property value.

Errors

400 bad_request (invalid date range), 401 unauthorized, 403 forbidden.

Customers

Convrs's identified + anonymous visitor model. There is no separate "Visitor" resource — an anonymous, never-converted visitor is a Customer row whose externalId is their tracker-assigned visitor_id; an identified customer's externalId is whatever ID/email you passed to convrs.identify().

Authentication: Bearer token · Scope: analytics.read for every endpoint on this page.

List Customers

1
GET /api/v1/customers

Query Parameters

ParameterTypeDefaultDescription
websiteIdstringyour token's websiteMust match your token's website.
limitinteger100Max 500.

No page parameter. This endpoint returns the first limit customers only, ordered newest-first. hasMore is a heuristic (true when the returned count equals limit), not an exact page boundary — see Pagination for why. Paging further than 500 customers isn't currently supported.

cURL

1
2
curl "https://convrs.dev/api/v1/customers?limit=50" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/customers?limit=50",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "data": [
    {
      "id": "cust_abc123",
      "name": "Jane Doe",
      "email": "jane@example.com",
      "avatar": null,
      "externalId": "user_42",
      "stripeCustomerId": "cus_...",
      "country": "US",
      "sales": 2,
      "saleAmount": 9800,
      "firstSaleAt": "2026-03-01T00:00:00.000Z",
      "subscriptionCanceledAt": null,
      "createdAt": "2026-02-20T00:00:00.000Z",
      "updatedAt": "2026-09-01T00:00:00.000Z",
      "device": "Desktop",
      "browser": "Chrome"
    }
  ],
  "pagination": { "page": 1, "limit": 50, "hasMore": false }
}
FieldDescription
idCustomer ID — use this as :customerId in the endpoints below.
name, email, avatarIdentity fields, null for anonymous (never-identified) visitors.
externalIdYour own user ID (from identify()), or the visitor ID for anonymous visitors.
stripeCustomerIdSet if this customer has a linked Stripe customer.
country, device, browserFrom the visitor's session.
sales, saleAmountNumber of payments and total amount (smallest currency unit) attributed to this customer.
firstSaleAt, subscriptionCanceledAtISO 8601 timestamps, null if not applicable.
createdAt, updatedAtISO 8601 timestamps.

Errors

400 bad_request (invalid limit), 401 unauthorized, 403 forbidden.

Get Customer

1
GET /api/v1/customers/:customerId

A customer ID belonging to a different website always returns 404 not_found — the lookup is scoped by your token's website internally, so it never leaks another website's data.

Path Parameters

ParameterDescription
customerIdThe customer ID from the list endpoint above.

cURL

1
2
curl https://convrs.dev/api/v1/customers/cust_abc123 \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/customers/cust_abc123",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

Same shape as one item from the list above, as a single object.

Errors

401 unauthorized, 403 forbidden, 404 not_found (customer doesn't exist in your website).

Customer Activity

1
GET /api/v1/customers/:customerId/activity

The customer's event history — pageviews, goal completions, and revenue events — grouped by day. Works for anonymous customers too, since their externalId is their tracker visitor_id.

Path Parameters

ParameterDescription
customerIdThe customer ID.

cURL

1
2
curl https://convrs.dev/api/v1/customers/cust_abc123/activity \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/customers/cust_abc123/activity",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  "data": [
    {
      "date": "September 1, 2026",
      "items": [
        {
          "event_id": "evt_...",
          "timestamp": "2026-09-01T14:32:00.000Z",
          "event_type": "goals",
          "event_name": "signup",
          "page": "/signup",
          "url": "https://acme.com/signup",
          "referer": "google.com",
          "referer_url": "https://google.com/search",
          "browser": "Chrome",
          "device": "Desktop",
          "country": "US",
          "utm_source": "newsletter",
          "utm_medium": "email",
          "utm_campaign": "launch",
          "event_properties": "{\"plan\":\"pro\"}",
          "revenue": 0,
          "currency": null
        }
      ]
    }
  ]
}

data is a plain array, oldest day first. Each items entry is a raw event with event_type of pageview, goals, identify, or revenue.

Errors

401 unauthorized, 403 forbidden, 404 not_found (customer doesn't exist in your website).

Payments

List Payments

1
GET /api/v1/payments

A redacted list of individual payment records for your website.

This endpoint deliberately excludes provider-side identifiers and customer PII: externalSessionId, externalPaymentId, externalEventId (Stripe/Dodo/Polar/Paddle/LemonSqueezy session/payment/event IDs), and customerEmail are never returned, regardless of which fields you request — there's no parameter to opt into them.

Authentication: Bearer token · Scope: payments.read

Query Parameters

ParameterTypeDefaultDescription
websiteIdstringyour token's websiteMust match your token's website.
page, limitinteger1, 100Pagination, max limit 500. See Pagination.

cURL

1
2
curl "https://convrs.dev/api/v1/payments?limit=50" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/payments?limit=50",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "data": [
    {
      "id": "pay_abc123",
      "amount": 4900,
      "currency": "USD",
      "provider": "stripe",
      "isRecurring": true,
      "billingInterval": "month",
      "createdAt": "2026-09-01T00:00:00.000Z",
      "attributionStatus": "attributed",
      "customerId": "cust_abc123"
    }
  ],
  "pagination": { "page": 1, "limit": 50, "hasMore": false }
}
FieldDescription
idPayment ID.
amountIn the smallest currency unit (e.g. cents for USD).
currencyISO 4217 code, as recorded on the payment — not converted to the website's display currency (unlike the Revenue endpoints, which report in the website's display currency).
providerstripe, dodo, polar, paddle, or lemonsqueezy.
isRecurringWhether this payment settles a subscription.
billingIntervalday, week, month, year, or null for a one-off payment.
createdAtISO 8601 timestamp.
attributionStatuspending, attributed, or unattributed — see Revenue for what attribution means here.
customerIdThe customer this payment belongs to — use with Customers.

Errors

400 bad_request (invalid limit), 401 unauthorized, 403 forbidden (missing payments.read, or websiteId mismatch).

Funnels

Funnel definitions (saved in your dashboard) and their step-by-step conversion analytics.

Authentication: Bearer token · Scope: analytics.read for every endpoint on this page.

Funnels are currently defined and managed from the Convrs dashboard only — there is no create/update/delete via the public API.

List Funnels

1
GET /api/v1/funnels

Query Parameters

ParameterTypeDefaultDescription
websiteIdstringyour token's websiteMust match your token's website.
page, limitinteger1, 100Pagination, max limit 500.

cURL

1
2
curl https://convrs.dev/api/v1/funnels \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
const res = await fetch("https://convrs.dev/api/v1/funnels", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "data": [
    {
      "id": "cly8k2p3q0000abcxyz12345",
      "name": "Signup funnel",
      "createdAt": "2026-05-01T00:00:00.000Z",
      "updatedAt": "2026-05-01T00:00:00.000Z",
      "steps": [
        { "id": "step_1", "name": "Landing", "value": "/pricing", "type": "page_view", "order": 0 },
        { "id": "step_2", "name": "Signup", "value": "signup", "type": "goal", "order": 1 }
      ]
    }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}

Funnel IDs have no special prefix — they're plain database IDs.

FieldDescription
id, name, createdAt, updatedAtFunnel metadata.
stepsOrdered array. type is "goal" (matched by goal-event name) or "page_view" (matched by page path) — but see the limitation under Funnel Analytics below: page_view steps aren't currently evaluated by the analytics endpoint.

Errors

401 unauthorized, 403 forbidden.

Get Funnel

1
GET /api/v1/funnels/:funnelId

A funnel ID belonging to a different website returns 404 not_found, the same as a nonexistent one.

Path Parameters

ParameterDescription
funnelIdThe funnel ID from the list endpoint.

cURL

1
2
curl https://convrs.dev/api/v1/funnels/cly8k2p3q0000abcxyz12345 \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/funnels/cly8k2p3q0000abcxyz12345",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

Same shape as one item from the list above, as a single object.

Errors

401 unauthorized, 403 forbidden, 404 not_found.

Funnel Analytics

1
GET /api/v1/analytics/funnels/:funnelId

Step-by-step conversion counts for a saved funnel, plus each step's top referrers and countries.

Important limitations

  • page_view steps are not currently supported. The underlying analytics query only matches a step against goal-event names — a step of type page_view (matched by page path) will always show 0 users for that step. This is a limitation of the current implementation, not something specific to the API; the dashboard's own funnel report has the same gap. Build funnels out of goal-type steps for accurate results today.
  • No date-range parameters. The underlying query always computes all-time data — there is no startAt/endAt/interval parameter for this endpoint. Parameters that would silently be ignored aren't offered at all.
  • No stepValue or revenue-per-step field. The response only includes what the underlying data can actually answer — visitor counts and traffic-source breakdowns per step — not a fabricated revenue-per-step approximation.

Path Parameters

ParameterDescription
funnelIdThe funnel ID.

cURL

1
2
curl https://convrs.dev/api/v1/analytics/funnels/cly8k2p3q0000abcxyz12345 \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/funnels/cly8k2p3q0000abcxyz12345",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "data": [
    {
      "step": "signup",
      "users": 420,
      "topSources": [{ "referer": "google.com", "visitors": 180, "pct": 43 }],
      "topCountries": [{ "country": "US", "visitors": 250, "pct": 60 }]
    },
    {
      "step": "purchase",
      "users": 65,
      "topSources": [{ "referer": "google.com", "visitors": 30, "pct": 46 }],
      "topCountries": [{ "country": "US", "visitors": 40, "pct": 62 }]
    }
  ]
}
FieldDescription
stepThe step's value (goal name).
usersDistinct visitors who completed this step and every step before it, in order.
topSourcesTop 3 referrers reaching this step: { referer, visitors, pct }.
topCountriesTop 3 countries reaching this step: { country, visitors, pct }.

data is a plain array, in step order, all-time.

Errors

401 unauthorized, 403 forbidden, 404 not_found (funnel doesn't exist in your website).

Revenue

Convrs is revenue-first analytics: these endpoints report actual attributed revenue from your connected payment provider (Stripe, Dodo, Polar, Paddle, or LemonSqueezy), not estimated or inferred figures.

Authentication: Bearer token · Scope: analytics.read for every endpoint on this page.

Attribution Model — Read This First

Revenue events are recorded from a server-side payment webhook, not a live browser session — when someone pays, there's no "current page" for the payment itself. Instead, at payment time Convrs looks up that visitor's most recent pageview session and attaches its context (page, referrer, UTM parameters, country, device) to the revenue event.

This is genuine last-touch attribution: the visitor's last touchpoint before converting. It is not first-touch attribution and not multi-touch/weighted attribution — Convrs does not currently split credit for a sale across multiple earlier sessions or channels.

Concretely:

  • If a payment can be matched to a prior visitor session, its page/referer/utm_* fields reflect that visitor's most recent session before paying.
  • If a payment can't be matched to any visitor session (no visitor ID in the payment metadata, or no matching session found), it's recorded with zero revenue and is excluded from every breakdown below (the underlying query filters to revenue > 0). This is why revenue totals reported by /analytics/revenue, and the sum across any of the by-X breakdowns for the same period, always agree — unattributed payments never silently show up in one place and not another.
  • attributionStatus on a payment record (attributed/unattributed/pending) tells you whether that specific payment was matched.

MRR

mrr (Monthly Recurring Revenue) is a real, computed metric — based on currently-active subscriptions, not a projection or estimate — but it is only available on the overview and timeseries endpoints. It is not available on the by-source/by-campaign/by-page breakdowns below, because the underlying query only computes the MRR variant of "revenue" for those two shapes — a breakdown request for metrics=mrr returns 400 bad_request rather than silently returning plain revenue mislabeled as MRR.

Convrs does not currently compute ARR, customer LTV, retention, or cohort metrics through this API.

Currency

Every revenue figure is reported in the website's display currency (currency from GET /api/v1/websites), converted from whatever currency each underlying payment was actually made in. This matches exactly what the dashboard shows. The one exception is GET /api/v1/payments, which reports each payment's own original currency, unconverted.

Revenue Overview

1
GET /api/v1/analytics/revenue

A revenue-focused snapshot for one date range, with an explicit currency field.

Query Parameters

ParameterTypeDefaultDescription
websiteId, interval, startAt/endAt, timezone——Same as Analytics overview.
metricscomma listall except mrrrevenue, mrr, new_revenue, refund_amount, revenue_per_visitor, conversion_rate. Requesting both revenue and mrr together returns 400 bad_request.
(filters)——Same filter set as Analytics.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/revenue?interval=30d" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
6
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/revenue?interval=30d",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
console.log(data.currency, data.revenue);

Response

1
2
3
4
5
6
7
8
9
10
{
  "data": {
    "currency": "USD",
    "revenue": 128500,
    "new_revenue": 130000,
    "refund_amount": 1500,
    "revenue_per_visitor": 30.5,
    "conversion_rate": 4.2
  }
}

Errors

400 bad_request (invalid metric, requesting revenue+mrr together, invalid date range), 401 unauthorized, 403 forbidden.

Revenue Timeseries

1
GET /api/v1/analytics/revenue/timeseries

Revenue over time — the same call as Revenue Overview, grouped into a time series.

Query Parameters

Same as Revenue Overview.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/revenue/timeseries?interval=30d" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
6
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/revenue/timeseries?interval=30d&metrics=revenue",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
console.log(data.currency, data.points);

Response

1
2
3
4
5
6
7
8
9
{
  "data": {
    "currency": "USD",
    "points": [
      { "date": "2026-09-01T00:00:00.000Z", "revenue": 4200 },
      { "date": "2026-09-02T00:00:00.000Z", "revenue": 3100 }
    ]
  }
}

Note the shape here is { currency, points: [...] }, not a bare array — currency is reported once since it doesn't vary per point.

Errors

Same as Revenue Overview.

Revenue by Source

1
GET /api/v1/analytics/revenue/by-source

Revenue grouped by the visitor's referrer or UTM parameters, using the last-touch attribution described above.

Query Parameters

ParameterTypeDefaultDescription
dimensionstringreferrerreferrer, referrer_url, utm_source, utm_medium, utm_campaign, utm_term, utm_content, or campaign.
metricscomma listrevenue, conversions, revenue_per_visitor, conversion_ratemrr and visitors are not available on this endpoint — see MRR above.
page, limitinteger1, 100Pagination, max limit 500.
(date range, filters)——Same as Analytics.

cURL

1
2
curl "https://convrs.dev/api/v1/analytics/revenue/by-source?dimension=utm_source" \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/revenue/by-source?dimension=utm_source",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
{
  "data": [
    { "utmSource": "newsletter", "revenue": 4200, "conversions": 6, "revenue_per_visitor": 12.35, "conversion_rate": 3.1 }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}

The dimension key matches Sources: referrer, referrerUrl, utmSource, utmMedium, utmCampaign, utmTerm, utmContent, or campaign.

Errors

400 bad_request (invalid dimension, or requesting mrr/visitors as a metric), 401 unauthorized, 403 forbidden.

Revenue by Campaign

1
GET /api/v1/analytics/revenue/by-campaign

Revenue grouped by campaign attribution — the same last-touch mechanism and the same underlying dimensions as Revenue by Source, scoped to just the two campaign-shaped dimensions.

Query Parameters

ParameterTypeDefaultDescription
dimensionstringutm_campaignutm_campaign (the raw UTM campaign value) or campaign (the full attribution query string, e.g. ?utm_source=x&utm_medium=y).
metrics, page, limit——Same as Revenue by Source.
(date range, filters)——Same as Analytics.

cURL

1
2
curl https://convrs.dev/api/v1/analytics/revenue/by-campaign \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/revenue/by-campaign",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
{
  "data": [
    { "utmCampaign": "summer_sale", "revenue": 9000, "conversions": 12, "revenue_per_visitor": 18.2, "conversion_rate": 5.4 }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}

Errors

Same as Revenue by Source.

Revenue by Page

1
GET /api/v1/analytics/revenue/by-page

Revenue grouped by the page the visitor was last on before completing a purchase (again, last-touch attribution).

Only page and hostname are offered — not entrypage/exitlink. Revenue events, being recorded from a server-side payment webhook rather than a live pageview, never populate entrypage/exitlink; every row would fall into a single empty bucket, so those two dimensions aren't exposed here rather than returning a misleading signal.

Query Parameters

ParameterTypeDefaultDescription
dimensionstringpagepage or hostname.
metrics, page (pagination), limit——Same as Revenue by Source.
(date range, filters)——Same as Analytics.

cURL

1
2
curl https://convrs.dev/api/v1/analytics/revenue/by-page \
  -H "Authorization: Bearer cvrs_..."

JavaScript

1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/revenue/by-page",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

Response

1
2
3
4
5
6
{
  "data": [
    { "page": "/pricing", "revenue": 12000, "conversions": 18, "revenue_per_visitor": 22.1, "conversion_rate": 6.7 }
  ],
  "pagination": { "page": 1, "limit": 100, "hasMore": false }
}

Errors

400 bad_request (invalid dimension — e.g. entrypage/exitlink are rejected here, or requesting mrr/visitors), 401 unauthorized, 403 forbidden.

Examples

Practical, end-to-end examples using real endpoint paths and parameter names. Every example uses cvrs_... as a placeholder — replace it with a real token from Settings → API Tokens. See Authentication if you haven't created one yet.

All JavaScript examples work in Node.js 18+ (native fetch) or any modern browser environment where it's safe to expose the token (typically a server, not client-side code — see the security guidance).

1. Get Account

Verify a token works.

1
2
curl https://convrs.dev/api/v1/account \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
const res = await fetch("https://convrs.dev/api/v1/account", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();
console.log(`Token for ${data.website.name}, scopes: ${data.token.scopes.join(", ")}`);

2. List Websites

1
2
curl https://convrs.dev/api/v1/websites \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
const res = await fetch("https://convrs.dev/api/v1/websites", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();
const website = data[0];

3. Get Analytics (Overview)

Visitors and revenue for the last 30 days.

1
2
curl "https://convrs.dev/api/v1/analytics?interval=30d&metrics=visitors,revenue" \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
6
const res = await fetch(
  "https://convrs.dev/api/v1/analytics?interval=30d&metrics=visitors,revenue",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
console.log(`${data.visitors} visitors, ${data.revenue} revenue`);

4. Get Analytics Timeseries

Daily visitors and revenue for a specific date range.

1
2
curl "https://convrs.dev/api/v1/analytics/timeseries?metrics=visitors,revenue&startAt=2026-09-01&endAt=2026-09-15" \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
6
7
8
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/timeseries?metrics=visitors,revenue&startAt=2026-09-01&endAt=2026-09-15",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
for (const point of data) {
  console.log(point.date, point.visitors, point.revenue);
}

5. Get Page Breakdown

Top pages by visitors, for the current month.

1
2
curl "https://convrs.dev/api/v1/analytics/pages?dimension=page&interval=mtd&limit=20" \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
6
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/pages?dimension=page&interval=mtd&limit=20",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data, pagination } = await res.json();
console.log(data.map((row) => `${row.page}: ${row.visitors}`));

6. Get Revenue

1
2
curl "https://convrs.dev/api/v1/analytics/revenue?interval=30d" \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
6
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/revenue?interval=30d",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
console.log(`${data.revenue} ${data.currency} over the last 30 days`);

7. Get Revenue Timeseries

1
2
curl "https://convrs.dev/api/v1/analytics/revenue/timeseries?interval=90d&metrics=revenue" \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
6
const res = await fetch(
  "https://convrs.dev/api/v1/analytics/revenue/timeseries?interval=90d&metrics=revenue",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
console.log(data.currency, data.points);

8. Get Customers

The 25 most recently created customers.

1
2
curl "https://convrs.dev/api/v1/customers?limit=25" \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
const res = await fetch(
  "https://convrs.dev/api/v1/customers?limit=25",
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();

9. Get Customer Activity

1
2
curl https://convrs.dev/api/v1/customers/cust_abc123/activity \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
6
7
8
9
const customerId = "cust_abc123";
const res = await fetch(
  `https://convrs.dev/api/v1/customers/${customerId}/activity`,
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
for (const day of data) {
  console.log(day.date, day.items.length, "events");
}

10. Get Goals

1
2
curl https://convrs.dev/api/v1/goals \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
const res = await fetch("https://convrs.dev/api/v1/goals", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();
console.log(data.map((g) => g.eventName));

11. Get Funnel Analytics

List funnels, then get step-by-step conversion for the first one.

1
2
3
4
5
curl https://convrs.dev/api/v1/funnels \
  -H "Authorization: Bearer cvrs_..."

curl https://convrs.dev/api/v1/analytics/funnels/cly8k2p3q0000abcxyz12345 \
  -H "Authorization: Bearer cvrs_..."
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const funnelsRes = await fetch("https://convrs.dev/api/v1/funnels", {
  headers: { Authorization: "Bearer cvrs_..." },
});
const { data: funnels } = await funnelsRes.json();
const funnelId = funnels[0].id;

const analyticsRes = await fetch(
  `https://convrs.dev/api/v1/analytics/funnels/${funnelId}`,
  { headers: { Authorization: "Bearer cvrs_..." } }
);
const { data: steps } = await analyticsRes.json();

for (const step of steps) {
  console.log(`${step.step}: ${step.users} users`);
}

Handling Errors

Every example above should check for a non-2xx response before reading data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const res = await fetch("https://convrs.dev/api/v1/analytics", {
  headers: { Authorization: "Bearer cvrs_..." },
});

if (!res.ok) {
  const { error } = await res.json();
  if (error.code === "rate_limit_exceeded") {
    const retryAfter = res.headers.get("Retry-After");
    // wait `retryAfter` seconds and retry
  }
  throw new Error(`${error.code}: ${error.message}`);
}

const { data } = await res.json();

See Errors for the full list of codes.