Connect a payment to its first visit

Read the visitor cookie on your server and pass RouteRev metadata to Stripe Checkout.

Keep identity on your server

Read _rr_vid from the incoming request’s cookies; do not ask the browser to supply metadata in a checkout body. Use your authenticated account ID for rr_user_id. The visitor cookie is an attribution signal, not authentication.

For subscriptions, place the same metadata in subscription_data.metadata as well as the Checkout session. RouteRev records subscription payments from invoices; invoice attribution reads subscription metadata. For a one-time payment, use mode: payment and session metadata.

Next.js route handler

Adapt your existing server Checkout handler. checkoutContext is your own app helper: it must authenticate the user and provide a trusted price and return URLs. STRIPE_SECRET_KEY is a server-only setting in your product, never part of the RouteRev snippet or client bundle.

import Stripe from 'stripe';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
import { checkoutContext } from './checkout-context';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: Request) {
  // Your own authenticated user, server-side price and return URLs.
  const { userId, priceId, successUrl, cancelUrl } =
    await checkoutContext(request);
  const visitorId = (await cookies()).get('_rr_vid')?.value;
  const metadata = {
    ...(visitorId ? { rr_visitor_id: visitorId } : {}),
    rr_user_id: userId,
  };
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    metadata,
    subscription_data: { metadata },
    success_url: successUrl,
    cancel_url: cancelUrl,
  });
  return NextResponse.json({ url: session.url });
}

Plain Node example

Pass a Stripe client and trusted checkout context from your existing server. This helper extracts only _rr_vid from the Cookie header and ignores a malformed encoded cookie.

export async function createCheckout(stripe, context) {
  // cookieHeader comes from your server request, not a client body.
  // userId, priceId and return URLs come from your trusted app logic.
  const { cookieHeader, userId, priceId, successUrl, cancelUrl } = context;
  const cookie = (cookieHeader || '').split(';')
    .map(part => part.trim()).find(part => part.startsWith('_rr_vid='));
  let visitorId;
  try { visitorId = cookie ? decodeURIComponent(cookie.slice(8)) : undefined; }
  catch { visitorId = undefined; }
  const metadata = {
    ...(visitorId ? { rr_visitor_id: visitorId } : {}),
    rr_user_id: userId,
  };
  return stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    metadata,
    subscription_data: { metadata },
    success_url: successUrl,
    cancel_url: cancelUrl,
  });
}

Connect the webhook

Connecting the webhook is done with us during early access. RouteRev receives invoice.paid, checkout.session.completed and charge.refunded at /api/hooks/stripe/<product-slug>, verifying each product’s webhook signature.

Checkout metadata is not automatically copied to every Stripe charge. Refund attribution depends on the metadata present on the charge; ask us to check it with your integration.