A social media aggregator API is a single API that pulls the content published on all of your connected social accounts into one normalized stream. One call returns your Instagram posts, Facebook page posts, TikTok videos, LinkedIn updates, Threads posts, and YouTube uploads as the same object, with the same fields, paged by the same cursor. It is the content half of aggregation: your own feed and post history, not other people's messages. This guide covers the normalized Post object, cross-platform pagination, freshness, and a working feed build.
What is a social media aggregator API?
A social media aggregator API is an abstraction over several platform APIs that returns the content you own through one consistent contract. Every platform models a post differently. Instagram calls it media, TikTok calls it a video, LinkedIn calls it a share URN, YouTube calls it an upload. An aggregator collapses those into one Post object with a shared envelope, so a portfolio wall, a client dashboard, or an analytics job reads a single shape instead of six.
The word "aggregate" gets stretched a lot in this category, so be precise about the direction of travel. Content aggregation is outbound artifacts flowing back to you: posts, captions, permalinks, media, engagement counts. Engagement aggregation is inbound: comments, DMs, reviews, mentions. Those are different objects with different lifecycles, and a product that does one well does not automatically do the other. SocialAPI.ai models them as separate nouns, Post and Interaction, precisely so you do not have to guess which one you are holding.
The practical test for a real aggregator is whether one loop can render everything. If your feed component still branches on platform to find the caption, or calls a different function to get a permalink for TikTok than for Threads, you have a fan-out client, not an aggregator. Genuine aggregation means the normalization happens below your code, and the only per-platform thing left in your application is a logo.
Aggregator API vs unified inbox vs listening: which do you need?
Most teams searching for an aggregator actually want one of four different things, and picking the wrong one costs weeks. The distinction is ownership and direction: whose surface the content lives on, and whether it flows out from you or in toward you. Use the table below to route yourself to the right surface, and to the guide that covers it in depth. This post owns the first row only.
| You want to... | Surface you need | Where it is covered |
|---|---|---|
| Show or analyze the content you published | Content aggregation (GET /v1/posts) | This guide |
| Read and answer comments, DMs, and reviews | Unified inbox (Interaction) | Unified social inbox API |
| Track where your brand is named elsewhere | Mentions (sapi_mnt_) | Social media mentions API |
| React within seconds of something happening | Signed webhooks | Social listening webhooks |
| Read and reply to star ratings | Reviews (sapi_rev_) | Online reviews API |
| Read follower counts, bios, and avatars | Profile and account data | Social media profile API |
| Publish once to every network | Publishing (POST /v1/posts) | One API for all social networks |
Read the second column, not the first. Two products can both be marketed as aggregators while returning completely different objects, and the object determines what you can build. A dashboard that shows your last 90 days of posts with engagement needs content aggregation. A support queue needs the unified inbox. A PR alerting tool needs mentions plus webhooks. Most real products eventually need two or three of these, which is the argument for getting them from one contract instead of three vendors.
How does post and feed aggregation work?
Aggregation works in three moves: fetch each connected account with the platform's own paging model, normalize every result into one Post shape, then merge and sort into a single stream. SocialAPI.ai exposes the end of that chain as GET /v1/posts, which accepts account_ids, brand_id, status, platform, page_id, a from/to RFC3339 range, a search term, and a sort of created_desc or scheduled_asc. One request, every account, cursor-paged.
The normalized object has two levels, and the split is what makes cross-posting representable. The post carries what is shared (id, text, media, status, timestamps). Each target carries the per-platform delivery row: which account it landed on, the native platform_post_id, the permalink, the media_type, and the engagement metrics for that copy. A post that fanned out to three networks is one row with three targets, not three unrelated records you have to correlate later. Here is what GET /v1/posts?status=published&sort=created_desc&limit=25 returns:
{
"data": [
{
"id": "p_01HZ9X3Q4R5M6N7P8V2K0W1J",
"text": "Spring collection is live.",
"status": "published",
"visibility": "public",
"media_ids": ["med_01HZ9X3Q4R5M6N7P8V2K0W1J"],
"published_at": "2026-04-01T10:00:05Z",
"created_at": "2026-04-01T09:42:11Z",
"targets": [
{
"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J",
"platform": "instagram",
"status": "published",
"media_type": "reel",
"platform_post_id": "17895695668004550",
"permalink": "https://www.instagram.com/p/ABC123/",
"metrics": { "likes": 142, "comments": 23, "shares": 8, "saves": 31 },
"metrics_synced_at": "2026-04-01T12:00:00Z"
},
{
"account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1K",
"platform": "facebook",
"status": "published",
"platform_post_id": "1234567890_9876543210",
"permalink": "https://www.facebook.com/permalink/...",
"metrics": { "likes": 61, "comments": 4, "shares": 12, "saves": 0 },
"metrics_synced_at": "2026-04-01T12:00:00Z"
}
]
}
],
"pagination": { "has_more": true, "limit": 25, "next_cursor": "eyJpZCI6InBfMDFI..." }
}Two fields in that payload do more work than they look like. metrics_synced_at is an honesty field: engagement numbers are a snapshot, not a live read, and a feed that shows counts without showing their age is quietly lying to whoever reads it. And status matters because a partial post is real. One target published, another failed, and an aggregated feed that ignores per-target status will happily render a card linking to a post that does not exist.
Pagination across platforms
Underneath one cursor, the platforms disagree about almost everything. Meta surfaces use opaque cursors plus optional since/until time windows. LinkedIn uses classic offset paging with start and count. TikTok sends its cursor in a POST body rather than a query string. Google Business Profile has no post-history finder at all. That disagreement is most of the work an aggregator absorbs.
| Platform | Native post-history call | Paging model | Documented ceiling |
|---|---|---|---|
GET /{ig-user-id}/media | Cursor + since/until | 10K most recent media; stories excluded | |
GET /{page-id}/feed | Cursor (after/before) + since/until | ~600 posts per year; max 100 per request | |
| Threads | GET /{user-id}/threads | Cursor + date range | Not published |
| TikTok | POST /v2/video/list/ | Cursor inside the request body | Not published |
GET /rest/posts?q=author | Offset (start / count) | count max 100; member posts need a restricted scope | |
| Google Business Profile | No history finder | Not applicable | No official post-history endpoint |
Now read the ceiling column as a product constraint rather than trivia. Your aggregated archive is only as deep as your shallowest platform. A brand with five years of Instagram history and five years of Facebook history does not get a symmetric five-year feed: Instagram serves up to 10,000 recent media items, while the Facebook Page feed returns roughly 600 posts per year and caps each request at 100 (Meta, 2026). Backfill jobs that assume symmetry produce a timeline with a hole in it.
Post history sync
There are two ways content enters an aggregated feed, and mixing them up is the most common design error. Content published through the API is already there, with its targets and delivery status attached. Content published natively (someone posted from the phone app) has to be discovered by reading the platform's history endpoint and reconciled against what you already hold. YouTube, for example, exposes an explicit re-sync at POST /v1/platforms/youtube/accounts/{id}/sync, which returns 501 when a connector has no post-sync capability.
Every connected account is fetched with its own native paging model, normalized into one Post with per-platform targets, merged on published_at, and served as a single feed to a dashboard, wall, or archive job.
Reconciliation needs a key that survives both paths, and the post id alone is not enough because one post can exist on several networks. Use the pair (platform, platform_post_id), or the composite of post id and target account. That pair is stable across a re-sync, stable across a native edit, and stable when the same content is deliberately cross-posted. Keying on your own row id instead is how a backfill quietly creates duplicates that only surface in a client's dashboard three weeks later.
Polling vs webhooks for keeping the feed fresh
A feed has two kinds of staleness and they need different fixes. Membership staleness is a post missing from the feed. Value staleness is a post that is present but showing yesterday's like count. Push solves the first, pull solves the second, and any design that tries to use one mechanism for both ends up either over-polling or under-reporting.
Membership changes are lifecycle events, and SocialAPI.ai emits them as webhooks in the posts category so your feed can react instead of re-scanning. The authoritative catalog is GET /v1/webhooks/events, so a dashboard can render the list without hardcoding it. The verification, retry, and idempotency mechanics behind these deliveries are the same ones covered in the social listening webhooks guide, so this post will not re-teach them.
post.scheduled— a post entered thescheduledstate with a futurescheduled_at. Render it in a queue view, not the live feed.post.published— every target succeeded. This is the event that should insert a card into an aggregated feed.post.partial— some targets published, others failed. Insert only the targets whose status ispublished.post.failed— no target succeeded. Nothing belongs in the feed; this belongs in an alert.post.updated,post.deleted,post.unpublished— the correction path. Without these, your feed drifts from reality and never recovers.post.retried— a failed or partial post is being re-attempted; a terminal event follows when it settles.
Value staleness is the pull half, and it runs on a different budget. Nothing fires a webhook when a like count ticks from 141 to 142, so metrics refresh on a schedule via GET /v1/posts/{pid}/metrics, which triggers a live platform call. Refresh recent posts often and old posts rarely: engagement decays fast. Meta's rate limit is an app-wide ceiling scaled by daily active users, so an undifferentiated refresh loop spends a shared budget on posts nobody is looking at (Meta, 2026).
How do you build an aggregated feed?
The build is smaller than the explanation. You page GET /v1/posts with a cursor until pagination.has_more is false, fan each post out into one card per published target, and sort the result. There is no platform switch anywhere in the loop, because the merge is a sort rather than a set of adapters. The limit maxes out at 100, and brand_id is the right scoping filter when you are rendering one agency client at a time.
# Every published post across every connected account, newest first.
curl -G https://api.social-api.ai/v1/posts \
-H "Authorization: Bearer $SOCAPI_KEY" \
-d status=published \
-d sort=created_desc \
-d limit=100 \
-d "from=2026-01-01T00:00:00Z"
# Narrow to one agency client, or one network.
curl -G https://api.social-api.ai/v1/posts \
-H "Authorization: Bearer $SOCAPI_KEY" \
-d brand_id=b_01HZAB12CDEFGHJKMNPQRSTUVW \
-d platform=instagram \
-d status=published
# Refresh engagement for one post (live platform call, per target).
curl https://api.social-api.ai/v1/posts/p_01HZ9X3Q4R5M6N7P8V2K0W1J/metrics \
-H "Authorization: Bearer $SOCAPI_KEY"// One aggregated feed across every connected account.
// The Post shape is identical for every platform, so the
// merge is a sort. No switch on platform anywhere below.
const API = "https://api.social-api.ai/v1";
async function* aggregatedFeed({ brandId, since } = {}) {
let cursor;
do {
const qs = new URLSearchParams({
status: "published",
sort: "created_desc",
limit: "100", // max page size
...(brandId && { brand_id: brandId }),
...(since && { from: since }), // RFC3339
...(cursor && { cursor }),
});
const res = await fetch(`${API}/posts?${qs}`, {
headers: { Authorization: `Bearer ${process.env.SOCAPI_KEY}` },
});
if (!res.ok) throw new Error(`posts list failed: ${res.status}`);
const { data, pagination } = await res.json();
// Fan a cross-posted row out into one card per platform copy.
for (const post of data) {
for (const t of post.targets) {
if (t.status !== "published") continue; // skip failed/pending copies
yield {
// Stable across re-sync and native edits. Dedupe on this.
key: `${t.platform}:${t.platform_post_id}`,
platform: t.platform,
text: t.text || post.text, // per-target override wins
permalink: t.permalink,
mediaType: t.media_type ?? null, // feed | reel | stories | carousel
publishedAt: t.published_at ?? post.published_at,
metrics: t.metrics ?? null,
metricsAge: t.metrics_synced_at ?? null, // show it, do not hide it
};
}
}
cursor = pagination?.has_more ? pagination.next_cursor : undefined;
} while (cursor);
}
// Merge is a sort, because every item already has the same shape.
const feed = [];
for await (const card of aggregatedFeed({ since: "2026-01-01T00:00:00Z" })) {
feed.push(card);
}
feed.sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));Three things in that loop are deliberate. Skipping targets whose status is not published keeps broken permalinks out of the UI. Keying on platform:platform_post_id makes the feed idempotent under re-sync. Carrying metrics_synced_at into the card means the interface can render "as of 2 hours ago" instead of implying a live number. If you need the same content as a downloadable archive rather than a live feed, POST /v1/accounts/{id}/export enqueues an async analytics export job instead.
Where does this fit in the rest of the cluster?
Content aggregation is one surface of a bigger contract. The same connected accounts that produce your feed also produce comments, DMs, reviews, and mentions, and the same authentication and rate-limit accounting sits underneath all of it. If you are scoping a product rather than a single screen, read the surface guide that matches the feature you are building next; each one assumes the model described here.
Unified social inbox API: one object for comments, DMs, reviews, and mentions
One API for all social networks: the publishing side of the same contract
Social media profile API: accounts, followers, bios, avatars
Social media mentions API: brand monitoring across platforms
Social listening webhooks: real-time delivery, signing, retries
Online reviews API: read and respond to Google Business Profile reviews
Frequently asked questions
- What is a social media aggregator API?
- A social media aggregator API is a single API that returns the content published on all of your connected social accounts in one normalized shape. Instead of calling Instagram, Facebook, TikTok, LinkedIn, Threads, and YouTube separately and reconciling six different post schemas, you call one endpoint and get one Post object per piece of content, with a per-platform target row for each copy. It is what powers dashboards, portfolio walls, content analytics, and archival jobs.
- How is a social media aggregator API different from a unified inbox API?
- They aggregate opposite directions. An aggregator API returns outbound content you published: posts, captions, permalinks, media, and engagement counts. A unified inbox API returns inbound engagement: comments, DMs, reviews, and mentions, normalized into an Interaction object. They share connected accounts and authentication but return different nouns with different lifecycles. Most products eventually need both, which is the argument for getting them from one contract rather than two vendors.
- Can a social post history API return all of my old posts?
- Not always, and the limit comes from the platform rather than the aggregator. Instagram's user media edge returns a maximum of 10,000 of the most recently created media and excludes stories. The Facebook Page feed returns roughly 600 posts per year and caps a single request at 100. Google Business Profile has no post-history finder at all. An aggregated archive is therefore only as deep as its shallowest platform, and any backfill design should account for that asymmetry rather than assume symmetry.
- How do you paginate a social feed API across platforms?
- You do not, if the aggregator is doing its job. Underneath, Meta surfaces use opaque cursors with optional since/until windows, LinkedIn uses offset paging with start and count, and TikTok puts its cursor in a POST body. SocialAPI.ai exposes one cursor: call GET /v1/posts, read pagination.next_cursor from the response, and pass it back as the cursor query parameter until has_more is false. Page size is capped at 100.
- Should I poll or use webhooks to keep an aggregated feed fresh?
- Both, for different kinds of staleness. Webhooks fix membership: post.published, post.partial, post.updated, post.deleted, and post.unpublished tell you when the set of posts changed, so you never re-scan to find out. Polling fixes values: no webhook fires when a like count increments, so engagement is refreshed on a schedule with GET /v1/posts/{pid}/metrics. Refresh recent posts frequently and old posts rarely, since engagement decays and rate-limit budget is shared.
- How do I avoid duplicates when aggregating posts?
- Key on the pair of platform and platform_post_id, or on the composite of your post id and the target account. Do not key on your own row id alone, because one cross-posted row represents several platform copies, and a history re-sync of natively published content will re-surface items you already hold. The platform pair is stable across re-syncs, across native edits, and across deliberate cross-posting, which is exactly the property a dedupe key needs.
- What can you build with a social media aggregator API?
- The common four are client dashboards that show every network in one timeline, embeddable portfolio or social walls on a marketing site, content analytics that compare performance across platforms on shared fields, and archival jobs that keep a durable copy of what was published. All four need the same thing: one normalized Post, honest per-target status, and metrics whose age is visible rather than implied.
A social media aggregator API is not a feed widget with extra steps. It is a decision to stop maintaining six post schemas, six paging models, and six history ceilings inside your product. Get the object right first (one post, one target per copy, a dedupe key that survives re-sync), then decide freshness deliberately rather than by cron interval. The current endpoint reference, filters, and per-platform coverage live at docs.social-api.ai so they stay accurate instead of drifting in prose.
Platform limits and paging behavior in this guide are linked to their primary vendor documentation so every claim can be checked at its source: Meta Graph API: Page /feed edge (approximately 600 posts per year, maximum 100 posts per request) · Meta Instagram Platform: IG User /media edge (maximum 10K most recently created media, stories excluded, time-based pagination) · LinkedIn Posts API: find posts by author (start/count offset paging, count maximum 100, sortBy LAST_MODIFIED or CREATED) · Meta Graph API: rate limiting overview (app-level ceiling scaled by daily active users)
