RekomiRekomiBlogPricing
Rekomi Docs
Rekomi Docs
Welcome to Rekomi
Quickstart for brandsPlans and trialsIntegrationsStripe Connect (sales tracking)Organization settingsTeam managementNotifications
Install tracking

Payment platforms

Install on StripeStripe Marketplace appInstall on PaddleInstall on BraintreeInstall on ShopifyInstall on Lemon SqueezyInstall on ChargebeeInstall on PolarInstall on RecurlyInstall on GumroadInstall on CreemInstall on Dodo PaymentsS2S API (any gateway)

Membership and course platforms

Install on MemberstackInstall on OutsetaInstall on PodiaInstall on TeachableInstall on Thinkific

Newsletter and creator platforms

Install on GhostInstall on beehiivInstall on Kit (ConvertKit)

Site builders

Install on WordPressInstall on WebflowInstall on FramerInstall on SquarespaceInstall on WixInstall on BubbleInstall on Carrd

Frameworks

Install on Vanilla JavaScriptInstall on Next.js App RouterInstall on Next.js Pages RouterInstall on React (Vite, CRA, Remix)Install on VueInstall on RailsInstall on Django

Campaigns and commissions

CampaignsCommission modelsPay per click or lead (CPC & CPL)Coupon-code attributionSub-affiliate recruitingTracking and attribution

Affiliates

Recruit affiliatesManage affiliatesAI co-pilotApply to the curated network

Money flow

SalesPayoutsMulti-currencyTax formsReports

Email

Sending domainBroadcasts
For brandsInstall tracking
|Brands|

Install on Next.js Pages Router

Use next/script in pages/_app.tsx. Don't paste raw script tags in _document.tsx, they conflict with serialization. Convert fires from useEffect in a success page using useRouter.

The Pages Router (Next.js's pre-App-Router pattern, still supported through Next 16) installs Rekomi via the next/script component in _app.tsx so the loader runs on every page render. Don't paste raw <script> tags in _document.tsx — that conflicts with how Pages Router serializes documents in Next 14+. The convert event fires from useEffect in your success page using useRouter for query params.

Install in pages/_app.tsx

Edit pages/_app.tsx and add the <Script> component inside the App component's return:

import type { AppProps } from "next/app";
import Script from "next/script";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Script
        id="rekomi"
        src="https://js.rekomi.com/v1/rekomi.js"
        strategy="afterInteractive"
        data-program-id={process.env.NEXT_PUBLIC_REKOMI_PROGRAM_ID}
      />
    </>
  );
}

strategy="afterInteractive" defers loading until after hydration, which is the right balance for click capture.

Set NEXT_PUBLIC_REKOMI_PROGRAM_ID in .env.local (and your hosting env vars). The NEXT_PUBLIC_ prefix is required for browser exposure.

Why not _document.tsx?

In older Next.js (pre-14), _document.tsx was a common spot for third-party scripts via <Head> content. In Next 14+, the Pages Router's hydration pipeline can race with raw script tags in _document.tsx, causing the loader to run before React has finished hydrating. Symptoms: window.Rekomi undefined when client components try to call it, double-firing convert events on fast networks.

next/script in _app.tsx solves this by integrating with Next's hydration lifecycle. Stick with _app.tsx.

Fire convert from a success page

On pages/success.tsx (or whichever route is your post-checkout destination):

import { useEffect } from "react";
import { useRouter } from "next/router";

export default function SuccessPage() {
  const router = useRouter();

  useEffect(() => {
    const sessionId = router.query.session_id as string;
    if (!sessionId) return;

    const amount = parseInt((router.query.amount as string) || "0", 10);
    const email = (router.query.email as string) || "";

    if (window.Rekomi) {
      window.Rekomi.convert({
        external_event_id: sessionId,
        amount_cents: amount,
        currency: "USD",
        customer_email: email,
      });
    }
  }, [router.query]);

  return <div>Thanks for your purchase!</div>;
}

useRouter().query exposes URL params parsed by Next. The useEffect runs after the page mounts, by which time window.Rekomi is hydrated.

For production, prefer fetching the canonical order data from your backend (a /api/checkout-session/[id] route) rather than trusting URL params — a buyer could append ?amount=1 to fake a refund-sized commission.

Stripe Checkout from a Pages Router app

The standard pattern: a Pages API route at pages/api/create-checkout-session.ts creates the Stripe Session server-side, then redirects the browser to the Stripe-hosted URL. The Rekomi referral travels via an HTTP header from the browser to your API route:

// pages/api/create-checkout-session.ts
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";

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

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== "POST") return res.status(405).end();

  const referral = req.headers["x-rekomi-referral"] as string | undefined;
  const { priceId } = req.body;

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    client_reference_id: referral,
    subscription_data: referral
      ? { metadata: { rekomi_affiliate_slug: referral } }
      : undefined,
  });

  res.json({ url: session.url });
}

The browser-side code that calls this endpoint reads the referral from window.Rekomi.getReferral() and forwards it as a header. See the Install on Stripe doc for the full client-side pattern.

Quirks worth knowing

next/script belongs in _app.tsx, not _document.tsx. Don't follow older Stack Overflow answers that put scripts in _document.tsx <Head>. That pattern fights Next 14+'s hydration in subtle ways.

strategy="afterInteractive" is the right choice. Same as App Router — load after hydration but before idle.

router.query is empty on first render. Pages Router's router.query is populated AFTER the page mounts. The useEffect with [router.query] dep array re-runs once params are available; that's why convert fires from useEffect, not from the component body.

Image domains config. If you load OG images from Rekomi, add js.rekomi.com and api.rekomi.com to next.config.js's images.domains array.

getServerSideProps can also pass the referral. If your success page uses SSR, you can read the cookie via context.req.cookies.rekomi_via in getServerSideProps and pass it as a prop. Useful if you want to verify the referral server-side before showing the success state.

Troubleshooting

window.Rekomi is undefined when calling from useEffect. The Script hasn't loaded yet. Move the convert call into a setTimeout(..., 100) or check window.Rekomi existence before calling (the if (window.Rekomi) guard above handles this).

Convert fires twice on the success page. The useEffect is re-running because router.query changes from {} to populated. Add a guard: only fire if sessionId is present and you haven't already fired (use a useRef to track).

API route returns 500. The STRIPE_SECRET_KEY env var is missing. Set it in .env.local and your hosting env vars.

Script tag appears but no cookie set. CSP blocking. Check the browser console for CSP violations and adjust next.config.js headers.

Related

  • JavaScript pixel reference
  • Install on Stripe — for the client-side referral capture pattern
  • Install on Next.js App Router — the modern pattern; consider migrating

Install on Next.js App Router

Use next/script in app/layout.tsx with strategy="afterInteractive". Raw script tags in metadata get stripped. Server Actions can pass the affiliate slug via cookies().

Install on React (Vite, CRA, Remix)

Vite and CRA install in index.html; Remix and React Router 7 install in app/root.tsx. SPA navigation doesn't reload pages, so convert fires from a useEffect in your success route.

On this page

Install in pages/_app.tsxWhy not _document.tsx?Fire convert from a success pageStripe Checkout from a Pages Router appQuirks worth knowingTroubleshootingRelated