Referrals in a Next.js app (App Router): the tag in the root layout, the partner's code kept by your own server, every signup reported as a lead, and sales from your billing system.
Before you start
- A key with the Referral tracking preset (the single scope
conversions:write), made under Settings, Developers. Keep it on your server asBOLTCLAW_API_KEY, never in a browser or an app. - The program's id, on its Setup tab, as
BOLTCLAW_PROGRAM_ID.
1. Put the tag on your site
Add the version 2 tag to every page, in your root layout or template. It keeps a
partner's code from ?via=<code> in a first-party cookie on your own domain,
bc_ref, for the program's cookie window, and gives you
BoltClawReferral.code() in the browser.
<script src="https://boltclaw.ai/referrals/v2.js" defer data-payment-links="auto"></script>
data-payment-links="auto" is only for Stripe Payment Links and the Buy Button;
data-welcome="auto" adds the friend's welcome. The tag makes no request at all
unless the welcome is on and the visitor arrived with a code.
2. Keep the code from your server
A middleware sets bc_ref from ?via= in your own response. Optionally it also
reports the visit as a click, for partner links that open straight on your site
(a click through boltclaw.ai/r/<code> is already counted). A click earns
nothing by itself.
// middleware.ts
import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";
const CODE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
const COOKIE_DAYS = 30; // the program's cookie window (30 days unless you changed it)
export function middleware(request: NextRequest, event: NextFetchEvent) {
const response = NextResponse.next();
const code = request.nextUrl.searchParams.get("via");
if (!code || code.length < 3 || code.length > 40 || !CODE.test(code)) return response;
response.cookies.set("bc_ref", code, {
path: "/", maxAge: COOKIE_DAYS * 86400, sameSite: "lax", secure: true, httpOnly: false,
});
event.waitUntil(
fetch("https://api.boltclaw.ai/v1/referrals/clicks", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BOLTCLAW_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
code,
// Your host's own header first; X-Forwarded-For's LAST entry is the one your proxy added
// (the first is whatever the visitor sent).
ip: request.headers.get("x-real-ip") ?? request.headers.get("x-forwarded-for")?.split(",").pop()?.trim(),
user_agent: request.headers.get("user-agent") ?? undefined,
referrer: request.headers.get("referer") ?? undefined,
landing_path: request.nextUrl.pathname,
}),
}).catch(() => undefined),
);
return response;
}
export const config = { matcher: ["/((?!_next/|api/).*)"] };
Why set the cookie from your server. Safari deletes a cookie a script
created once the visitor has gone 7 days of browsing without using your site,
and keeps one only 24 hours on a page reached through a tracking link, so a
code the tag keeps can be gone a week later. A cookie your OWN server sets in
its response keeps its full lifetime, unless it comes through somebody else's
host (a third-party CNAME or IP address), which Safari caps at 7 days
(WebKit's published tracking prevention rules, read 2026-10-23). Setting bc_ref
from your server, not HttpOnly, keeps the full window and the tag still reads
it. The @boltclaw/referrals package ships the same as
referralsMiddleware().
POST /v1/referrals/clicks hashes the address and classes the user agent as a
click on /r/<code> is, and stores neither.
3. Report each signup as a lead
Where your app creates an account (a server action or a route handler), report
it with your own id for the person and the code from the cookie. event_id is
the idempotency key: reporting the same signup twice changes nothing.
// referrals.js: server only
export async function reportConversion(body) {
const res = await fetch("https://api.boltclaw.ai/v1/referrals/conversions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BOLTCLAW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ program_id: process.env.BOLTCLAW_PROGRAM_ID, ...body }),
});
if (!res.ok) throw new Error(`Bolt Claw answered ${res.status}`);
return res.json();
}
// app/signup/actions.js
"use server";
import { cookies } from "next/headers";
import { reportConversion } from "@/lib/referrals";
export async function signUp(formData) {
const user = await createUser(formData); // your own code
const code = (await cookies()).get("bc_ref")?.value;
await reportConversion({
event: "lead",
event_id: `signup-${user.id}`,
customer: { id: user.id },
referral: code ? { code } : undefined,
});
}
Report from the server, never from the browser: anything shipped to a visitor can be read by that visitor.
4. Record sales
Connect the billing system you take payments with on the Setup tab (see
Connect your billing system), and pass the code to checkout
as its guide shows: sales, renewals and refunds are then reported for you, and
the setup status ticks them off. Not on one of the twelve? Report each sale from
your server with event: "sale", your own event_id for the payment, the same
customer.id as the signup, amount in minor units and currency.
5. Send a test sale
Press Send a test sale on the Setup tab: a click, a signup, a sale with two
items and a partial refund go through your real rules as test data, credited to
the program's test partner, never paid and never counted. To check your own
wiring, send one of your own conversions with "test": true: it is test data
too, and Clear test data deletes it all. The setup status's rows turn green
only for live events.