Developer Docs / Web React SDK / Ads

Web React In-App Advertising

Initialize ads in a React app, show GrowthCat-managed banners and interstitials, build custom ad UI, and validate rewarded ads server-side.

Ad formats

Banner and interstitial placements, including rewarded formats.

Exports

GrowthCatAdBanner, GrowthCatAdInterstitial, and useAd.

Init order

Initialize GrowthCat once. Ads config is fetched from the SDK bootstrap.

1. Overview

Ads use the same GrowthCat client as referrals. During initialization, GrowthCat fetches the SDK bootstrap and configures the ad service with the returned ads settings.

2. Install

bash
npm install @growthcat/web react react-dom
# or
yarn add @growthcat/web react react-dom

Install the GrowthCat integration skill

The WebSDK repository includes an agent skill that inspects your app, checks the installed SDK types, and adds referrals, attribution, ads, sponsorships, or feedback using your existing architecture.

bash
npx skills add . --skill growthcat-web-integration

Run this from a local GrowthCatSDK-Web checkout. For a remote install, replace the dot with the repository Git URL. Then tell your coding agent which capability and placement you want.

Use $growthcat-web-integration to add a feedback board to my settings page.

3. Initialize GrowthCat

Call GrowthCat.initialize() before rendering any ad components.

ts
import { GrowthCat } from "@growthcat/web";

GrowthCat.initialize({
  apiKey: process.env.NEXT_PUBLIC_GROWTHCAT_KEY!,
  workspace: "live",
  logsEnabled: process.env.NODE_ENV !== "production",
  measurementMode: "essential",
});

Use analytics only after your app establishes consent or another valid legal basis. You can apply consent changes immediately with GrowthCat.setMeasurementMode().

4. Initialize Ads

Ads are initialized by the SDK bootstrap. After GrowthCat.initialize(), the client has access toadsConfig, including whether ads are enabled and the cache TTL.

ts
const client = GrowthCat.shared;

if (client.adsConfig?.adsEnabled) {
  GrowthCat.shared.prefetchAdCatalogs(["home_banner", "level_interstitial"]);
}

await GrowthCat.shared.refreshSDKBootstrap();

6. Interstitial Ads

tsx
import { useState } from "react";
import { GrowthCatAdInterstitial } from "@growthcat/web/react";

export function LevelCompleteScreen() {
  const [showAd, setShowAd] = useState(false);

  return (
    <>
      <button onClick={() => setShowAd(true)}>Continue</button>
      <GrowthCatAdInterstitial
        placementKey="level_complete_interstitial"
        appUserId="user_123"
        isOpen={showAd}
        onDismiss={() => setShowAd(false)}
        onReward={(response) => {
          // Called only after successful server validation.
          grantCoins(response.reward?.amount ?? 0);
          setShowAd(false);
        }}
        onRewardError={(error) => {
          // Validation failed: close without granting a reward.
          console.error(error.message);
          setShowAd(false);
        }}
      />
    </>
  );
}

Rewarded interstitials

Pass appUserId for rewarded ads. Only grant rewards from onReward or after validateAdReward confirms the reward server-side.

A dismissed ad, elapsed client timer, validation error, or offline state must never grant value. TreatonNoFill as an expected empty state and continue without showing an error.

7. Custom Ad Rendering

Use useAd() when GrowthCat should select and track the ad, but your app owns the visual design. Custom renderers must measure real visibility; the deprecated trackImpression option must not be used for load-time impressions.

tsx
import { useEffect, useRef } from "react";
import { useAd } from "@growthcat/web/react";

export function CustomBanner({ placementKey }: { placementKey: string }) {
  const ref = useRef<HTMLButtonElement>(null);
  const { ad, state, creativeInstanceId, trackEvent } = useAd({ placementKey });

  useEffect(() => {
    if (!ad || !ref.current) return;
    let timer: ReturnType<typeof setTimeout> | undefined;
    const observer = new IntersectionObserver(([entry]) => {
      clearTimeout(timer);
      if (entry.intersectionRatio >= 0.5 && document.visibilityState === "visible") {
        timer = setTimeout(() => {
          trackEvent("impression", {
            visible_fraction: entry.intersectionRatio,
            visible_duration_ms: 1000,
          });
        }, 1000);
      }
    }, { threshold: 0.5 });
    observer.observe(ref.current);
    return () => { clearTimeout(timer); observer.disconnect(); };
  }, [ad, creativeInstanceId, trackEvent]);

  if (state === "loading") return <div style={{ height: 60 }} />;
  if (!ad) return null;

  const { creative } = ad;

  return (
    <button
      ref={ref}
      onClick={() => {
        trackEvent("click");
        if (creative.destinationUrl) window.open(creative.destinationUrl, "_blank", "noopener,noreferrer");
      }}
    >
      <small>Sponsored</small>
      {creative.publicAssetUrl ? <img src={creative.publicAssetUrl} alt="" /> : null}
      <strong>{creative.headline}</strong>
      <span>{creative.ctaText}</span>
    </button>
  );
}

8. Manual Loading And Events

ts
const ad = await GrowthCat.shared.loadAd({ placementKey: "rewarded_coins" });
if (ad) showWatchAdButton();

const creativeInstanceId = GrowthCat.makeCreativeInstanceId();
GrowthCat.shared.trackAdEvent("impression", ad, {
  format: "banner",
  placementKey: "home_banner",
  appUserId: "user_123",
  sessionId,
  creativeInstanceId,
  metadata: { visible_fraction: 0.75, visible_duration_ms: 1250 },
});

GrowthCat.shared.trackAdEvent("click", ad, {
  format: "banner",
  placementKey: "home_banner",
  appUserId: "user_123",
  creativeInstanceId,
});

Generate one creativeInstanceId for the presentation and reuse it for its impression and clicks. Available events also include video start, progress, completion, close, and report events.

9. Reward Validation

ts
const response = await GrowthCat.shared.validateAdReward(ad, "user_123", {
  sessionId,
  viewedSeconds: 30,
  completed: true,
});

if (response.rewardValidated) {
  grantCoins(response.reward?.amount ?? 0);
}

Never grant a reward if rewardValidated is false or the validation call throws.

10. Offline Behavior

  • Catalog fetch failures can return a cached entry while the TTL is valid.
  • Tracking events are persisted to localStorage and replayed automatically when the browser comes back online.
  • Reward validation throws when offline, so no reward should be granted.