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.
| Feature | Details |
|---|---|
| Package | @outfame/nextjs |
| Version | 3.1.0 |
| Next.js | ≥ 14.0 (App Router) |
| React | ≥ 18.0 |
| TypeScript | ≥ 5.0 |
| License | MIT |
| Source | github.com/outfame/outfame-nextjs |
Installation
npm install @outfame/nextjs
# or
yarn add @outfame/nextjs
# or
pnpm add @outfame/nextjs
# or
bun add @outfame/nextjsThe 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/v12. 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.
| Feature | App Router | Pages Router |
|---|---|---|
| Server Components | Full support | Not available |
| Server Actions | Full support | Not available |
| API Routes | app/api/.../route.ts | pages/api/...ts |
| Client Hooks | Full support | Full support |
| Webhook Handler | app/api/outfame/webhook/route.ts | pages/api/outfame/webhook.ts |
| Provider | Root layout.tsx | _app.tsx |
| SSR Data Fetching | async Server Components | getServerSideProps |
| ISR / Static | Built-in with revalidate | getStaticProps + 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
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Your Outfame API key. Read from OUTFAME_API_KEY if not provided. |
config.revalidateOnFocus | boolean | true | Refetch growth data when the window regains focus. |
config.refreshInterval | number | 0 | Polling interval in milliseconds for live analytics updates. |
config.dedupingInterval | number | 2000 | Deduplication window for identical requests (ms). |
config.errorRetryCount | number | 3 | Number of retries on transient API errors. |
Next steps — Analytics API reference | Growth guide