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 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.

React installs vary by build tool. Vite and Create React App ship a static index.html entry point where the head script goes. Remix and React Router 7 use Server-Side Rendering with app/root.tsx as the document shell. Both patterns share the same client-side behavior: the SPA never does a full page reload on route change, so the convert event fires from a useEffect in your success route component rather than from a top-level document load handler.

A note on Create React App: the React team deprecated CRA in early 2025. New projects should use Vite (or Remix / React Router 7 for SSR). The CRA install pattern still works if you have an existing CRA app, but plan to migrate.

Vite + CRA: install in index.html

In your project's index.html (root for Vite, public/index.html for CRA):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Your App</title>
    <script>
      (function(){
        var s=document.createElement('script');
        s.src='https://js.rekomi.com/v1/rekomi.js';
        s.async=true;
        s.dataset.programId='YOUR_PROGRAM_ID';
        document.head.appendChild(s);
      })();
    </script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Vite serves index.html at dev time and inlines it at build time. CRA injects build-time variables but the script approach is identical.

Your program ID is in Rekomi at Settings > Tracking.

Remix / React Router 7: install in app/root.tsx

Remix (now React Router 7 under its post-merge naming) does SSR and doesn't have a static index.html. The document shell lives in app/root.tsx:

import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
        <script
          dangerouslySetInnerHTML={{
            __html: `(function(){
              var s=document.createElement('script');
              s.src='https://js.rekomi.com/v1/rekomi.js';
              s.async=true;
              s.dataset.programId='${process.env.REKOMI_PROGRAM_ID}';
              document.head.appendChild(s);
            })();`,
          }}
        />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Place the <script> element BEFORE <Meta /> and <Links /> if you want the loader available during the first server-rendered paint. After <Meta />/<Links /> is also fine — the difference is sub-100ms timing on slow networks.

Fire convert from a useEffect

Once installed, window.Rekomi is globally available after hydration. On your success route:

import { useEffect } from "react";
import { useSearchParams } from "react-router-dom"; // or "react-router" for v7

export function SuccessPage() {
  const [searchParams] = useSearchParams();

  useEffect(() => {
    const sessionId = searchParams.get("session_id");
    if (!sessionId) return;

    const amount = parseInt(searchParams.get("amount") || "0", 10);
    const email = searchParams.get("email") || "";

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

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

useEffect runs after the component mounts, by which time the loader has hydrated. The if (window.Rekomi) guard handles cases where the script hasn't finished loading yet (rare on production but worth keeping).

React Router 6/7 client navigation

When a user navigates client-side from your marketing pages to the success route (no full document reload, just React Router's <Link> navigation), the head script does persist — it was loaded on the initial document load and stays in browser memory. The cookie set during click capture is still readable. No need to re-fire any install snippet on route change.

This is the key behavior of SPAs: install once, fire convert per route as needed.

Vite environment variables

For Vite, expose the program ID via VITE_REKOMI_PROGRAM_ID (the VITE_ prefix is required for browser exposure):

<script>
  (function(){
    var s=document.createElement('script');
    s.src='https://js.rekomi.com/v1/rekomi.js';
    s.async=true;
    s.dataset.programId='__VITE_REKOMI_PROGRAM_ID__';
    document.head.appendChild(s);
  })();
</script>

Then use a Vite plugin or vite.config.ts define block to substitute __VITE_REKOMI_PROGRAM_ID__ at build time. Alternatively, hard-code the program ID in index.html since it's a public identifier (not a secret).

CRA environment variables

CRA exposes vars with the REACT_APP_ prefix at build time. CRA's public/index.html supports %REACT_APP_REKOMI_PROGRAM_ID% substitution:

<script>
  (function(){
    var s=document.createElement('script');
    s.src='https://js.rekomi.com/v1/rekomi.js';
    s.async=true;
    s.dataset.programId='%REACT_APP_REKOMI_PROGRAM_ID%';
    document.head.appendChild(s);
  })();
</script>

Set REACT_APP_REKOMI_PROGRAM_ID in .env and your CI/build env.

Quirks worth knowing

SPA doesn't reload between pages. Unlike a multi-page app, React Router navigates client-side without a full document load. The head script loads once on initial visit; subsequent route changes don't re-run it. Convert events fire from useEffect per route, not from a global document handler.

CRA is deprecated. The React team retired Create React App in early 2025. Vite is the recommended successor for SPA work. If you're on CRA, the install works but plan to migrate.

Remix renamed to React Router 7 in 2024. The library merge is complete; what you might know as Remix now ships as React Router 7. The app/root.tsx pattern is unchanged; only the package name (react-router instead of @remix-run/react).

Hot Module Replacement and the loader. In Vite dev mode, HMR doesn't reload index.html, so the head script keeps its initial state across component updates. You don't need to worry about double-firing during development.

Strict Mode runs useEffect twice in development. React's <StrictMode> invokes useEffect twice in dev to surface bugs. Your convert event would fire twice in dev. This doesn't happen in production builds. Rekomi de-dupes on external_event_id server-side, so even if it did fire twice, the second would be a no-op.

Troubleshooting

window.Rekomi is undefined in useEffect on first render. The script loads async, so on slow networks it might not finish before your component mounts. Add a small delay or use a polling loop:

useEffect(() => {
  let attempts = 0;
  const interval = setInterval(() => {
    if (window.Rekomi) {
      clearInterval(interval);
      window.Rekomi.convert({ /* ... */ });
    } else if (++attempts > 50) {
      clearInterval(interval); // Give up after 5 seconds
    }
  }, 100);
  return () => clearInterval(interval);
}, []);

Convert fires twice in dev mode. React Strict Mode running useEffect twice. Production doesn't have this problem. Rekomi de-dupes anyway.

Vite build doesn't include the script tag. Vite strips <script> from index.html if it doesn't have type="module" — except it leaves IIFE-style inline scripts alone. The pattern above uses an IIFE so Vite preserves it.

Remix root.tsx renders the script twice. You probably have it both in the JSX and in a <Scripts /> block. The pattern above is the correct one — render via JSX, not via Remix's <Scripts /> (which is for app-bundled scripts).

Related

  • JavaScript pixel reference
  • Install on Stripe — for the client-side referral capture pattern
  • Install on Vue — for the Vue equivalent

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.

Install on Vue

Vite Vue 3 installs in index.html. Nuxt 3 uses useHead in app.vue or the script array in nuxt.config.ts. Convert fires from onMounted on the success page.

On this page

Vite + CRA: install in index.htmlRemix / React Router 7: install in app/root.tsxFire convert from a useEffectReact Router 6/7 client navigationVite environment variablesCRA environment variablesQuirks worth knowingTroubleshootingRelated