How to track purchases in Next.js
The purchase is the only event with money attached, so it is the one event you cannot afford to lose to an ad blocker or a capped cookie. It must be confirmed by the system that took the payment.
Client event (fast, blockable) + server event (authoritative) → collapsed on the order id
Send it twice, on purpose. The client event tells you what the person did and when; the server event confirms it happened and carries the context the browser never had. A shared the order id collapses the two into one record marked confirmed by both sides.
Never send only the client event for anything with money attached — ad blockers see roughly 20–30% of paid-traffic sessions.
The server-side event
This is the one that has to be right. It runs on your infrastructure, so no ad blocker, browser policy or network condition can stop it.
// app/api/webhooks/stripe/route.ts — the payment processor is the truth
export async function POST(req: Request) {
const event = stripe.webhooks.constructEvent(await req.text(), sig, secret);
if (event.type === "checkout.session.completed") {
const s = event.data.object;
await flowsk.purchase({
anonymousId: s.metadata.fsk_id, // put the id in metadata at checkout
email: s.customer_details.email,
valueCents: s.amount_total,
dedupKey: s.id, // the session id de-dups client + server
source: "server",
});
}
return new Response(null, { status: 200 });
}
What goes wrong
Three mistakes account for nearly every broken purchases implementation we see.
Firing on the thank-you page only — refreshes double-count and blocked pages under-count.
Sending the cart total instead of the amount actually captured, so refunds and failed payments stay in your numbers.
No de-duplication key, so the client event and the server event both land as separate sales.
Frequently asked questions
Should I track purchases client-side or server-side?
Both. The client event captures behaviour in real time and is cheap; the server event is authoritative and cannot be blocked. Give them a shared de-duplication key (the order id) and they collapse into one confirmed record instead of double counting.
What is the de-duplication key for purchases in Next.js?
The order id — a value both sides already know. That is the whole requirement: the browser and your backend must be able to derive the same string independently.
What goes wrong most often?
Firing on the thank-you page only — refreshes double-count and blocked pages under-count.
Why does the server event matter more here?
The purchase is the only event with money attached, so it is the one event you cannot afford to lose to an ad blocker or a capped cookie. It must be confirmed by the system that took the payment.
Related guides
See the receipt for every conversion.
Flowsk Signals stitches the anonymous click to the email to the purchase — first-party, server-side, de-duplicated on a key you choose. One snippet, $29/mo, and a journey you can inspect event by event.
More free tools
Same deal — instant, no signup.