SDKs & Integrations

Next.js Integration

Drop-in package for Next.js App Router. Includes server-side client, React hooks, pre-built components, and webhook utilities. Wraps @outfame/sdkso you don’t need to install it separately.

FeatureDetails
Package@outfame/nextjs
Version3.1.0
Next.js≥ 14.0 (App Router)
React≥ 18.0
TypeScript≥ 5.0
LicenseMIT
Sourcegithub.com/outfame/outfame-nextjs

Installation

npm install @outfame/nextjs
# or
yarn add @outfame/nextjs
# or
pnpm add @outfame/nextjs
# or
bun add @outfame/nextjs

The package includes @outfame/sdkas a dependency — no separate install needed.


Quick setup

1. Environment variables

Add your API credentials to .env.local:

# .env.local
OUTFAME_API_KEY=sk_live_your_api_key
OUTFAME_WEBHOOK_SECRET=whsec_your_webhook_secret

# Optional: Override the default API base URL
# OUTFAME_API_URL=https://api.outfame.com/v1

2. Add the provider

Wrap your app with OutfameProvider in the root layout. This makes the client available to all hooks and components.

// app/layout.tsx
import { OutfameProvider } from "@outfame/nextjs";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <OutfameProvider
          apiKey={process.env.OUTFAME_API_KEY!}
          config={{
            revalidateOnFocus: true,
            refreshInterval: 30000, // Poll every 30s for live growth data
          }}
        >
          {children}
        </OutfameProvider>
      </body>
    </html>
  );
}

API routes

Handle webhook events in an App Router API route.

// app/api/outfame/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyWebhook, type OutfameWebhookEvent } from "@outfame/nextjs/webhooks";

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get("x-outfame-signature") ?? "";
  const timestamp = request.headers.get("x-outfame-timestamp") ?? "";

  // Verify the webhook signature to ensure it's from Outfame
  const isValid = verifyWebhook({
    payload: body,
    signature,
    timestamp,
    secret: process.env.OUTFAME_WEBHOOK_SECRET!,
  });

  if (!isValid) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const event: OutfameWebhookEvent = JSON.parse(body);

  switch (event.type) {
    case "account.growth_milestone":
      console.log(`Milestone: ${event.data.milestone} followers reached`);
      // Notify your team, update your database, trigger celebrations
      break;

    case "engagement.daily_summary":
      console.log(`Daily growth: +${event.data.net_growth} followers`);
      console.log(`Engagement rate: ${event.data.engagement_rate}%`);
      break;

    case "targeting.suggestion_ready":
      console.log(`AI targeting suggestions available for ${event.data.account_id}`);
      break;

    case "campaign.status_changed":
      console.log(`Campaign ${event.data.campaign_id}: ${event.data.status}`);
      break;
  }

  return NextResponse.json({ received: true });
}

Server Components

Fetch analytics server-side with zero client JS. The API call runs at the edge during rendering.

// app/dashboard/page.tsx
import { outfame } from "@outfame/nextjs/server";

export default async function DashboardPage() {
  // Fetch data server-side — no loading spinners, instant render
  const [account, analytics, targeting] = await Promise.all([
    outfame.accounts.retrieve("acc_7Gx2kLm9Qr"),
    outfame.analytics.overview("acc_7Gx2kLm9Qr", { period: "30d" }),
    outfame.targeting.config("acc_7Gx2kLm9Qr"),
  ]);

  return (
    <div className="space-y-8">
      <header>
        <h1>@{account.instagram_username}</h1>
        <p className="text-gray-500">
          {account.followers_count.toLocaleString()} followers
        </p>
      </header>

      {/* Growth overview */}
      <section className="grid grid-cols-3 gap-6">
        <MetricCard
          label="Net Growth"
          value={`+${analytics.net_growth.toLocaleString()}`}
          trend={analytics.growth_trend}
        />
        <MetricCard
          label="Engagement Rate"
          value={`${analytics.engagement_rate}%`}
          trend={analytics.engagement_trend}
        />
        <MetricCard
          label="AI Quality Score"
          value={`${(targeting.ai_optimization.quality_threshold * 100).toFixed(0)}%`}
        />
      </section>

      {/* Audience demographics */}
      <section>
        <h2>Audience Breakdown</h2>
        <p>
          {analytics.audience_quality.real_percentage}% real, engaged followers.
          {analytics.audience_quality.bot_percentage}% estimated bot accounts.
        </p>
      </section>
    </div>
  );
}

function MetricCard({ label, value, trend }: {
  label: string;
  value: string;
  trend?: "up" | "down" | "stable";
}) {
  return (
    <div className="rounded-xl border p-6">
      <p className="text-sm text-gray-500">{label}</p>
      <p className="text-3xl font-bold">{value}</p>
      {trend && (
        <span className={trend === "up" ? "text-emerald-500" : "text-rose-500"}>
          {trend === "up" ? "\u2191" : "\u2193"} vs. last period
        </span>
      )}
    </div>
  );
}

React Server Actions

Manage accounts and targeting from forms. API key stays on the server, interactions stay instant.

// app/campaigns/actions.ts
"use server";

import { outfame } from "@outfame/nextjs/server";
import { revalidatePath } from "next/cache";

export async function createGrowthCampaign(formData: FormData) {
  const username = formData.get("username") as string;
  const competitors = (formData.get("competitors") as string)
    .split(",")
    .map((s) => s.trim());
  const hashtags = (formData.get("hashtags") as string)
    .split(",")
    .map((s) => s.trim());

  // Create account with targeting config
  const account = await outfame.accounts.create({
    instagram_username: username,
    platform: "instagram",
    targeting_config: {
      competitor_accounts: competitors,
      hashtags,
      ai_optimization: {
        enabled: true,
        mode: "balanced",
        quality_threshold: 0.75,
      },
    },
  });

  // Start engagement
  await outfame.engagement.resume(account.id);

  revalidatePath("/campaigns");
  return { success: true, accountId: account.id };
}

export async function pauseCampaign(accountId: string) {
  await outfame.engagement.pause(accountId, {
    reason: "Paused by user",
  });
  revalidatePath("/campaigns");
}

export async function updateTargeting(accountId: string, formData: FormData) {
  const mode = formData.get("mode") as "conservative" | "balanced" | "aggressive";

  // Get AI suggestions before updating
  const suggestions = await outfame.targeting.suggestions(accountId, {
    goal: "growth",
    includeReasoning: true,
  });

  // Apply suggestions
  await outfame.targeting.update(accountId, {
    competitor_accounts: suggestions.suggestions.add_competitors.map(
      (s) => s.username
    ),
    ai_optimization: {
      enabled: true,
      mode,
      quality_threshold: mode === "aggressive" ? 0.6 : 0.8,
    },
  });

  revalidatePath(`/campaigns/${accountId}`);
}

Using Server Actions in a form

// app/campaigns/new/page.tsx
import { createGrowthCampaign } from "../actions";

export default function NewCampaignPage() {
  return (
    <form action={createGrowthCampaign} className="space-y-6">
      <div>
        <label htmlFor="username">Instagram Username</label>
        <input
          id="username"
          name="username"
          placeholder="@yourbrand"
          required
        />
      </div>

      <div>
        <label htmlFor="competitors">Competitor Accounts</label>
        <input
          id="competitors"
          name="competitors"
          placeholder="competitor1, competitor2, competitor3"
        />
        <p className="text-sm text-gray-500">
          Followers of these accounts will be targeted.
        </p>
      </div>

      <div>
        <label htmlFor="hashtags">Target Hashtags</label>
        <input
          id="hashtags"
          name="hashtags"
          placeholder="fitness, wellness, health"
        />
      </div>

      <button type="submit">
        Create Campaign
      </button>
    </form>
  );
}

Real-time analytics

The useOutfameAnalytics() hook polls for live data and handles optimistic updates.

"use client";

import {
  useOutfameAnalytics,
  useOutfameAccount,
  FollowerGrowthChart,
  EngagementStats,
} from "@outfame/nextjs";

export default function GrowthDashboard({
  accountId,
}: {
  accountId: string;
}) {
  const { data: account, isLoading } = useOutfameAccount(accountId);
  const {
    data: analytics,
    isLoading: analyticsLoading,
    timeRange,
    setTimeRange,
  } = useOutfameAnalytics(accountId, {
    period: "30d",
    granularity: "daily",
    refreshInterval: 30000, // Live updates every 30s
  });

  if (isLoading || analyticsLoading) {
    return <DashboardSkeleton />;
  }

  return (
    <div className="space-y-8">
      {/* Real-time follower count */}
      <div className="flex items-baseline gap-4">
        <h2 className="text-4xl font-bold">
          {account.followers_count.toLocaleString()}
        </h2>
        <span className="text-emerald-500 font-medium">
          +{analytics.net_growth.toLocaleString()} this month
        </span>
      </div>

      {/* Pre-built growth chart — renders follower gains over time */}
      <FollowerGrowthChart
        data={analytics.data_points}
        height={320}
        showTrend
        showAnnotations
        annotations={analytics.milestones}
      />

      {/* Engagement breakdown */}
      <EngagementStats
        stats={analytics.engagement}
        showConversionRates
      />

      {/* Time range selector */}
      <div className="flex gap-2">
        {(["7d", "30d", "90d", "1y"] as const).map((range) => (
          <button
            key={range}
            onClick={() => setTimeRange(range)}
            className={timeRange === range ? "bg-pink-500 text-white" : ""}
          >
            {range}
          </button>
        ))}
      </div>

      {/* Quality breakdown */}
      <div className="rounded-xl border p-6">
        <h3>Audience Quality</h3>
        <p className="text-gray-500">
          Follower quality analysis
        </p>
        <div className="mt-4 grid grid-cols-3 gap-4">
          <div>
            <p className="text-2xl font-bold text-emerald-500">
              {analytics.audience_quality.real_percentage}%
            </p>
            <p className="text-sm text-gray-500">Real followers</p>
          </div>
          <div>
            <p className="text-2xl font-bold">
              {analytics.audience_quality.engagement_rate}%
            </p>
            <p className="text-sm text-gray-500">Engagement rate</p>
          </div>
          <div>
            <p className="text-2xl font-bold">
              {analytics.audience_quality.growth_velocity}
            </p>
            <p className="text-sm text-gray-500">Daily growth avg</p>
          </div>
        </div>
      </div>
    </div>
  );
}

function DashboardSkeleton() {
  return (
    <div className="space-y-8 animate-pulse">
      <div className="h-10 w-48 rounded bg-gray-200" />
      <div className="h-80 rounded-xl bg-gray-200" />
      <div className="h-32 rounded-xl bg-gray-200" />
    </div>
  );
}

App Router vs Pages Router

Both routing systems work. App Router is recommended for new projects since it enables Server Components and Server Actions.

FeatureApp RouterPages Router
Server ComponentsFull supportNot available
Server ActionsFull supportNot available
API Routesapp/api/.../route.tspages/api/...ts
Client HooksFull supportFull support
Webhook Handlerapp/api/outfame/webhook/route.tspages/api/outfame/webhook.ts
ProviderRoot layout.tsx_app.tsx
SSR Data Fetchingasync Server ComponentsgetServerSideProps
ISR / StaticBuilt-in with revalidategetStaticProps + revalidate

Pages Router example

// pages/_app.tsx
import { OutfameProvider } from "@outfame/nextjs";
import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <OutfameProvider apiKey={process.env.NEXT_PUBLIC_OUTFAME_KEY!}>
      <Component {...pageProps} />
    </OutfameProvider>
  );
}

// pages/dashboard.tsx
import { outfame } from "@outfame/nextjs/server";
import type { GetServerSideProps } from "next";
import type { AnalyticsOverview } from "@outfame/nextjs";

type Props = { analytics: AnalyticsOverview };

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  const analytics = await outfame.analytics.overview("acc_7Gx2kLm9Qr", {
    period: "30d",
  });
  return { props: { analytics } };
};

export default function Dashboard({ analytics }: Props) {
  return (
    <div>
      <h1>Instagram Growth Dashboard</h1>
      <p>+{analytics.net_growth} followers this month</p>
    </div>
  );
}

SEO benefits

Server-rendered follower counts and engagement metrics give search engines structured signals about your page content.

Dynamic metadata

// app/creators/[username]/page.tsx
import { outfame } from "@outfame/nextjs/server";
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { username: string };
}): Promise<Metadata> {
  const account = await outfame.accounts.retrieve(params.username);
  const analytics = await outfame.analytics.overview(account.id, {
    period: "30d",
  });

  return {
    title: `@${account.instagram_username} — ${account.followers_count.toLocaleString()} Followers`,
    description: `${account.instagram_username} grew +${analytics.net_growth} followers this month. View engagement analytics and growth stats.`,
    openGraph: {
      title: `@${account.instagram_username} Instagram Growth`,
      description: `+${analytics.net_growth} followers in 30 days`,
    },
  };
}

Structured data

// app/creators/[username]/page.tsx
import { outfame } from "@outfame/nextjs/server";

export default async function CreatorPage({
  params,
}: {
  params: { username: string };
}) {
  const account = await outfame.accounts.retrieve(params.username);

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "ProfilePage",
    mainEntity: {
      "@type": "Person",
      name: account.display_name,
      identifier: account.instagram_username,
      interactionStatistic: {
        "@type": "InteractionCounter",
        interactionType: "https://schema.org/FollowAction",
        userInteractionCount: account.followers_count,
      },
    },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      {/* Page content */}
    </>
  );
}

Middleware

Gate pages behind an active account or redirect based on campaign status.

// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { outfame } from "@outfame/nextjs/server";

export async function middleware(request: NextRequest) {
  const accountId = request.cookies.get("outfame_account_id")?.value;

  if (!accountId) {
    return NextResponse.redirect(new URL("/setup", request.url));
  }

  try {
    const account = await outfame.accounts.retrieve(accountId);

    if (account.status === "paused") {
      return NextResponse.redirect(new URL("/reactivate", request.url));
    }
  } catch {
    return NextResponse.redirect(new URL("/setup", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/analytics/:path*"],
};

Error handling

// app/dashboard/error.tsx
"use client";

import { useEffect } from "react";
import { OutfameError, RateLimitError } from "@outfame/nextjs";

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log to your error tracking service
    console.error("Dashboard error:", error);
  }, [error]);

  if (error instanceof RateLimitError) {
    return (
      <div>
        <h2>Too many requests</h2>
        <p>Please wait a moment and try again.</p>
        <button onClick={reset}>Retry</button>
      </div>
    );
  }

  return (
    <div>
      <h2>Something went wrong</h2>
      <p>Failed to load analytics.</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Configuration

OptionTypeDefaultDescription
apiKeystringYour Outfame API key. Read from OUTFAME_API_KEY if not provided.
config.revalidateOnFocusbooleantrueRefetch growth data when the window regains focus.
config.refreshIntervalnumber0Polling interval in milliseconds for live analytics updates.
config.dedupingIntervalnumber2000Deduplication window for identical requests (ms).
config.errorRetryCountnumber3Number of retries on transient API errors.

Next stepsAnalytics API reference | Growth guide