Rekomi Docs
For brandsInstall tracking
Brands

Install on React (Vite, CRA, Remix)

Vite and CRA install the script tag in index.html; Remix and React Router 7 install it in app/root.tsx. Sales record through your gateway or server-to-server, not a browser convert call.

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, but that does not matter for conversion tracking because sales are recorded off the page (through your payment gateway or a server-to-server call), not from a browser convert event.

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>
    <!-- Rekomi tracking -->
    <script async src="https://api.rekomi.com/api/v1/r/loader.js" data-program-id="YOUR_PROGRAM_ID"></script>
    <noscript>
      <img src="https://api.rekomi.com/api/v1/r/c.gif" width="1" height="1" alt="" referrerpolicy="no-referrer-when-downgrade" />
    </noscript>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Vite and CRA both leave an external <script src> in index.html untouched, so the tag ships as written.

Copy the snippet with your program ID already filled in from your campaign's install recipe under Setup > Install in the dashboard.

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, and you render the same script tag directly in the head JSX:

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 />
        {/* Rekomi tracking */}
        <script
          async
          src="https://api.rekomi.com/api/v1/r/loader.js"
          data-program-id={import.meta.env.VITE_REKOMI_PROGRAM_ID}
        />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Rendering the <script> element directly (not through Remix's <Scripts />, which is for app-bundled JS) injects the exact tag into the server-rendered document.

React Router 7 builds with Vite, so the environment variable needs the VITE_ prefix (declare VITE_REKOMI_PROGRAM_ID in .env) and is read via import.meta.env. Don't use process.env here: root.tsx re-renders during client hydration, where process is not defined, so the render throws a ReferenceError. Since the program ID is a public identifier, hard-coding it also works.

Recording the sale

The browser pixel no longer records sales, so there is no convert call to fire from a useEffect. A sale is recorded through one of these paths:

  • Your payment gateway. Connect it once in the Connect payment gateway step at /dashboard/setup/connect-sales (Stripe, Paddle, Braintree, Shopify, Wix, Lemon Squeezy, Chargebee, Polar, Recurly, Gumroad, Creem, or Dodo Payments) and sales, refunds, and cancellations record automatically.
  • Server-to-server. POST the sale from your backend to Rekomi's S2S endpoint with an HMAC signature. See Server-to-server tracking.
  • Coupon codes. Give each affiliate a unique discount code (Campaign > Coupons); a redeemed code credits them at checkout.

Carry the referral into checkout with window.Rekomi.getReferral(). For a Stripe Checkout Session created from your own backend, forward the returned slug so your server can stamp it on the subscription's metadata as rekomi_affiliate_slug (that is what Rekomi reads for attribution):

async function startCheckout(priceId: string) {
  const referral = window.Rekomi?.getReferral?.();
  const res = await fetch("/api/create-checkout-session", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(referral ? { "x-rekomi-referral": referral } : {}),
    },
    body: JSON.stringify({ priceId }),
  });
  const { url } = await res.json();
  window.location.href = url;
}

Capturing leads (optional)

window.Rekomi.convert() is now a LEAD capture call, not a sale. On a signup or waitlist route you can tie a customer email to the referral before payment so a later sale is still credited:

import { useEffect } from "react";

export function SignupThanks({ email }: { email: string }) {
  useEffect(() => {
    if (email) window.Rekomi?.ready?.(() => window.Rekomi.convert(email));
  }, [email]);

  return <div>Thanks for signing up!</div>;
}

Browser lead capture works on every plan; the server-side lead endpoint requires Starter or higher. See Track leads and signups.

React Router 6/7 client navigation

When a user navigates client-side from your marketing pages to a success route (no full document reload, just React Router's <Link> navigation), the head script does persist: it loaded on the initial document load and stays in browser memory. The referral captured on that first load is still available via window.Rekomi.getReferral(). No need to re-inject the script on route change.

This is the key behavior of SPAs: install once, read the referral wherever you need it.

Vite environment variables

For Vite, expose the program ID via VITE_REKOMI_PROGRAM_ID (the VITE_ prefix is required for browser exposure), then substitute it into index.html with a build-time replace or the %VITE_*% HTML transform:

<script async src="https://api.rekomi.com/api/v1/r/loader.js" data-program-id="%VITE_REKOMI_PROGRAM_ID%"></script>

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, and public/index.html supports %REACT_APP_*% substitution:

<script async src="https://api.rekomi.com/api/v1/r/loader.js" data-program-id="%REACT_APP_REKOMI_PROGRAM_ID%"></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. That's fine: click capture and getReferral() both work off the single initial load.

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

Strict Mode runs effects twice in development. React's <StrictMode> invokes useEffect twice in dev to surface bugs, so a lead convert() in an effect can fire twice locally. Rekomi de-dupes leads per affiliate and email, so the second call is a no-op, and this does not happen in production builds.

Troubleshooting

window.Rekomi is undefined when you call it on first render. The script loads async, so on slow networks it might not finish before your component mounts. Use window.Rekomi?.ready?.(cb) to defer until the loader is ready, or guard with if (window.Rekomi).

Vite build doesn't include the script tag. An external <script src> is preserved by Vite as-is; only inline non-module scripts get stripped. Use the external-src tag shown above rather than an inline block.

Remix root.tsx renders the script twice. You probably have it both in the head JSX and in a <Scripts /> block. Render it once via the head JSX, not via Remix's <Scripts /> (which is for app-bundled scripts).

Referral captured but the sale is not attributed. The referral did not travel into checkout. Forward window.Rekomi.getReferral() to your create-session endpoint and stamp it as subscription_data.metadata.rekomi_affiliate_slug, or include it in your S2S call.