Recipe: track revenue server-side
The reliable place to record revenue is your backend, right after a payment settles. Browser tracking can be blocked or the tab can close before a purchase confirms. Emitting the event server-side from your payment handler guarantees it counts, and lets you make it idempotent.
The three things that make it work
Section titled “The three things that make it work”unit: pass the same id you experiment on (the visitor id, user id, or account id you use for assignment). This is what ties the revenue to the right variant or flag.valuein cents: send minor units so metric math is exact.$49.99is4999.eventId: a stable idempotency key so a retried webhook does not double-count.
-
Emit the event when the payment succeeds
Section titled “Emit the event when the payment succeeds”Do this in the code path that confirms the charge, a Stripe webhook handler, a queue worker, or the success branch of your checkout logic.
POST /v1/track curl -X POST https://statskit.ai/api/v1/track \-H "Authorization: Bearer ssk_live_…" \-H "Content-Type: application/json" \-d '{"unit": "user_8f21","event": "purchase","value": 4999,"currency": "usd","props": { "plan": "pro" },"eventId": "evt_ord_10432"}'On payment success import { StatsKitServerClient } from "@statskit/sdk";const statskit = new StatsKitServerClient({apiKey: process.env.VERSUCH_SERVER_KEY!, // ssk_live_…});await statskit.track("user_8f21", "purchase", {value: 4999,currency: "usd",props: { plan: "pro" },eventId: "evt_ord_10432",});Stripe webhook handler export async function POST(req: Request) {const event = await verifyStripeWebhook(req);if (event.type === "checkout.session.completed") {const s = event.data.object;await statskit.track(s.metadata.user_id, "purchase", {value: s.amount_total, // Stripe amounts are already in centscurrency: s.currency,eventId: event.id, // Stripe event id = idempotency key});}return new Response("ok");} -
Use the same unit you experiment on
Section titled “Use the same unit you experiment on”If your experiments assign on
user_id, pass that sameuser_idasunit. If they assign on the visitor id, pass the visitor id (read it off the purchase’sstatskit_visitor_idmetadata; see Stripe attribution). A mismatched unit will not link the revenue to any assignment. -
Make it idempotent with
Section titled “Make it idempotent with eventId”eventIdPayment webhooks retry. Pass a stable
eventIdderived from the payment (the order id, or the Stripeevent.id) so re-delivery is de-duplicated. Do not use a random uuid, a retry would generate a new one and count the sale twice. -
Turn it into a revenue metric
Section titled “Turn it into a revenue metric”Define a custom metric with
aggregation: "sum",event: "purchase",direction: "higher",format: "usd", and attach it as the primary metric on the experiment. Revenue then shows per variant in results.
Related
Section titled “Related”- Tracking events, the full
POST /v1/trackfield reference and browser vs server guidance. - Revenue attribution with Stripe, how vid/sid metadata links purchases to journeys.