BigQuery integration

Instagram data in BigQuery

Pipe public Instagram data (profiles, follower counts, posts, reels, likes, comments) into BigQuery on a schedule, then answer questions in SQL. No Meta app, no connected accounts, no scraper to maintain: one API key and a short Python script.

50 free credits on signup · no card required

curl "https://api.profilequery.com/v1/profile?handle=nike" \
  -H "Authorization: Bearer $INSTAGRAMAPI_KEY"

What you get

Structured JSON, not raw HTML

  • ▹Track any public account, not just the ones you own: competitors, creators, brands
  • ▹Daily follower/following/post-count snapshots for growth curves
  • ▹Every post and reel with likes, comments, views, caption, and timestamp
  • ▹Flat JSON that maps 1:1 onto BigQuery columns, so there's nothing to parse
  • ▹Works from Cloud Run, Cloud Functions, Airflow/Composer, dbt, or a cron box
  • ▹Pay per request; a 50-account daily snapshot costs pennies

Step by step

Build the Instagram → BigQuery pipeline

1. Create the tables

Two tables cover most analysis: a daily snapshot of each profile (for growth), and one row per post (for engagement). Partitioning by date keeps queries cheap as history piles up.

CREATE SCHEMA IF NOT EXISTS instagram;

CREATE TABLE IF NOT EXISTS instagram.profile_snapshots (
  snapshot_date DATE NOT NULL,
  user_id STRING NOT NULL,
  username STRING,
  full_name STRING,
  followers INT64,
  following INT64,
  posts INT64,
  is_verified BOOL,
  category STRING
) PARTITION BY snapshot_date;

CREATE TABLE IF NOT EXISTS instagram.posts (
  fetched_at TIMESTAMP NOT NULL,
  post_id STRING NOT NULL,
  shortcode STRING,
  username STRING,
  type STRING,          -- photo | video | carousel
  product_type STRING,  -- feed | clips (reels)
  caption STRING,
  taken_at TIMESTAMP,
  like_count INT64,
  comment_count INT64,
  view_count INT64
) PARTITION BY DATE(fetched_at);

2. Load the data with Python

The script below snapshots each profile and pulls its most recent posts, then appends both to BigQuery. It uses the official google-cloud-bigquery client, so it authenticates with whatever service account the job runs as.

# pip install requests google-cloud-bigquery
import os, datetime, requests
from google.cloud import bigquery

API = "https://api.profilequery.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['INSTAGRAMAPI_KEY']}"}
ACCOUNTS = ["nike", "adidas", "puma"]

def get(path, **params):
    r = requests.get(f"{API}/{path}", headers=HEADERS, params=params, timeout=60)
    r.raise_for_status()
    return r.json()["data"]

now = datetime.datetime.now(datetime.timezone.utc)
profiles, posts = [], []

for handle in ACCOUNTS:
    p = get("profile", handle=handle)
    profiles.append({
        "snapshot_date": now.date().isoformat(),
        "user_id": p["id"], "username": p["username"], "full_name": p["full_name"],
        "followers": p["followers"], "following": p["following"], "posts": p["posts"],
        "is_verified": p["is_verified"], "category": p["category"],
    })
    for m in get("profile/posts", handle=handle)["items"]:
        posts.append({
            "fetched_at": now.isoformat(), "post_id": m["id"], "shortcode": m["shortcode"],
            "username": handle, "type": m["type"], "product_type": m["product_type"],
            "caption": m["caption"], "taken_at": m["taken_at"],
            "like_count": m["like_count"], "comment_count": m["comment_count"],
            "view_count": m["view_count"],
        })

bq = bigquery.Client()
append = bigquery.LoadJobConfig(write_disposition="WRITE_APPEND")
bq.load_table_from_json(profiles, "instagram.profile_snapshots", job_config=append).result()
bq.load_table_from_json(posts, "instagram.posts", job_config=append).result()
print(f"Loaded {len(profiles)} profiles, {len(posts)} posts")

Need more history than the latest page? Every list endpoint returns next_cursor. Pass it back as cursor= to keep paging until it comes back null. Swap profile/posts for profile/reels to load reels with view counts.

3. Run it daily

Package the script as a Cloud Run job and trigger it from Cloud Scheduler. Store the API key in Secret Manager and expose it as INSTAGRAMAPI_KEY. Any scheduler works (Airflow, GitHub Actions, plain cron); the script has no state of its own.

gcloud run jobs deploy instagram-to-bq --source . --region us-central1 \
  --set-secrets INSTAGRAMAPI_KEY=instagramapi-key:latest

gcloud scheduler jobs create http instagram-to-bq-daily \
  --location us-central1 --schedule "0 6 * * *" \
  --uri "https://run.googleapis.com/v2/projects/$PROJECT/locations/us-central1/jobs/instagram-to-bq:run" \
  --http-method POST --oauth-service-account-email "$SA_EMAIL"

4. Query it

Because posts are re-fetched daily, keep the latest row per post before aggregating. Engagement rate per account over the last 30 days:

WITH latest_posts AS (
  SELECT * FROM instagram.posts
  WHERE taken_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
  QUALIFY ROW_NUMBER() OVER (PARTITION BY post_id ORDER BY fetched_at DESC) = 1
),
followers AS (
  SELECT username, followers FROM instagram.profile_snapshots
  WHERE snapshot_date = (SELECT MAX(snapshot_date) FROM instagram.profile_snapshots)
)
SELECT
  p.username,
  COUNT(*) AS posts_30d,
  ROUND(AVG(p.like_count + p.comment_count) / ANY_VALUE(f.followers) * 100, 3) AS engagement_rate_pct
FROM latest_posts p JOIN followers f USING (username)
GROUP BY p.username
ORDER BY engagement_rate_pct DESC;

Follower growth, week over week:

SELECT
  username,
  snapshot_date,
  followers,
  followers - LAG(followers, 7) OVER (PARTITION BY username ORDER BY snapshot_date) AS gained_7d
FROM instagram.profile_snapshots
ORDER BY username, snapshot_date DESC;

Best hour to post, by average engagement (UTC):

SELECT
  EXTRACT(HOUR FROM taken_at) AS hour_utc,
  COUNT(*) AS posts,
  ROUND(AVG(like_count + comment_count)) AS avg_engagement
FROM instagram.posts
QUALIFY ROW_NUMBER() OVER (PARTITION BY post_id ORDER BY fetched_at DESC) = 1
GROUP BY hour_utc
ORDER BY avg_engagement DESC;

Top hashtags across tracked accounts:

SELECT LOWER(tag) AS hashtag, COUNT(*) AS uses, ROUND(AVG(like_count)) AS avg_likes
FROM instagram.posts, UNNEST(REGEXP_EXTRACT_ALL(caption, r'#(\w+)')) AS tag
GROUP BY hashtag
HAVING uses >= 3
ORDER BY avg_likes DESC
LIMIT 25;

Public data vs. Instagram Insights

If you only need your own account's reach, impressions, and saves, those come from Meta's Graph API Insights and require the account to be connected through a Facebook app. This pipeline covers what Insights can't: the public metrics of any account, so you can benchmark against competitors and creators side by side in the same warehouse.

Example response

What comes back

{
  "data": {
    "id": "13460080",
    "username": "nike",
    "full_name": "Nike",
    "biography": "Just Do It.",
    "is_verified": true,
    "is_private": false,
    "is_business": true,
    "category": null,
    "followers": 291952375,
    "following": 264,
    "posts": 1657,
    "external_url": "http://empli.fi/nike",
    "profile_pic_url": "https://instagram.com/.../nike.jpg",
    "public_email": null,
    "public_phone_number": null
  }
}

Response fields

Fields returned by GET /v1/profile

FieldDescription
idNumeric account id (stable even if the username changes).
usernameThe account's @handle.
full_nameDisplay name shown on the profile.
biographyThe profile's bio text.
is_verifiedWhether the account has a verified badge.
is_privateWhether the account is private.
is_businessWhether this is a business/professional account.
categoryBusiness category label when set (e.g. "Sportswear"); null otherwise.
followersNumber of accounts following this account.
followingNumber of accounts this account follows.
postsTotal number of posts on the profile.
external_urlThe clickable link in the bio, if any.
profile_pic_urlURL of the account's profile picture.
public_emailPublic contact email, if the account exposes one; null otherwise.
public_phone_numberPublic contact phone number, if exposed; null otherwise.

No login required

Nothing to authenticate but your API key

No OAuth flow

One static API key in an Authorization: Bearer header. No redirect, no refresh tokens, no expiring session.

No Meta app review

Never touch Meta's developer platform, Business Verification, or a connected Facebook Page. Sign up directly with Instagram API.

Public data only

Every endpoint returns only what's already visible on Instagram without logging in. Private accounts are never accessible.

Pricing

1 credit per request. Credits start at $8.00 per 1,000 and drop to $6.00 per 1,000 at volume — no subscription, no minimums. New accounts get 50 free credits.

See full pricing

FAQ

Instagram data in BigQuery, explained

Is there a native Instagram connector for BigQuery?

Google's BigQuery Data Transfer Service doesn't include Instagram, and Meta's Graph API only exposes accounts you've connected. The usual approach is a small scheduled job that calls an API and appends JSON rows. The Python script on this page is that job.

Can I load data for accounts I don't own?

Yes. Every endpoint returns public data for any public account: profiles, posts, reels, comments, hashtags, and locations. Private accounts are never accessible.

How much does a daily snapshot cost?

A profile lookup is 1 credit and a page of posts is 2 credits, so tracking 50 accounts daily is about 150 credits/day. Credits start at $8.00 per 1,000, and new accounts get 50 free.

Does this work with Snowflake, Postgres, or Google Sheets?

Yes. The API returns flat JSON, so only the last two lines of the script change. Swap the BigQuery load for your warehouse's client (snowflake-connector, psycopg, gspread) and keep everything else.

Can I get Instagram Insights (reach, impressions) into BigQuery this way?

No. Reach, impressions, and saves are private to the account owner and only available through Meta's Graph API for connected accounts. This API covers public metrics (followers, likes, comments, views) for any account.

Start pulling bigquery data

Create a free key and make your first request in under a minute — 50 credits on us.