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.
<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_IDwith the Website ID from your Convrs dashboard.
HTML & Vanilla JS
The simplest installation — add the Convrs tracking script anywhere in your HTML.
<!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
asyncensures 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:
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:
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.
Option A — index.html (Recommended)
The simplest approach is to add the Convrs script directly to your index.html file.
Vite
<!-- 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
<!-- 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.
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
index.html (Recommended)
<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
<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.
nuxt.config.ts (Recommended)
export default defineNuxtConfig({
app: {
head: {
script: [
{
src: 'https://convrs.dev/script.js',
'data-website-id': 'YOUR_WEBSITE_ID',
async: true,
},
],
},
},
});
app.vue
<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.
<!-- 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.
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.
---
---
<!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:persistto the script tag to prevent reinitialization.
Gatsby
Install Convrs globally using Gatsby SSR APIs.
gatsby-ssr.js
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
// 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
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:
<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
- Open your Webflow project.
- Click Project Settings.
- Open the Custom Code tab.
- Paste the following into Head Code:
<script
async
src="https://convrs.dev/script.js"
data-website-id="YOUR_WEBSITE_ID">
</script>
- Save changes.
- Republish your website.
Convrs will immediately begin tracking pageviews once your site is published.
Shopify
Install Convrs across your entire Shopify storefront.
Installation
- Open your Shopify Admin.
- Navigate to Online Store → Themes.
- Select Actions → Edit Code.
- Open
layout/theme.liquid. - Paste the following before the closing
</head>tag:
<script
async
src="https://convrs.dev/script.js"
data-website-id="YOUR_WEBSITE_ID">
</script>
- 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
npm install @convrs/sdk
Quick Start
import { initConvrs } from "@convrs/sdk";
const convrs = await initConvrs({
websiteId: "YOUR_WEBSITE_ID",
});
Track Events
convrs.track("signup", {
source: "homepage",
});
Identify Users
convrs.identify("user_123", {
email: "user@example.com",
plan: "pro",
});
React / Next.js Example
lib/analytics.ts
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
const convrs = await initConvrs({
websiteId: "YOUR_WEBSITE_ID",
autoCapturePageviews: true,
});
Configuration Options
| Option | Type | Description |
|---|---|---|
websiteId | string | Your Convrs website ID |
domain | string | Override the current hostname |
apiUrl | string | Custom ingestion endpoint |
debug | boolean | Enable debug logging |
allowLocalhost | boolean | Enable localhost tracking |
allowIframe | boolean | Enable tracking inside iframes |
allowedHostnames | string[] | Domains used for cross-domain tracking |
Example Configuration
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.
| Attribute | Required | Description |
|---|---|---|
data-website-id | Yes | Your unique Website ID from the Convrs dashboard. |
data-api | No | Override the default tracking endpoint. Useful for custom domains, proxies, or self-hosted ingestion. |
data-debug | No | Enable debug logging in the browser console. |
data-allow-localhost | No | Enable tracking while running on localhost. |
Example
<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-idis 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
convrs.track(eventName, properties?)
Parameters
| Parameter | Type | Description |
|---|---|---|
eventName | string | Name of the event. |
properties | object | Optional event metadata. |
Examples
convrs.track("signup");
convrs.track("purchase", {
amount: 99,
currency: "USD",
plan: "Pro",
});
identify()
Associate events with a user.
Syntax
convrs.identify(userId, traits?)
Parameters
| Parameter | Type | Description |
|---|---|---|
userId | string | Unique identifier for the user. |
traits | object | Optional user attributes. |
Examples
convrs.identify("user_123");
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
autoCapturePageviewsis enabled.
Track Current Page
convrs.trackPageview();
Track Custom Path
convrs.trackPageview("/pricing");
flush()
Immediately send all queued events.
Syntax
await convrs.flush();
Example
await convrs.flush();
Useful before:
- Page unloads
- Checkout redirects
- Critical conversion events
reset()
Reset the current visitor and session.
Syntax
convrs.reset();
Example
function logout() {
convrs.reset();
}
Useful when a user logs out.
getConvrsClient()
Access the initialized Convrs client anywhere in your application.
Syntax
import { getConvrsClient } from "@convrs/sdk";
const convrs = getConvrsClient();
Example
import { getConvrsClient } from "@convrs/sdk";
const convrs = getConvrsClient();
convrs.track("feature_used");
buildCrossDomainUrl()
Generate a URL containing Convrs tracking parameters.
Syntax
convrs.buildCrossDomainUrl(url);
Example
const url = convrs.buildCrossDomainUrl(
"https://app.example.com/signup"
);
Result:
https://app.example.com/signup?_cv_vid=xxx&_cv_sid=xxx
getTrackingParams()
Retrieve the current visitor and session identifiers.
Syntax
const params = convrs.getTrackingParams();
Example
const params = convrs.getTrackingParams();
console.log(params);
Result:
{
visitorId: "visitor_123",
sessionId: "session_456"
}
Complete Example
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
window.convrs(eventName, properties)
| Parameter | Type | Description |
|---|---|---|
eventName | string | Name of the event to track. |
properties | object | Optional metadata associated with the event. |
Event names should be descriptive and consistent. Examples:
signup,purchase,invite_sent,checkout_started.
Event Examples
Button Click
<button onclick="window.convrs('cta_clicked', { location: 'hero' })">
Get Started
</button>
Form Submission
document
.querySelector('#signup-form')
.addEventListener('submit', () => {
window.convrs('signup_submitted');
});
React — Button Click
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
window.convrs('add_to_cart', {
product_id: 'sku_1234',
product_name: 'Wireless Headphones',
price: 79.99,
currency: 'USD',
});
Purchase Completed
window.convrs('purchase', {
order_id: 'order_123',
amount: 99,
currency: 'USD',
plan: 'pro',
});
Video Played
document
.querySelector('#hero-video')
.addEventListener('play', () => {
window.convrs('video_played', {
video: 'hero_demo',
});
});
Checkout Started
window.convrs('checkout_started', {
plan: 'pro',
billing_cycle: 'monthly',
});
Invite Sent
window.convrs('invite_sent', {
team_size: 5,
});
Event Properties
Properties provide additional context for an event.
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:
window.convrs('signup');
window.convrs('checkout_started');
window.convrs('purchase');
window.convrs('invite_sent');
Avoid:
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
npm install @convrs/ai-bot-sdk
2. Add it to your server
Most common setup — Next.js middleware:
// 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:
| Category | Example | What 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.txtand/llms-full.txt/sitemap.xmland other sitemap files- Any path ending in
.xmlunder/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:
- Is this a
GET/HEADrequest? (configurable) - Does the User-Agent match a known bot? If not, stop here — nothing is sent.
- Is the path a static asset, an ignored prefix, or over the length limit?
- 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
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:
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:
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:
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.
POST https://ingest.convrs.dev/api/ai-crawls
Content-Type: application/json
Authorization: Bearer cvbot_... (only if you've enabled required auth)
Request body:
{
"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"
}
}
| Field | Required | Notes |
|---|---|---|
siteId | Yes | Your project token. websiteId is accepted as an alias. |
url | Yes | Absolute URL the crawler requested. Hostname must belong to your site. |
domain | Recommended | Hostname that received the request. Falls back to the URL's own hostname if omitted. |
bot.userAgent | Yes | Full raw User-Agent string. Classification happens from this alone. |
bot.ip | Recommended | Crawler's source IP as seen by your server. |
bot.statusCode | Optional | Your response status, 100-599. Omit if unavailable. |
bot.source | Optional | Free-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:
{ "success": true, "tracked": true, "category": "training_crawler" }
or, for a user-agent that isn't a known bot:
{ "success": true, "tracked": false, "reason": "not_a_bot" }
Optional request authentication
Add a site-specific token without touching your existing integration:
- Open your Bot traffic card settings and generate a token.
- Add it to your SDK config or raw HTTP header.
- Once deployed, flip on Reject unauthenticated requests.
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:
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
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.
See which tweets drive your traffic and revenue
Referrer data full of generic t.co links doesn't tell you much. Convrs's X integration matches those links back to the actual tweets, so you can see exactly which posts are sending traffic — and revenue — to your site.
Requires a Convrs Growth plan.
Why this matters
A shortened link tells you nothing about what's actually working. With tweet-level attribution, you can:
- Spot your best-performing accounts — see who's consistently sending high-value traffic.
- See ROI at the tweet level — know which specific posts convert, not just which ones get clicks.
- Track campaigns properly — follow a hashtag push or social promo through to real traffic and revenue numbers.
How it works
Convrs continuously scans X for links to your site and ties the resulting traffic and revenue back to the tweet that sent it. To see it in action:
- Open the Referrers card on your main dashboard.
- Add an X filter to isolate that traffic.
There are two ways Convrs attributes X traffic:
1. Posts that link to your site
Any time someone tweets a link to your website, Convrs picks it up automatically — nothing to configure.
2. Link-in-bio attribution
If your site is linked in an X profile's bio (yours, a teammate's, or an affiliate's), add that handle so Convrs can attribute the traffic it sends:
- Go to Settings → Integrations.
- Find the Twitter / X card.
- Click the Link attribution tab.
- Add the handle(s) that link to your site, e.g.
@convrsdev.
Only add accounts that genuinely have your website link in their profile — your own account, or a trusted affiliate account. Attribution for existing traffic and revenue can take a few hours to backfill once you add a handle.
Go further
Once you know which tweets are driving results, pair this with X mentions tracking to catch every mention of your brand and product — even the ones that aren't linking directly to your site yet.
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
- A visitor lands on your website.
- Convrs assigns a unique visitor and session ID.
- You pass those IDs to Stripe when creating a checkout session or payment intent.
- Stripe sends the payment data to Convrs.
- Convrs attributes revenue to the original visitor, campaign, and traffic source.
Required Metadata
When creating a payment, include the following metadata:
| Key | Description |
|---|---|
convrs_visitor_id | Unique visitor identifier assigned by Convrs. |
convrs_session_id | Session 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.
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.
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.
const tracking =
convrs.getTrackingParams();
console.log(tracking);
Example response:
{
visitorId: "visitor_123",
sessionId: "session_456"
}
Recommended Flow
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
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:
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:
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:
| Parameter | Type | Description |
|---|---|---|
websiteId | string | Defaults to the website your token is scoped to. If provided, it must match that website or the request is rejected with 403 forbidden. |
interval | string | Relative date range: 24h, 7d, 30d, 90d, 1y, mtd, qtd, ytd. Defaults to 30d. Ignored if startAt/endAt are set. |
startAt / endAt | string (ISO 8601) | Explicit date range. Takes precedence over interval. Maximum range: 366 days. |
timezone | string (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:
{
"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:
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
| Resource | Description |
|---|---|
| Authentication | Token format, creating tokens, scopes, website scope |
| Errors | Error envelope and status codes |
| Pagination | List response format, limits, defaults |
| Rate Limits | Request limits and the 429 response |
| Account | Verify a token and see authenticated context |
| Websites | The website your token is scoped to |
| Analytics | Overview, timeseries, breakdowns, realtime, bot traffic |
| Goals | Goal catalogue, completions over time, goal properties |
| Customers | Customer list, detail, and event activity |
| Payments | Redacted payment records |
| Funnels | Funnel definitions and step-by-step conversion analytics |
| Revenue | Revenue overview, timeseries, and attribution breakdowns |
| Examples | End-to-end cURL and JavaScript examples |
Metadata Endpoint
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
curl https://convrs.dev/api/v1/meta \
-H "Authorization: Bearer cvrs_..."
Response:
{
"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:
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 legacybc_prefix and continue to work. - A missing or malformed
Authorizationheader, or an invalid token, returns401 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.
| Scope | Grants access to |
|---|---|
analytics.read | Account, websites, core analytics (overview/timeseries/pages/sources/geo/devices/realtime/bots), customers, funnels, revenue |
goals.read | Goal catalogue, goal completions over time, goal properties |
payments.read | The redacted payments list |
apis.read | Shorthand for every current and future read scope |
apis.all | Full 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:
{
"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:
{
"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 Status | Code | When it happens |
|---|---|---|
| 400 | bad_request | Invalid 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. |
| 401 | unauthorized | Missing/malformed Authorization header, an invalid token, an expired token, or a revoked token. |
| 403 | forbidden | The token doesn't have the required scope, or the request's websiteId doesn't match the token's own website. |
| 404 | not_found | The requested resource (a customer or a funnel) doesn't exist in your website. |
| 429 | rate_limit_exceeded | You've exceeded 60 requests/minute for this token. See Rate Limits. |
| 500 | internal_server_error | An unexpected server error. If this persists, it's worth reporting. |
Notes
- A
401doesn't distinguish between "token never existed" and "token was revoked" — both return the same generic message, to avoid leaking information about token history. - A
404for a customer or funnel ID from a different website looks identical to a404for an ID that never existed at all — the API never confirms or denies that a resource exists outside your own website. 400validation errors include a specific message naming the offending parameter, e.g.`startAt` must be before `endAt`.orMetric \mrr` is not available on this endpoint.`
Pagination
Response Format
List/breakdown endpoints return a data array alongside a pagination object:
{
"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
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
page | integer | 1 | — | 1-indexed page number. |
limit | integer | 100 | 500 | Rows 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:
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{
"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
metricsin one call to/api/v1/analyticsinstead 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
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
curl https://convrs.dev/api/v1/account \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
{
"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"
}
}
}
| Field | Description |
|---|---|
user.id, user.name, user.email | The user who created this token. user is null if that user record can't be resolved. |
website.id, website.name, website.domain | The website this token is scoped to. |
token.name | The name given to this token when it was created. |
token.scopes | The scopes granted to this token. |
plan.plan, plan.planFamily | The 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
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
curl https://convrs.dev/api/v1/websites \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch("https://convrs.dev/api/v1/websites", {
headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();
Response
{
"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"
}
]
}
| Field | Description |
|---|---|
id | The website ID (ws_-prefixed). Use this as websiteId elsewhere in the API. |
name | Workspace/website name. |
domain | The tracked domain. |
timezone | IANA timezone used for this website's default analytics bucketing. |
currency | ISO 4217 display currency for revenue figures. |
createdAt | ISO 8601 creation timestamp. |
plan, planFamily | The current billing plan. |
cookielessMode | Whether cookieless tracking mode is enabled. |
status | "active" or "inactive", based on billing entitlement. |
Errors
401 unauthorized, 403 forbidden (missing websites.read scope).
Get Website
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
| Parameter | Description |
|---|---|
websiteId | The website ID, e.g. ws_abc123. Must match your token's own website. |
cURL
curl https://convrs.dev/api/v1/websites/ws_abc123 \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
| Metric | Description |
|---|---|
visitors | Distinct visitors. Convrs measures unique visitors, not raw pageview/session totals — there is no pageviews or sessions metric. |
new_visitors | Distinct visitors seen for the first time in this bucket. |
returning_visitors | Distinct visitors who had already been seen before this bucket. |
bounce_rate | Percentage of single-pageview sessions. |
avg_session_duration | Average session duration, in seconds. |
conversion_rate | Percentage of sessions whose visitor completed a purchase. |
revenue_per_visitor | Revenue divided by distinct visitors. |
revenue | Total revenue (in the website's display currency). |
mrr | Monthly 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_revenue | Revenue from non-refunded charges. |
refund_amount | Total refunded amount. |
conversions | Goal 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 -:
| Filter | Example |
|---|---|
country | country=US or country=US,CA or country=-US |
city | city=New York |
region | region=NY |
continent | continent=NA |
device | device=Desktop |
browser | browser=Chrome |
os | os=Windows |
referer | referer=google.com |
refererUrl | refererUrl=https://google.com/search |
hostname | hostname=blog.example.com |
page | page=/pricing |
entrypage | entrypage=/ |
exitlink | exitlink=/checkout |
utm_source, utm_medium, utm_campaign, utm_term, utm_content | utm_source=newsletter |
goal | goal=signup — restricts to sessions where this goal fired |
saleType | new or recurring |
Plus the shared websiteId/interval/startAt/endAt/timezone parameters from the overview.
Overview
GET /api/v1/analytics
A single composite snapshot for one date range.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
websiteId | string | your token's website | Must match your token's website. |
interval | string | 30d | 24h, 7d, 30d, 90d, 1y, mtd, qtd, ytd. Ignored if startAt is set. |
startAt / endAt | ISO 8601 | — | Explicit range, max 366 days. |
timezone | IANA string | UTC | |
metrics | comma list | all except mrr | Any 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
curl "https://convrs.dev/api/v1/analytics?interval=30d&metrics=visitors,revenue" \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
{
"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
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
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
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
{
"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
GET /api/v1/analytics/pages
Breakdown by page, hostname, entry page, or exit link.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
dimension | string | page | page, hostname, entrypage, or exitlink. |
metrics | comma list | visitors, conversions, revenue, revenue_per_visitor, conversion_rate | Subset of those five. |
page, limit | integer | 1, 100 | Pagination — see Pagination. limit max 500. |
| (date range, filters) | — | — | Same as Overview. |
cURL
curl "https://convrs.dev/api/v1/analytics/pages?dimension=page&limit=50" \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
{
"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
GET /api/v1/analytics/sources
Breakdown by referrer or UTM parameters.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
dimension | string | referrer | referrer, 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
curl "https://convrs.dev/api/v1/analytics/sources?dimension=utm_source" \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/analytics/sources?dimension=utm_source",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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
GET /api/v1/analytics/geo
Breakdown by country, region, city, or continent.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
dimension | string | country | country, region, city, or continent. |
metrics, page, limit | — | — | Same as Pages. |
| (date range, filters) | — | — | Same as Overview. |
cURL
curl "https://convrs.dev/api/v1/analytics/geo?dimension=country" \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/analytics/geo?dimension=country",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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
GET /api/v1/analytics/devices
Breakdown by device type, browser, or OS.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
dimension | string | device | device, browser, or os. |
metrics, page, limit | — | — | Same as Pages. |
| (date range, filters) | — | — | Same as Overview. |
cURL
curl "https://convrs.dev/api/v1/analytics/devices?dimension=browser" \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/analytics/devices?dimension=browser",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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
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
curl https://convrs.dev/api/v1/analytics/realtime \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch("https://convrs.dev/api/v1/analytics/realtime", {
headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();
Response
{
"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 }]
}
}
| Field | Description |
|---|---|
count | Visitors active in the last 30 seconds. |
pages | Current pages being viewed: { page, count }. |
points | Geo points for a live map: { id, latitude, longitude, value }. |
referrers | { source, count }. |
countries | { country, code, count }. |
Errors
401 unauthorized, 403 forbidden.
Bots
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
| Parameter | Type | Default | Description |
|---|---|---|---|
breakdown | string | count | count (overview), timeseries, providers, top_pages, or categories. |
domain | string | — | Filter to one tracked hostname. |
category | string | — | answer_agent, index_crawler, training_crawler, or other. |
granularity | string | auto | hour, day, or week — only used by breakdown=timeseries. |
page, limit | integer | 1, 100 | Pagination — 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
curl "https://convrs.dev/api/v1/analytics/bots?breakdown=categories" \
-H "Authorization: Bearer cvrs_..."
JavaScript
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:
{
"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:
{
"data": [
{ "start": "2026-09-01T00:00:00.000Z", "gptbot": 40, "claudebot": 12 }
]
}
breakdown=providers / top_pages / categories — a paginated array, e.g. for providers:
{
"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
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
| Parameter | Type | Default | Description |
|---|---|---|---|
websiteId | string | your token's website | Must match your token's website. |
page, limit | integer | 1, 100 | Pagination, max limit 500. See Pagination. |
cURL
curl https://convrs.dev/api/v1/goals \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch("https://convrs.dev/api/v1/goals", {
headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();
Response
{
"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 }
}
| Field | Description |
|---|---|
eventName | The goal's name, as fired by the tracker (convrs.track("signup")) or via data-goal="signup". |
eventType | Always "goals". |
trigger | How the goal fires — currently always "goal". |
firstSeenAt, lastSeenAt | ISO 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
GET /api/v1/analytics/goals
Goal completions, bucketed over time.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
websiteId, interval, startAt/endAt, timezone | — | — | Same as Analytics overview. |
goals | comma list | all goals | Goal names to filter to (from List Goals above). |
cURL
curl "https://convrs.dev/api/v1/analytics/goals?goals=signup" \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/analytics/goals?goals=signup",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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
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
| Parameter | Type | Default | Description |
|---|---|---|---|
websiteId | string | your token's website | Must match your token's website. |
goal | string | all goals | The 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, limit | integer | 1, 100 | Pagination, max limit 500. |
cURL
curl "https://convrs.dev/api/v1/analytics/goals/properties?goal=signup" \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/analytics/goals/properties?goal=signup",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"data": [
{ "propertyKey": "plan", "propertyValue": "pro", "count": 42 },
{ "propertyKey": "plan", "propertyValue": "free", "count": 118 }
],
"pagination": { "page": 1, "limit": 100, "hasMore": false }
}
| Field | Description |
|---|---|
propertyKey | The custom property's key, e.g. "plan". |
propertyValue | The value recorded for that key, e.g. "pro". |
count | Distinct 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
GET /api/v1/customers
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
websiteId | string | your token's website | Must match your token's website. |
limit | integer | 100 | Max 500. |
No
pageparameter. This endpoint returns the firstlimitcustomers only, ordered newest-first.hasMoreis a heuristic (truewhen the returned count equalslimit), not an exact page boundary — see Pagination for why. Paging further than 500 customers isn't currently supported.
cURL
curl "https://convrs.dev/api/v1/customers?limit=50" \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/customers?limit=50",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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 }
}
| Field | Description |
|---|---|
id | Customer ID — use this as :customerId in the endpoints below. |
name, email, avatar | Identity fields, null for anonymous (never-identified) visitors. |
externalId | Your own user ID (from identify()), or the visitor ID for anonymous visitors. |
stripeCustomerId | Set if this customer has a linked Stripe customer. |
country, device, browser | From the visitor's session. |
sales, saleAmount | Number of payments and total amount (smallest currency unit) attributed to this customer. |
firstSaleAt, subscriptionCanceledAt | ISO 8601 timestamps, null if not applicable. |
createdAt, updatedAt | ISO 8601 timestamps. |
Errors
400 bad_request (invalid limit), 401 unauthorized, 403 forbidden.
Get Customer
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
| Parameter | Description |
|---|---|
customerId | The customer ID from the list endpoint above. |
cURL
curl https://convrs.dev/api/v1/customers/cust_abc123 \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
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
| Parameter | Description |
|---|---|
customerId | The customer ID. |
cURL
curl https://convrs.dev/api/v1/customers/cust_abc123/activity \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/customers/cust_abc123/activity",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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
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), andcustomerEmailare never returned, regardless of which fields you request — there's no parameter to opt into them.
Authentication: Bearer token · Scope: payments.read
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
websiteId | string | your token's website | Must match your token's website. |
page, limit | integer | 1, 100 | Pagination, max limit 500. See Pagination. |
cURL
curl "https://convrs.dev/api/v1/payments?limit=50" \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/payments?limit=50",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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 }
}
| Field | Description |
|---|---|
id | Payment ID. |
amount | In the smallest currency unit (e.g. cents for USD). |
currency | ISO 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). |
provider | stripe, dodo, polar, paddle, or lemonsqueezy. |
isRecurring | Whether this payment settles a subscription. |
billingInterval | day, week, month, year, or null for a one-off payment. |
createdAt | ISO 8601 timestamp. |
attributionStatus | pending, attributed, or unattributed — see Revenue for what attribution means here. |
customerId | The 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
GET /api/v1/funnels
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
websiteId | string | your token's website | Must match your token's website. |
page, limit | integer | 1, 100 | Pagination, max limit 500. |
cURL
curl https://convrs.dev/api/v1/funnels \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch("https://convrs.dev/api/v1/funnels", {
headers: { Authorization: "Bearer cvrs_..." },
});
const { data } = await res.json();
Response
{
"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.
| Field | Description |
|---|---|
id, name, createdAt, updatedAt | Funnel metadata. |
steps | Ordered 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
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
| Parameter | Description |
|---|---|
funnelId | The funnel ID from the list endpoint. |
cURL
curl https://convrs.dev/api/v1/funnels/cly8k2p3q0000abcxyz12345 \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
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_viewsteps are not currently supported. The underlying analytics query only matches a step against goal-event names — a step of typepage_view(matched by page path) will always show0users 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 ofgoal-type steps for accurate results today.- No date-range parameters. The underlying query always computes all-time data — there is no
startAt/endAt/intervalparameter for this endpoint. Parameters that would silently be ignored aren't offered at all.- No
stepValueor 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
| Parameter | Description |
|---|---|
funnelId | The funnel ID. |
cURL
curl https://convrs.dev/api/v1/analytics/funnels/cly8k2p3q0000abcxyz12345 \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/analytics/funnels/cly8k2p3q0000abcxyz12345",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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 }]
}
]
}
| Field | Description |
|---|---|
step | The step's value (goal name). |
users | Distinct visitors who completed this step and every step before it, in order. |
topSources | Top 3 referrers reaching this step: { referer, visitors, pct }. |
topCountries | Top 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. attributionStatuson 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
GET /api/v1/analytics/revenue
A revenue-focused snapshot for one date range, with an explicit currency field.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
websiteId, interval, startAt/endAt, timezone | — | — | Same as Analytics overview. |
metrics | comma list | all except mrr | revenue, 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
curl "https://convrs.dev/api/v1/analytics/revenue?interval=30d" \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
{
"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
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
curl "https://convrs.dev/api/v1/analytics/revenue/timeseries?interval=30d" \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
{
"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
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
| Parameter | Type | Default | Description |
|---|---|---|---|
dimension | string | referrer | referrer, referrer_url, utm_source, utm_medium, utm_campaign, utm_term, utm_content, or campaign. |
metrics | comma list | revenue, conversions, revenue_per_visitor, conversion_rate | mrr and visitors are not available on this endpoint — see MRR above. |
page, limit | integer | 1, 100 | Pagination, max limit 500. |
| (date range, filters) | — | — | Same as Analytics. |
cURL
curl "https://convrs.dev/api/v1/analytics/revenue/by-source?dimension=utm_source" \
-H "Authorization: Bearer cvrs_..."
JavaScript
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
{
"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
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
| Parameter | Type | Default | Description |
|---|---|---|---|
dimension | string | utm_campaign | utm_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
curl https://convrs.dev/api/v1/analytics/revenue/by-campaign \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/analytics/revenue/by-campaign",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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
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
pageandhostnameare offered — notentrypage/exitlink. Revenue events, being recorded from a server-side payment webhook rather than a live pageview, never populateentrypage/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
| Parameter | Type | Default | Description |
|---|---|---|---|
dimension | string | page | page or hostname. |
metrics, page (pagination), limit | — | — | Same as Revenue by Source. |
| (date range, filters) | — | — | Same as Analytics. |
cURL
curl https://convrs.dev/api/v1/analytics/revenue/by-page \
-H "Authorization: Bearer cvrs_..."
JavaScript
const res = await fetch(
"https://convrs.dev/api/v1/analytics/revenue/by-page",
{ headers: { Authorization: "Bearer cvrs_..." } }
);
const { data } = await res.json();
Response
{
"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.
curl https://convrs.dev/api/v1/account \
-H "Authorization: Bearer cvrs_..."
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
curl https://convrs.dev/api/v1/websites \
-H "Authorization: Bearer cvrs_..."
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.
curl "https://convrs.dev/api/v1/analytics?interval=30d&metrics=visitors,revenue" \
-H "Authorization: Bearer cvrs_..."
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.
curl "https://convrs.dev/api/v1/analytics/timeseries?metrics=visitors,revenue&startAt=2026-09-01&endAt=2026-09-15" \
-H "Authorization: Bearer cvrs_..."
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.
curl "https://convrs.dev/api/v1/analytics/pages?dimension=page&interval=mtd&limit=20" \
-H "Authorization: Bearer cvrs_..."
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
curl "https://convrs.dev/api/v1/analytics/revenue?interval=30d" \
-H "Authorization: Bearer cvrs_..."
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
curl "https://convrs.dev/api/v1/analytics/revenue/timeseries?interval=90d&metrics=revenue" \
-H "Authorization: Bearer cvrs_..."
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.
curl "https://convrs.dev/api/v1/customers?limit=25" \
-H "Authorization: Bearer cvrs_..."
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
curl https://convrs.dev/api/v1/customers/cust_abc123/activity \
-H "Authorization: Bearer cvrs_..."
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
curl https://convrs.dev/api/v1/goals \
-H "Authorization: Bearer cvrs_..."
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.
curl https://convrs.dev/api/v1/funnels \
-H "Authorization: Bearer cvrs_..."
curl https://convrs.dev/api/v1/analytics/funnels/cly8k2p3q0000abcxyz12345 \
-H "Authorization: Bearer cvrs_..."
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:
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.