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.

FeatureDetails
Package@outfame/sdk (via ESM import)
Version2.4.1
Framer planAny plan with custom code
React≥ 18.0 (Framer built-in)
LicenseMIT

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:

ControlTypeDescription
apiKeyControlType.StringPublishable API key. Store in Framer environment variables for security.
accountIdControlType.StringThe Outfame Instagram account ID to display data for.
periodControlType.EnumTime range for growth and engagement data.
themeControlType.Enumlight 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:

FieldTypeMaps to
HandleTextinstagram_username
FollowersNumberfollowers_count
Growth (7d)Numbernet_growth_7d
Engagement RateTextengagement_rate
Avatar URLLinkprofile_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

IssueSolution
Component shows loading skeleton indefinitelyVerify apiKey is a publishable key starting with pk_ and the account ID exists in your Outfame dashboard.
CORS error in Framer previewAdd your Framer preview domain (*.framer.app) to allowed origins in Outfame Settings → API Keys.
Property controls not appearingEnsure addPropertyControls is called after the component definition, not inside it.
Data stale after publishComponents fetch fresh data on every page load. If using CMS, ensure your sync cron job is running.
Override not replacing textMake 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.