Clerk
Conversion tracking is available on the Business plan and above .
In conversion tracking, a lead event is an
action that signals interest in your product:
- signing up for an account;
- booking a demo;
- joining a mailing list.
The lead event establishes the customer’s identity and the link they came from: every later action (a purchase, a plan upgrade) is attributed to that same link.
In this guide, we will be focusing on tracking new user sign-ups for a SaaS application that uses Clerk for user authentication.
Prerequisites
First, enable conversion tracking for your links — without it a click is never linked to the lead and sale events that follow.
You can skip this step for partner program links: RevRoute enables conversion tracking on them automatically.
For the whole workspace. Open Settings → Analytics and turn on the Conversion tracking switch in the page header — it controls the workspace-level setting. Every link created from that point on has conversion tracking enabled.
For a single link. Open the
link builder and turn
on the Conversion tracking switch — inside the builder it is also bound to
the C keyboard shortcut.
Via the API. Pass trackConversion: true when creating or updating a link
(it defaults to false):
curl -X POST "https://api.revroute.ru/links" \
-H "Authorization: Bearer rr_xxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"trackConversion": true
}'Then install the RevRoute client-side script on your site — it records the click and stores its ID, which is what lead events are attached to.
The RevRoute client-side script is served from our domain — there are no npm
packages to install. It picks up the rr_id query parameter, stores it in a
first-party rr_id cookie on your domain (90 days) and uses it to attribute
future conversions to the original link.
Add the tag to the <head> section of every page on your site:
<script defer src="https://app.revroute.ru/analytics/script.js"></script>In Next.js (App Router), use next/script in the root layout:
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
<Script defer src="https://app.revroute.ru/analytics/script.js" />
</html>
);
}You can verify the installation in two ways:
- Open the browser console and type
_revRouteAnalytics— if the script is installed, the object shows up in the console. - Add the
?rr_id=testquery parameter to your site URL and check that therr_idcookie is set in your browser.
If neither works, make sure that:
- the script tag sits in the
<head>section of the page; - any CDN cache in front of your site has been purged.
The server-side client
The examples below use the TypeScript SDK from the dub npm package. Always
pass the RevRoute API serverURL to the client — without it the SDK will not
talk to our API.
npm install dubimport { Dub } from "dub";
export const revroute = new Dub({
token: process.env.REVROUTE_API_KEY, // secret rr_* key from your workspace settings
serverURL: "https://api.revroute.ru", // required: the RevRoute API address
});Keep the secret key (rr_*) in environment variables and never expose it to
the browser. Create one under API keys .
The SDK is optional: /track/lead and /track/sale are plain HTTPS endpoints
you can call from any language.
Configure Clerk
Here’s a quick summary of the steps:
Add environment variables
Add the following environment variables to your app:
# get it here: https://dashboard.clerk.com/apps/new
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
CLERK_SECRET_KEY=your_secret_key
# get it here: https://app.revroute.ru/settings/tokens
REVROUTE_API_KEY=your_api_keyAdd a custom claim to your Clerk session token
Add the following JSON as a custom claim to your Clerk session token:
{
"metadata": "{{user.public_metadata}}"
}Wire the client script to Clerk's `useUser` hook
Wrap the RevRoute client script in a component that calls a trackLead server action. No npm package needed — the script is served from our domain.
"use client";
import { trackLead } from "@/actions/track-lead";
import { useUser } from "@clerk/nextjs";
import { useEffect } from "react";
export function RevRouteAnalytics() {
const { user } = useUser();
useEffect(() => {
if (!user || user.publicMetadata.revrouteClickId) return;
// if the user is loaded but hasn't been persisted to RevRoute yet, track the lead event
trackLead({
id: user.id,
name: user.fullName!,
email: user.primaryEmailAddress?.emailAddress,
avatar: user.imageUrl,
}).then(async (res) => {
if (res.ok) await user.reload();
else console.error(res.error);
});
// you can also use an API route instead of a server action
/*
fetch("/api/track-lead", {
method: "POST",
body: JSON.stringify({
id: user.id,
name: user.fullName,
email: user.primaryEmailAddress?.emailAddress,
avatar: user.imageUrl,
}),
}).then(res => {
if (res.ok) await user.reload();
else console.error(res.statusText);
});
*/
}, [user]);
return (
<script
defer
src="https://app.revroute.ru/analytics/script.js"
data-api-host="https://api.revroute.ru"
/>
);
}Then, add the DubAnalytics component to your app’s root layout component:
import { RevRouteAnalytics } from "@/components/analytics-overview";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<RevRouteAnalytics />
{children}
</body>
</html>
);
}Implement the `trackLead` server action
On the server side, implement the trackLead server action. Alternatively, you can also create an API route instead:
// This is a server action
"use server";
import { revroute } from "@/lib/revroute";
import { clerkClient } from "@clerk/nextjs/server";
import { cookies } from "next/headers";
export async function trackLead({
id,
name,
email,
avatar,
}: {
id: string;
name?: string | null;
email?: string | null;
avatar?: string | null;
}) {
try {
const cookieStore = await cookies();
const clickId = cookieStore.get("rr_id")?.value;
if (clickId) {
// Send lead event to RevRoute
await revroute.track.lead({
clickId,
eventName: "Sign Up",
customerExternalId: id,
customerName: name,
customerEmail: email,
customerAvatar: avatar,
});
// Delete the rr_id cookie
cookieStore.set("rr_id", "", {
expires: new Date(0),
});
}
const clerk = await clerkClient();
await clerk.users.updateUser(id, {
publicMetadata: {
revrouteClickId: clickId || "n/a",
},
});
return { ok: true };
} catch (error) {
console.error("Error in trackLead:", error);
return { ok: false, error: (error as Error).message };
}
}Request fields for POST /track/lead:
| Field | Required | Description |
|---|---|---|
clickId | Yes | ID of the click the lead is attributed to. Read it from the rr_id cookie. For a deferred lead an empty string is allowed — the click is then taken from the customer found by customerExternalId. |
eventName | Yes | Name of the event (e.g. Sign up). It also ties the lead to a later sale through leadEventName in /track/sale. |
customerExternalId | Yes | The customer’s ID in your system. All future events for this customer are attributed through it. |
discountCode | No | The partner’s discount code, for customers who arrive without clicking a link. If both clickId and discountCode are sent, clickId wins. |
customerName | No | Customer name. If omitted, a random one is generated. |
customerEmail | No | Customer email. |
customerAvatar | No | Customer avatar URL. |
mode | No | async (default) — don’t block the request; wait — block until the event is recorded; deferred — defer lead creation to a later request. |
eventQuantity | No | Numeric value of the event (e.g. seats provisioned in a trial): the lead is counted that many times. |
metadata | No | Arbitrary event data, up to 10,000 characters when stringified. |
Where to see conversions
Once everything is set up, tracked conversions show up in your workspace analytics in three views:
- Time series — clicks, leads and sales over time: Analytics .
- Funnel — conversion and drop-off at every step (clicks → leads → sales): Analytics in funnel mode .
- Events stream — every conversion event across all links in the workspace, in real time: Events .