<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kollab Kit</title>
    <description>The latest articles on DEV Community by Kollab Kit (@kollab_kit).</description>
    <link>https://dev.to/kollab_kit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4021581%2F25e4c94e-5d4b-49e9-a075-d183169dec4c.jpeg</url>
      <title>DEV Community: Kollab Kit</title>
      <link>https://dev.to/kollab_kit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kollab_kit"/>
    <language>en</language>
    <item>
      <title>How We Built a Live Media Kit Generator That Pulls Real Instagram Data in Under 30 Seconds</title>
      <dc:creator>Kollab Kit</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:54:24 +0000</pubDate>
      <link>https://dev.to/kollab_kit/how-we-built-a-live-media-kit-generator-that-pulls-real-instagram-data-in-under-30-seconds-4fhl</link>
      <guid>https://dev.to/kollab_kit/how-we-built-a-live-media-kit-generator-that-pulls-real-instagram-data-in-under-30-seconds-4fhl</guid>
      <description>&lt;p&gt;Most creator tools are just fancy form builders with a PDF export button.&lt;/p&gt;

&lt;p&gt;You fill in your follower count manually. Upload a screenshot of your analytics. Pick a Canva template. Export a PDF. Send it to a brand. Three weeks later your numbers have changed and the PDF is already wrong.&lt;/p&gt;

&lt;p&gt;We wanted to build something different with &lt;a href="https://kollabkit.com" rel="noopener noreferrer"&gt;KollabKit&lt;/a&gt; — a live, API-connected &lt;a href="https://kollabkit.com/media-kit-builder/" rel="noopener noreferrer"&gt;media kit&lt;/a&gt; that generates in under 30 seconds. Here's how we built it.&lt;/p&gt;

&lt;h5&gt;
  
  
  Table of Contents
&lt;/h5&gt;

&lt;p&gt;The Problem&lt;br&gt;
Step 1: Instagram Graph API&lt;br&gt;
Step 2: The OAuth Flow&lt;br&gt;
Step 3: Pulling the Metrics&lt;br&gt;
Step 4: Computing Engagement Rate&lt;br&gt;
Step 5: The Live URL Architecture&lt;br&gt;
Step 6: Rate Card Calculation&lt;br&gt;
What We Learned&lt;br&gt;
What's Next&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;A media kit is basically a snapshot of a creator's performance at a point in time. The problem with PDFs is that the snapshot goes stale the moment you export it.&lt;/p&gt;

&lt;p&gt;What we needed wasn't a document generator. It was a live data layer sitting between Instagram's API and a shareable public URL, refreshing whenever someone opens the link.&lt;/p&gt;

&lt;p&gt;The architecture had to handle three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OAuth + API connection&lt;/strong&gt; - connect to Instagram's official Graph API without storing credentials insecurely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data normalisation&lt;/strong&gt; - take raw API responses and turn them into readable metrics like engagement rate, audience split, and top cities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live rendering&lt;/strong&gt; - serve a public URL that always shows current data, not a cached snapshot&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Step 1: Instagram Graph API
&lt;/h2&gt;

&lt;p&gt;This is where most builders run into trouble. Instagram's API has two tiers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Display API&lt;/strong&gt; - deprecated as of late 2024. Don't build on this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instagram Graph API (via &lt;a href="https://developers.facebook.com/" rel="noopener noreferrer"&gt;Meta for Developers&lt;/a&gt;)&lt;/strong&gt; - the current option. Requires the creator to have a Professional account (Creator or Business) connected to a Facebook Page.&lt;/p&gt;

&lt;p&gt;The permissions you need for a media kit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;instagram_basic
instagram_manage_insights
pages_show_list
pages_read_engagement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;instagram_manage_insights&lt;/code&gt; is the important one. It gives you account-level metrics like impressions, reach, follower demographics, and top locations.&lt;/p&gt;

&lt;p&gt;Getting these approved by Meta means submitting your app for review with a screen recording showing exactly how you use each permission. Set aside &lt;strong&gt;2 to 4 weeks&lt;/strong&gt; for this. It's annoying but there's no way around it if you want verified data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: The OAuth Flow
&lt;/h2&gt;

&lt;p&gt;We went with a standard OAuth 2.0 flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 1: Redirect user to Meta's OAuth dialog&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://www.facebook.com/v19.0/dialog/oauth?
  client_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_APP_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  &amp;amp;redirect_uri=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
  &amp;amp;scope=instagram_basic,instagram_manage_insights,pages_show_list
  &amp;amp;response_type=code`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2: Exchange code for short-lived token&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://graph.facebook.com/v19.0/oauth/access_token?
    client_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_APP_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    &amp;amp;client_secret=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    &amp;amp;redirect_uri=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
    &amp;amp;code=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;authCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 3: Exchange for long-lived token (60-day expiry)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;longLivedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://graph.facebook.com/v19.0/oauth/access_token?
    grant_type=fb_exchange_token
    &amp;amp;client_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_APP_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    &amp;amp;client_secret=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    &amp;amp;fb_exchange_token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shortLivedToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long-lived tokens expire after 60 days. We handle the refresh server-side before expiry so the creator never has to reconnect unless they revoke access themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Pulling the Metrics
&lt;/h2&gt;

&lt;p&gt;Once you have a valid token, pulling the data is fairly simple. The tricky bit is knowing which endpoints to chain together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get connected Instagram Business Account ID&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;igAccountRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://graph.facebook.com/v19.0/me/accounts?
    fields=instagram_business_account&amp;amp;access_token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;igAccountId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;igAccountRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;instagram_business_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Pull core profile metrics&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profileRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://graph.facebook.com/v19.0/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;igAccountId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?
    fields=username,name,biography,followers_count,
    media_count,profile_picture_url,website&amp;amp;
    access_token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Pull insights - engagement, reach, impressions&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insightsRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://graph.facebook.com/v19.0/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;igAccountId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/insights?
    metric=impressions,reach,follower_count&amp;amp;
    period=day&amp;amp;
    since=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;thirtyDaysAgo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;
    until=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;
    access_token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Pull audience demographics&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audienceRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://graph.facebook.com/v19.0/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;igAccountId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/insights?
    metric=audience_city,audience_country,
    audience_gender_age&amp;amp;
    period=lifetime&amp;amp;
    access_token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; &lt;code&gt;audience_city&lt;/code&gt;, &lt;code&gt;audience_country&lt;/code&gt;, and &lt;code&gt;audience_gender_age&lt;/code&gt; only work with &lt;code&gt;period=lifetime&lt;/code&gt;. They don't support date ranges. You get the current distribution as of whenever you make the call.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4: Computing Engagement Rate
&lt;/h2&gt;

&lt;p&gt;Instagram's API doesn't give you engagement rate directly. You have to compute it from the raw data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pull recent media for engagement calculation&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mediaRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://graph.facebook.com/v19.0/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;igAccountId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/media?
    fields=like_count,comments_count,timestamp&amp;amp;
    limit=30&amp;amp;access_token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mediaRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalEngagements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;like_count&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments_count&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avgEngagementsPerPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalEngagements&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engagementRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;avgEngagementsPerPost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;followersCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Round to 2 decimal places&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;displayEngagementRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engagementRate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We sample the last 30 posts. Fewer posts makes the rate jump around too much. More posts pulls in older content that doesn't reflect where the account is today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: The Live URL Architecture
&lt;/h2&gt;

&lt;p&gt;We didn't want to cache the media kit as a static page. We wanted it to show current data every time a brand opens the link. But we also couldn't hit Instagram's API fresh on every page load — rate limits and latency would kill that fast.&lt;/p&gt;

&lt;p&gt;So we went with a short-TTL cache layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMediaKitData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;creatorId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`mediakit:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;creatorId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Check cache first (TTL: 6 hours)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Cache miss - pull fresh from Instagram API&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;freshData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pullInstagramData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;creatorId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Store with 6-hour expiry&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;21600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;freshData&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;freshData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;6 hours works well here. Creator metrics don't shift meaningfully in that window, but brands always see data that's at most half a day old rather than a 3-month-old PDF. When a creator connects or manually refreshes, we invalidate the cache immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Rate Card Calculation
&lt;/h2&gt;

&lt;p&gt;The rate calculator is its own module. We don't share the exact formula publicly, but the general logic follows industry benchmarks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Follower tier&lt;/td&gt;
&lt;td&gt;Base rate for nano / micro / mid / macro / mega&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engagement rate&lt;/td&gt;
&lt;td&gt;Higher engagement = higher multiplier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Niche coefficient&lt;/td&gt;
&lt;td&gt;Finance and tech creators charge more than lifestyle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content format&lt;/td&gt;
&lt;td&gt;Reel, Story, Static Post, YouTube — each has its own rate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We started the ranges from aggregated industry data and keep refining them as more real deal data flows through the platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Meta's API review is the actual bottleneck.&lt;/strong&gt; The engineering wasn't the hard part. Waiting for permission approval and writing data usage policy documentation took longer than building the data pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follower count is a bad primary metric.&lt;/strong&gt; Once you work with real engagement data you see how misleading follower counts are. A creator with 20K followers and 8% engagement is worth more to a brand than one with 200K and 0.4%. We built the kit to lead with engagement, not followers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token refresh needs to be invisible.&lt;/strong&gt; If creators have to reconnect their Instagram every 60 days, churn goes up. Silent background refresh isn't a nice-to-have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URL format matters more than you'd think.&lt;/strong&gt; We tried slugs, hashes, numeric IDs. Short readable custom slugs like &lt;code&gt;kollabkit.com/kit/username&lt;/code&gt; got significantly better click-through from brand email chains than opaque hash URLs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;YouTube Data API&lt;/strong&gt; - same OAuth flow, different endpoints, adding video metrics alongside Instagram&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI caption suggestor&lt;/strong&gt; - trained on niche-specific content to help creators write brand-aligned captions faster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI video deliverable approver&lt;/strong&gt; - brands submit a brief, creators submit a video, our pipeline checks compliance before it hits a human reviewer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creator discovery&lt;/strong&gt; - search across verified creator profiles using real engagement data, not self-reported numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building in the creator economy and want to talk through the Instagram Graph API — especially the permission review process — drop a comment. Happy to share what worked and what didn't.&lt;/p&gt;

&lt;p&gt;And if you're a creator or work with them, the &lt;a href="https://kollabkit.com/media-kit-builder/" rel="noopener noreferrer"&gt;media kit&lt;/a&gt; is free to try.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;javascript&lt;/code&gt; &lt;code&gt;webdev&lt;/code&gt; &lt;code&gt;api&lt;/code&gt; &lt;code&gt;startup&lt;/code&gt; &lt;code&gt;instagram&lt;/code&gt; &lt;code&gt;buildinpublic&lt;/code&gt; &lt;code&gt;saas&lt;/code&gt; &lt;code&gt;productivity&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
