SDKs & Integrations
Framer Integration
Display live follower data on your Framer site using code components, property controls, or the lightweight embed script. Framer runs React, so you have direct access to the @outfame/sdk package via ESM imports.
| Feature | Details |
|---|---|
| Package | @outfame/sdk (via ESM import) |
| Version | 2.4.1 |
| Framer plan | Any plan with custom code |
| React | ≥ 18.0 (Framer built-in) |
| License | MIT |
Code components
Code components appear in Framer’s insert panel. Build once, reuse across pages.
Follower counter component
Create a new code component in Framer (Assets → Code → New Component) and paste the following:
import { useState, useEffect } from "react"
import { addPropertyControls, ControlType } from "framer"
const OUTFAME_API_URL = "https://api.outfame.com/v1"
interface Props {
apiKey: string
accountId: string
format: "compact" | "full"
showGrowth: boolean
period: "24h" | "7d" | "30d"
style?: React.CSSProperties
}
/**
* Instagram Follower Counter — powered by Outfame
*
* Displays real-time follower count and growth for any Instagram account
* managed through the Outfame growth service.
*
* @framerSupportedLayoutWidth any
* @framerSupportedLayoutHeight any
*/
export default function OutfameFollowerCounter({
apiKey,
accountId,
format = "compact",
showGrowth = true,
period = "7d",
style,
}: Props) {
const [data, setData] = useState<{
followers: number
growth: number
} | null>(null)
useEffect(() => {
if (!apiKey || !accountId) return
fetch(`${OUTFAME_API_URL}/analytics/overview?account_id=${accountId}&period=${period}`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
.then((res) => res.json())
.then((res) => {
setData({
followers: res.data.followers_count,
growth: res.data.net_growth,
})
})
.catch(console.error)
}, [apiKey, accountId, period])
if (!data) {
return (
<div style={{ ...containerStyle, ...style }}>
<div style={skeletonStyle} />
</div>
)
}
const formattedCount =
format === "compact"
? data.followers >= 1000
? `${(data.followers / 1000).toFixed(1)}K`
: data.followers.toString()
: data.followers.toLocaleString()
return (
<div style={{ ...containerStyle, ...style }}>
<span style={countStyle}>{formattedCount}</span>
<span style={labelStyle}>Instagram Followers</span>
{showGrowth && data.growth > 0 && (
<span style={growthStyle}>+{data.growth.toLocaleString()} this {period === "24h" ? "day" : period === "7d" ? "week" : "month"}</span>
)}
</div>
)
}
const containerStyle: React.CSSProperties = {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 4,
padding: 16,
fontFamily: "Inter, system-ui, sans-serif",
}
const countStyle: React.CSSProperties = {
fontSize: 40,
fontWeight: 700,
letterSpacing: "-0.02em",
color: "#111",
}
const labelStyle: React.CSSProperties = {
fontSize: 14,
color: "#6b7280",
fontWeight: 500,
}
const growthStyle: React.CSSProperties = {
fontSize: 13,
fontWeight: 600,
color: "#16a34a",
background: "#dcfce7",
borderRadius: 999,
padding: "2px 10px",
marginTop: 4,
}
const skeletonStyle: React.CSSProperties = {
width: 120,
height: 48,
borderRadius: 12,
background: "#f3f4f6",
}
addPropertyControls(OutfameFollowerCounter, {
apiKey: {
type: ControlType.String,
title: "API Key",
placeholder: "pk_live_xxxxxxxxxxxx",
description: "Your Outfame publishable API key",
},
accountId: {
type: ControlType.String,
title: "Account ID",
placeholder: "acc_7Gx2kLm9Qr",
description: "Outfame Instagram account ID",
},
format: {
type: ControlType.Enum,
title: "Format",
options: ["compact", "full"],
optionTitles: ["Compact (12.4K)", "Full (12,432)"],
defaultValue: "compact",
},
showGrowth: {
type: ControlType.Boolean,
title: "Show Growth",
defaultValue: true,
},
period: {
type: ControlType.Enum,
title: "Period",
options: ["24h", "7d", "30d"],
optionTitles: ["24 Hours", "7 Days", "30 Days"],
defaultValue: "7d",
},
})Custom code setup
Lighter alternative to code components. Add the embed script in Site Settings → Custom Code → Head:
<!-- Outfame Instagram Growth — Framer Site Custom Code (Head) -->
<script
src="https://cdn.outfame.com/embed/v1/outfame.min.js"
data-api-key="pk_live_xxxxxxxxxxxx"
data-account-id="acc_7Gx2kLm9Qr"
defer
></script>Then use Framer’s HTML Embed element to place widgets anywhere on the canvas:
<div
data-outfame="follower-counter"
data-format="compact"
data-show-growth="true"
data-period="7d"
></div>React components
Framer uses React, so you can build analytics components with hooks. Here’s a growth chart that fetches data and renders an SVG sparkline:
import { useState, useEffect, useRef } from "react"
import { addPropertyControls, ControlType } from "framer"
interface GrowthPoint {
date: string
followers: number
net: number
}
interface Props {
apiKey: string
accountId: string
period: "7d" | "30d" | "90d"
color: string
height: number
}
/**
* Instagram Growth Chart — powered by Outfame
*
* Renders an SVG sparkline of follower growth over time.
* Connect to any Outfame-managed account.
*
* @framerSupportedLayoutWidth any
* @framerSupportedLayoutHeight any
*/
export default function OutfameGrowthChart({
apiKey,
accountId,
period = "30d",
color = "#e91e8c",
height = 200,
}: Props) {
const [points, setPoints] = useState<GrowthPoint[]>([])
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
if (!apiKey || !accountId) return
fetch(
`https://api.outfame.com/v1/analytics/growth?account_id=${accountId}&period=${period}&granularity=daily`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
)
.then((res) => res.json())
.then((res) => setPoints(res.data.data_points))
.catch(console.error)
}, [apiKey, accountId, period])
if (points.length === 0) {
return (
<div style={{ height, background: "#f9fafb", borderRadius: 12 }} />
)
}
const width = 400
const padding = 8
const maxFollowers = Math.max(...points.map((p) => p.followers))
const minFollowers = Math.min(...points.map((p) => p.followers))
const range = maxFollowers - minFollowers || 1
const pathData = points
.map((point, i) => {
const x = padding + (i / (points.length - 1)) * (width - padding * 2)
const y = padding + (1 - (point.followers - minFollowers) / range) * (height - padding * 2)
return `${i === 0 ? "M" : "L"}${x},${y}`
})
.join(" ")
const totalGrowth = points[points.length - 1].followers - points[0].followers
return (
<div style={{ position: "relative" }}>
<svg
ref={svgRef}
viewBox={`0 0 ${width} ${height}`}
style={{ width: "100%", height }}
>
<defs>
<linearGradient id="outfame-gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.15} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
</defs>
<path
d={`${pathData} L${width - padding},${height - padding} L${padding},${height - padding} Z`}
fill="url(#outfame-gradient)"
/>
<path d={pathData} fill="none" stroke={color} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div style={{
position: "absolute",
top: 12,
right: 12,
background: totalGrowth > 0 ? "#dcfce7" : "#fef2f2",
color: totalGrowth > 0 ? "#16a34a" : "#dc2626",
borderRadius: 999,
padding: "4px 12px",
fontSize: 13,
fontWeight: 600,
fontFamily: "Inter, system-ui, sans-serif",
}}>
{totalGrowth > 0 ? "+" : ""}{totalGrowth.toLocaleString()} followers
</div>
</div>
)
}
addPropertyControls(OutfameGrowthChart, {
apiKey: {
type: ControlType.String,
title: "API Key",
placeholder: "pk_live_xxxxxxxxxxxx",
},
accountId: {
type: ControlType.String,
title: "Account ID",
placeholder: "acc_7Gx2kLm9Qr",
},
period: {
type: ControlType.Enum,
title: "Period",
options: ["7d", "30d", "90d"],
optionTitles: ["7 Days", "30 Days", "90 Days"],
defaultValue: "30d",
},
color: {
type: ControlType.Color,
title: "Chart Color",
defaultValue: "#e91e8c",
},
height: {
type: ControlType.Number,
title: "Height",
defaultValue: 200,
min: 100,
max: 500,
step: 10,
},
})Property controls
Property controls let designers configure components visually. Expose these core controls on every Outfame component:
| Control | Type | Description |
|---|---|---|
apiKey | ControlType.String | Publishable API key. Store in Framer environment variables for security. |
accountId | ControlType.String | The Outfame Instagram account ID to display data for. |
period | ControlType.Enum | Time range for growth and engagement data. |
theme | ControlType.Enum | light or dark to match the page design. |
Advanced: conditional controls
Use Framer’s hidden function to show or hide controls based on other property values. This keeps the properties panel clean:
addPropertyControls(OutfameWidget, {
apiKey: {
type: ControlType.String,
title: "API Key",
},
accountId: {
type: ControlType.String,
title: "Account ID",
},
showGrowth: {
type: ControlType.Boolean,
title: "Show Growth Badge",
defaultValue: true,
},
period: {
type: ControlType.Enum,
title: "Growth Period",
options: ["24h", "7d", "30d"],
optionTitles: ["24 Hours", "7 Days", "30 Days"],
defaultValue: "7d",
// Only show when growth badge is enabled
hidden: (props) => !props.showGrowth,
},
showEngagement: {
type: ControlType.Boolean,
title: "Show Engagement Rate",
defaultValue: false,
},
engagementLabel: {
type: ControlType.String,
title: "Engagement Label",
defaultValue: "Engagement Rate",
hidden: (props) => !props.showEngagement,
},
})Overrides
Inject live data into existing text layers, shapes, or images without a custom component.
import type { ComponentType } from "react"
import { useState, useEffect } from "react"
const OUTFAME_API_KEY = "pk_live_xxxxxxxxxxxx"
const OUTFAME_ACCOUNT_ID = "acc_7Gx2kLm9Qr"
// Apply to any text layer to show the live follower count
export function withFollowerCount(Component: ComponentType): ComponentType {
return (props: any) => {
const [count, setCount] = useState<string>("...")
useEffect(() => {
fetch(
`https://api.outfame.com/v1/analytics/overview?account_id=${OUTFAME_ACCOUNT_ID}&period=7d`,
{ headers: { Authorization: `Bearer ${OUTFAME_API_KEY}` } }
)
.then((res) => res.json())
.then((res) => {
const followers = res.data.followers_count
setCount(followers >= 1000 ? `${(followers / 1000).toFixed(1)}K` : followers.toString())
})
.catch(() => setCount("—"))
}, [])
return <Component {...props} text={count} />
}
}
// Apply to any text layer to show weekly growth
export function withWeeklyGrowth(Component: ComponentType): ComponentType {
return (props: any) => {
const [growth, setGrowth] = useState<string>("...")
useEffect(() => {
fetch(
`https://api.outfame.com/v1/analytics/overview?account_id=${OUTFAME_ACCOUNT_ID}&period=7d`,
{ headers: { Authorization: `Bearer ${OUTFAME_API_KEY}` } }
)
.then((res) => res.json())
.then((res) => {
const net = res.data.net_growth
setGrowth(net > 0 ? `+${net.toLocaleString()}` : net.toLocaleString())
})
.catch(() => setGrowth("—"))
}, [])
return <Component {...props} text={growth} />
}
}
// Apply to a progress bar or shape to reflect engagement rate
export function withEngagementWidth(Component: ComponentType): ComponentType {
return (props: any) => {
const [width, setWidth] = useState("0%")
useEffect(() => {
fetch(
`https://api.outfame.com/v1/analytics/overview?account_id=${OUTFAME_ACCOUNT_ID}&period=30d`,
{ headers: { Authorization: `Bearer ${OUTFAME_API_KEY}` } }
)
.then((res) => res.json())
.then((res) => {
// Engagement rate typically 1-10%, scale to percentage width
const rate = Math.min(res.data.engagement_rate * 10, 100)
setWidth(`${rate}%`)
})
.catch(() => setWidth("0%"))
}, [])
return <Component {...props} style={{ ...props.style, width }} />
}
}Select a text layer, open the properties panel, and attach the override (e.g. withFollowerCount). The text content updates with live data at runtime.
CMS integration
Sync follower data into Framer CMS collections for dynamic pages — handy for agencies with multiple client accounts or public analytics dashboards.
1. Create a CMS collection
In Framer, go to CMS → New Collection and create an Instagram Accounts collection with these fields:
| Field | Type | Maps to |
|---|---|---|
| Handle | Text | instagram_username |
| Followers | Number | followers_count |
| Growth (7d) | Number | net_growth_7d |
| Engagement Rate | Text | engagement_rate |
| Avatar URL | Link | profile_picture_url |
2. Sync via Framer API
// Sync Outfame growth data → Framer CMS
// Run this as a daily cron job or trigger via Outfame webhooks
const OUTFAME_API_KEY = process.env.OUTFAME_API_KEY;
const FRAMER_API_TOKEN = process.env.FRAMER_API_TOKEN;
const FRAMER_COLLECTION_ID = "your_collection_id";
async function syncToFramerCMS() {
// Fetch all accounts from Outfame
const accounts = await fetch("https://api.outfame.com/v1/accounts?platform=instagram&status=active", {
headers: { Authorization: `Bearer ${OUTFAME_API_KEY}` },
}).then((res) => res.json());
for (const account of accounts.data) {
// Fetch analytics for each Instagram account
const analytics = await fetch(
`https://api.outfame.com/v1/analytics/overview?account_id=${account.id}&period=7d`,
{ headers: { Authorization: `Bearer ${OUTFAME_API_KEY}` } }
).then((res) => res.json());
// Upsert to Framer CMS
await fetch(`https://api.framer.com/v1/collections/${FRAMER_COLLECTION_ID}/items`, {
method: "POST",
headers: {
Authorization: `Bearer ${FRAMER_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
slug: account.instagram_username,
fieldData: {
handle: account.instagram_username,
followers: analytics.data.followers_count,
"growth-7d": analytics.data.net_growth,
"engagement-rate": `${analytics.data.engagement_rate}%`,
"avatar-url": analytics.data.profile_picture_url,
},
}),
});
}
}
syncToFramerCMS();API fetch in Framer
For full control over rendering, fetch directly from the API inside code components instead of using the embed script.
Custom hook
import { useState, useEffect } from "react"
interface OutfameAnalytics {
followers_count: number
net_growth: number
engagement_rate: number
growth_rate: number
actions: {
likes: { performed: number; limit: number }
follows: { performed: number; limit: number }
}
}
/**
* Fetch real-time Instagram analytics from the Outfame growth API.
* Use inside any Framer code component to display live follower data.
*/
export function useOutfameAnalytics(
apiKey: string,
accountId: string,
period: "24h" | "7d" | "30d" = "7d"
) {
const [data, setData] = useState<OutfameAnalytics | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!apiKey || !accountId) {
setLoading(false)
return
}
let cancelled = false
async function fetchData() {
try {
const response = await fetch(
`https://api.outfame.com/v1/analytics/overview?account_id=${accountId}&period=${period}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
}
)
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
const result = await response.json()
if (!cancelled) {
setData(result.data)
setLoading(false)
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : "Failed to fetch")
setLoading(false)
}
}
}
fetchData()
// Refresh every 60 seconds for real-time data
const interval = setInterval(fetchData, 60_000)
return () => {
cancelled = true
clearInterval(interval)
}
}, [apiKey, accountId, period])
return { data, loading, error }
}Using the hook in a component
import { addPropertyControls, ControlType } from "framer"
import { useOutfameAnalytics } from "./useOutfameAnalytics"
/**
* Analytics Dashboard — powered by Outfame
*
* Follower count, growth rate, and engagement metrics in a single card.
*
* @framerSupportedLayoutWidth any
* @framerSupportedLayoutHeight any
*/
export default function OutfameAnalyticsDashboard({
apiKey,
accountId,
period = "7d",
theme = "light",
}: {
apiKey: string
accountId: string
period: "24h" | "7d" | "30d"
theme: "light" | "dark"
}) {
const { data, loading, error } = useOutfameAnalytics(apiKey, accountId, period)
const isDark = theme === "dark"
const bg = isDark ? "#141414" : "#ffffff"
const text = isDark ? "#ffffff" : "#111827"
const muted = isDark ? "rgba(255,255,255,0.5)" : "#6b7280"
const border = isDark ? "rgba(255,255,255,0.1)" : "#f3f4f6"
if (loading) {
return (
<div style={{ ...cardStyle, background: bg }}>
<div style={{ ...skeletonStyle, background: border }} />
<div style={{ ...skeletonStyle, width: "60%", background: border }} />
</div>
)
}
if (error || !data) {
return (
<div style={{ ...cardStyle, background: bg, color: muted }}>
<p>Unable to load Instagram analytics</p>
</div>
)
}
return (
<div style={{ ...cardStyle, background: bg }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
<div>
<p style={{ margin: 0, fontSize: 13, color: muted, fontWeight: 500 }}>
Instagram Followers
</p>
<p style={{ margin: "4px 0 0", fontSize: 36, fontWeight: 700, color: text, letterSpacing: "-0.02em" }}>
{data.followers_count.toLocaleString()}
</p>
</div>
<div style={{
background: data.net_growth > 0 ? "#dcfce7" : "#fef2f2",
color: data.net_growth > 0 ? "#16a34a" : "#dc2626",
borderRadius: 999,
padding: "4px 12px",
fontSize: 13,
fontWeight: 600,
}}>
{data.net_growth > 0 ? "+" : ""}{data.net_growth.toLocaleString()}
</div>
</div>
<div style={{ display: "flex", gap: 24, marginTop: 20, borderTop: `1px solid ${border}`, paddingTop: 16 }}>
<div>
<p style={{ margin: 0, fontSize: 12, color: muted }}>Engagement Rate</p>
<p style={{ margin: "2px 0 0", fontSize: 18, fontWeight: 600, color: text }}>{data.engagement_rate}%</p>
</div>
<div>
<p style={{ margin: 0, fontSize: 12, color: muted }}>Growth Rate</p>
<p style={{ margin: "2px 0 0", fontSize: 18, fontWeight: 600, color: text }}>{data.growth_rate}%</p>
</div>
<div>
<p style={{ margin: 0, fontSize: 12, color: muted }}>Likes Today</p>
<p style={{ margin: "2px 0 0", fontSize: 18, fontWeight: 600, color: text }}>{data.actions.likes.performed}</p>
</div>
</div>
</div>
)
}
const cardStyle: React.CSSProperties = {
borderRadius: 16,
padding: 24,
fontFamily: "Inter, system-ui, sans-serif",
boxShadow: "0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04)",
}
const skeletonStyle: React.CSSProperties = {
height: 20,
borderRadius: 8,
marginBottom: 12,
}
addPropertyControls(OutfameAnalyticsDashboard, {
apiKey: {
type: ControlType.String,
title: "API Key",
placeholder: "pk_live_xxxxxxxxxxxx",
},
accountId: {
type: ControlType.String,
title: "Account ID",
placeholder: "acc_7Gx2kLm9Qr",
},
period: {
type: ControlType.Enum,
title: "Period",
options: ["24h", "7d", "30d"],
optionTitles: ["24 Hours", "7 Days", "30 Days"],
defaultValue: "7d",
},
theme: {
type: ControlType.Enum,
title: "Theme",
options: ["light", "dark"],
optionTitles: ["Light", "Dark"],
defaultValue: "light",
},
})Troubleshooting
| Issue | Solution |
|---|---|
| Component shows loading skeleton indefinitely | Verify apiKey is a publishable key starting with pk_ and the account ID exists in your Outfame dashboard. |
| CORS error in Framer preview | Add your Framer preview domain (*.framer.app) to allowed origins in Outfame Settings → API Keys. |
| Property controls not appearing | Ensure addPropertyControls is called after the component definition, not inside it. |
| Data stale after publish | Components fetch fresh data on every page load. If using CMS, ensure your sync cron job is running. |
| Override not replacing text | Make sure the override is applied to a text layer, not a frame. The text prop only works on text elements. |
| Rate limit errors (429) | Publishable keys have a 100 req/min limit per domain. Add caching or reduce the refresh interval from 60s to 300s. |