Skip to content

Profiles API Reference

The Profiles API allows you to retrieve and manage connected social media profiles. Profiles represent authenticated connections to social media platforms.

MethodEndpointDescription
GET/api/profilesList all profiles
GET/api/profiles/:idGet a single profile (with latest stats)
GET/api/profiles/:id/placementsList placements for a profile
PATCH/api/profiles/:id/assign_placement_to_groupAssign a placement to another profile group
PATCH/api/profiles/:id/assign_to_groupMove a profile to another profile group
GET/api/profiles/:id/statsGet profile stats timeseries
GET/api/profiles/:id/follower_historyDaily follower series for a date range
GET/api/profiles/:id/daily_statsPer-day account metrics for a date range
GET/api/profiles/:id/participants/:user_idFollow/verified status of an Instagram user (Instagram only)
GET/api/profiles/:id/resolve_mentionResolve a LinkedIn company page to its mention URN (LinkedIn only)
POST/api/profiles/:id/backfill_postsBackfill older posts from the platform
GET/api/profiles/:id/post_syncsList post sync runs
GET/api/profiles/:id/post_syncs/:post_sync_idGet one post sync run
DELETE/api/profiles/:idDelete/disconnect a profile

A profile represents a connected social media account.

FieldTypeDescription
idstringUnique profile identifier (id)
namestringDisplay name of the connected account
usernamestring|nullHandle on the platform (without @). null when the platform does not expose one
statusstringPlatform connection status: active, expired, inactive (might be disconnected or suspended on a platform)
platformstringPlatform identifier
profile_group_idstringID of the profile group this belongs to
expires_atstring|nullISO 8601 timestamp when the connection expires (if applicable)
post_countintegerNumber of posts made through this profile
avatar_urlstring|nullURL to the profile’s avatar image (resized, hosted by Postproxy). null if not yet downloaded
platform_urlstring|nullPublic URL of the account on the platform. null when the platform has no stable public profile URL (e.g. linkedin, google_business)
placements_sync_statusstring|nullState of the latest pull of this profile’s placements: syncing, synced, or failed. The pull runs in the background right after connecting and can take several minutes on profiles with many pages/locations. null for platforms without placements and for telegram, whose channels arrive via webhook
PlatformAccount type
facebookFacebook Page
instagramInstagram Business/Creator Account
tiktokTikTok Account
linkedinLinkedIn Profile or Company Page
youtubeYouTube Channel
twitterX (Twitter) Account
threadsThreads Account
pinterestPinterest Account
blueskyBluesky Account
telegramTelegram Bot (publishes to channels via placements)
google_businessGoogle Business Profile (publishes to locations via placements)

GET /api/profiles

Retrieves your profiles. Pass profile_group_id to restrict to a single group; omit it to return profiles across every group your API key or OAuth user can access.

NameTypeRequiredDefaultDescription
profile_group_idstringNo-Restrict to profiles in this group. When omitted, returns profiles across every profile group your API key or OAuth user can access.
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"data": [
{
"id": "prof123abc",
"name": "My Company Page",
"username": null,
"platform": "facebook",
"status": "active",
"profile_group_id": "grp456xyz",
"expires_at": null,
"post_count": 42,
"avatar_url": "https://cdn.postproxy.dev/uploads/avatar_prof123abc.jpg",
"platform_url": "https://www.facebook.com/123456789012345"
},
{
"id": "prof789def",
"name": "@mycompany",
"username": "mycompany",
"platform": "instagram",
"status": "expired",
"profile_group_id": "grp456xyz",
"expires_at": "2024-03-15T00:00:00.000Z",
"post_count": 38,
"avatar_url": "https://cdn.postproxy.dev/uploads/avatar_prof789def.jpg",
"platform_url": "https://www.instagram.com/mycompany/"
},
{
"id": "prof321ghi",
"name": "John Doe",
"username": null,
"platform": "linkedin",
"status": "inactive",
"profile_group_id": "grp456xyz",
"expires_at": null,
"post_count": 15,
"avatar_url": null,
"platform_url": null
},
{
"id": "prof654jkl",
"name": "@mycompany",
"username": "mycompany",
"platform": "twitter",
"status": "active",
"profile_group_id": "grp456xyz",
"expires_at": null,
"post_count": 127,
"avatar_url": "https://cdn.postproxy.dev/uploads/avatar_prof654jkl.jpg",
"platform_url": "https://x.com/mycompany"
}
]
}

The same scoping rule applies to the :id endpoints below (GET /api/profiles/:id, /placements, /stats, /follower_history, /daily_stats, PATCH /api/profiles/:id/assign_placement_to_group, PATCH /api/profiles/:id/assign_to_group, and DELETE /api/profiles/:id):

profile_group_id sent?Profile ID lookup scope
YesThe profile must belong to that group
NoResolved across every profile group your API key or OAuth user can access

This mirrors the resolution rule already used by POST /api/posts.


GET /api/profiles/:id

Retrieves a single profile by its ID. The response includes the profile fields plus the latest stats snapshot per placement and (for placement networks) a summary_stats rollup.

For non-placement networks (e.g. bluesky, twitter), latest_stats contains a single entry with placement_id: null and summary_stats is null.

Snapshots are typically refreshed every 23 hours per profile. If latest_stats is empty, the profile has been connected but has not yet been polled for stats.

NameTypeRequiredDescription
idstringYesProfile id
FieldTypeDescription
latest_statsarrayLatest snapshot per placement. One entry for non-placement networks (with placement_id: null). Empty array if no snapshots have been recorded yet.
latest_stats[].placement_idstring|nullPlatform-specific placement ID. null for non-placement networks.
latest_stats[].statsobjectPlatform-specific metrics. See Stats fields by network.
latest_stats[].recorded_atstringISO 8601 timestamp when the snapshot was captured.
summary_statsobject|nullFor placement networks: numeric values summed across the latest snapshot of every placement. null for non-placement networks and when no snapshots exist. Non-numeric values (e.g. channel_title) are omitted from the summary.
summary_stats.statsobjectSummed metrics.
summary_stats.recorded_atstringISO 8601 timestamp of the most recent placement snapshot included in the summary.
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof123abc" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"id": "prof_li_001",
"name": "Acme Inc",
"username": null,
"platform": "linkedin",
"status": "active",
"profile_group_id": "grp456xyz",
"expires_at": null,
"post_count": 42,
"avatar_url": "https://cdn.postproxy.dev/uploads/avatar_prof_li_001.jpg",
"platform_url": null,
"latest_stats": [
{
"placement_id": "108520199",
"stats": {
"followerCount": 4567,
"shareCount": 10,
"likeCount": 99,
"allPageViews": 12728,
"overviewPageViews": 6348,
"aboutPageViews": 1533,
"careersPageViews": 1378,
"peoplePageViews": 3370,
"insightsPageViews": 99
},
"recorded_at": "2026-05-11T08:00:00Z"
},
{
"placement_id": "110131347",
"stats": {
"followerCount": 1200,
"shareCount": 4,
"likeCount": 22,
"allPageViews": 3100
},
"recorded_at": "2026-05-11T08:00:01Z"
}
],
"summary_stats": {
"stats": {
"followerCount": 5767,
"shareCount": 14,
"likeCount": 121,
"allPageViews": 15828,
"overviewPageViews": 6348,
"aboutPageViews": 1533,
"careersPageViews": 1378,
"peoplePageViews": 3370,
"insightsPageViews": 99
},
"recorded_at": "2026-05-11T08:00:01Z"
}
}

GET /api/profiles/:id/placements

For an account-wide listing across every profile — including which connected profile actively serves each placement and how to switch it — see Placements.

Retrieves the available placements for a profile. For Facebook profiles, placements are business pages. For LinkedIn profiles, placements include the personal profile and organizations. For Pinterest profiles, placements are boards. For Telegram profiles, placements are the channels the bot has been added to. For Google Business profiles, placements are the locations associated with the connected Business Profile account(s).

This endpoint is available for facebook, linkedin, pinterest, telegram, and google_business profiles.

Placements are account-level records: your account holds exactly one record per placement, shared by every profile that can see it on the platform, and this endpoint returns all of them for the profile — so two profiles that both admin the same Facebook Page both list it, under the same id. A placement and its history survive its connecting profile’s disconnect; any other connected profile with access serves it from then on. The listing follows the profile-group scoping below: a group-scoped key or profile_group_id returns that group’s placements; an unscoped account key also includes placements left unassigned by a deleted group.

Placements are pulled in the background right after a profile connects, which can take several minutes for profiles with many pages or locations. An empty list right after connecting usually means that pull is still running: check placements_sync_status on the Profile object (syncingsynced / failed), or subscribe to the profile.placements_synced webhook instead of polling.

If no placement is specified when creating a post:

  • LinkedIn: defaults to the personal profile
  • Facebook: it fails — page_id is always required
  • Pinterest: it fails
  • Telegram: it fails — chat_id is always required
  • Google Business: it fails — location_id (the location resource path) is always required

For Telegram, each placement is a channel the bot has been added to. The placement id is the Telegram chat_id you pass as chat_id when creating a post. The list is empty until the user adds the bot as administrator to a channel — Telegram pushes a my_chat_member event for each one and we record it. Poll this endpoint after connecting Telegram until the expected channels appear.

For Google Business, each placement is a location resource managed by the connected account(s). The placement id is the full Business Profile resource path (e.g. accounts/123456789/locations/987654321) you pass as location_id when creating a post. Each location’s metadata.place_id is its Google Maps Place ID — use it to cross-reference the location against Google Maps / Places or build a reviews permalink; it’s omitted when Google returns no Place ID for the location. Listing locations issues one call per Google Business account on the profile, so responses may be slower than other networks when many accounts/locations are linked.

NameTypeRequiredDescription
idstringYesProfile id
FieldTypeDescription
idstring|nullPlatform-specific placement ID. null for personal profile (LinkedIn)
namestringDisplay name of the placement
metadataobjectExtra per-placement data Postproxy stores. For Google Business this carries the location’s place_id (Google Maps Place ID). {} when there’s nothing extra.
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof123abc/placements" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"data": [
{
"id": null,
"name": "Personal Profile",
"metadata": {}
},
{
"id": "108520199",
"name": "Acme Marketing",
"metadata": {}
},
{
"id": "110131347",
"name": "Acme Labs",
"metadata": {}
}
]
}

PATCH /api/profiles/:id/assign_placement_to_group

Moves a single placement (a Facebook Page, LinkedIn organization, Telegram channel, Google Business location, …) into a different profile group, independent of where its parent connection is homed. This is the API equivalent of the group selector on a profile’s page in the app. Group-scoped surfaces (profile lists, post attribution, composer, queues) follow the placement’s group; publishing itself is untouched.

A connection shows a placement in exactly one group at a time. On accounts allowed to connect the same platform account more than once, each connection carries its own view: this endpoint moves the placement only for the connection in the URL, and other connections seeing the same placement keep their groups. A placement lands in the group of the profile that connected it and only becomes unassigned when that group is deleted; an unassigned placement can’t be posted to until it’s assigned again. Account-wide API keys can assign unassigned placements; a group-scoped key stays confined to its own group’s placements.

NameTypeRequiredDescription
idstringYesProfile ID (a connection that can see the placement; the move applies to this connection’s view)
NameTypeRequiredDescription
placement_idstringYesPlatform-specific placement ID, as returned by List placements
target_profile_group_idstringYesID of the profile group to move the placement into. Must be in the same environment (live/sandbox) as your API key
Terminal window
curl -X PATCH "https://api.postproxy.dev/api/profiles/prof123abc/assign_placement_to_group" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"placement_id": "967627696429808",
"target_profile_group_id": "grp456def"
}'

Response — the moved placement in the Placement object shape, plus its new group:

{
"id": "967627696429808",
"name": "Acme Store",
"metadata": {
"username": "acmestore"
},
"profile_group_id": "grp456def"
}

An API key scoped to a single profile group cannot move placements across groups — both the placement and the target group must be within the key’s group, so any cross-group attempt returns 404. Use an account-wide API key to move placements between groups.

Profile, placement, or target group not found / not accessible (404):

{
"error": "Not found. Make sure you pass the correct profile_group_id"
}

Missing placement_id or target_profile_group_id (400):

{
"status": 400,
"error": "Bad Request",
"message": "param is missing or the value is empty or invalid: placement_id"
}

PATCH /api/profiles/:id/assign_to_group

Moves a profile (the connection itself) into a different profile group, without disconnecting and reconnecting it. This is the API equivalent of the group selector next to a profile’s name in the app, and the counterpart of Assign placement to group, which moves a single placement rather than the whole connection. The connection keeps its ID, tokens, and post history, so nothing scheduled against it breaks.

The profile’s placements that sit in its current home group move with it. Placements you had already assigned to another group stay where they are.

The target group must be able to take the connection: on group-billed plans a group holds one profile per network, so moving a profile into a group that already has one on the same network returns 422. The target group must be in the same environment (live/sandbox) as the profile.

NameTypeRequiredDescription
idstringYesProfile ID
NameTypeRequiredDescription
target_profile_group_idstringYesID of the profile group to move the profile into. Must be in the same environment (live/sandbox) as your API key
Terminal window
curl -X PATCH "https://api.postproxy.dev/api/profiles/prof123abc/assign_to_group" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"target_profile_group_id": "grp456def"
}'

Response — the moved profile, in the same shape as Get profile, with profile_group_id set to the new group. Moving a profile into the group it is already in is a no-op and returns 200:

{
"id": "prof123abc",
"name": "Acme Corp",
"username": null,
"platform": "linkedin",
"status": "active",
"profile_group_id": "grp456def",
"expires_at": "2026-12-01T10:00:00Z",
"post_count": 42,
"avatar_url": "https://...",
"platform_url": null,
"placements_sync_status": null,
"latest_stats": [],
"summary_stats": null
}

An API key scoped to a single profile group cannot move profiles across groups — both the profile and the target group must be within the key’s group, so any cross-group attempt returns 404. Use an account-wide API key to move profiles between groups.

Profile or target group not found / not accessible (404):

{
"error": "Not found. Make sure you pass the correct profile_group_id"
}

Target group already has a profile on this network (422):

{
"error": "Another linkedin profile is already connected to this profile group. Connect it to a different profile group, or move to a plan billed per profile to keep several here."
}

Missing target_profile_group_id (400):

{
"status": 400,
"error": "Bad Request",
"message": "param is missing or the value is empty or invalid: target_profile_group_id"
}

GET /api/profiles/:id/stats

Retrieves the full stats timeseries for a profile. Mirrors Post Stats in shape (records[].stats + recorded_at) — use this to plot follower growth and engagement trends over time.

This is the raw snapshot series — one record per polling cycle, keys straight from the platform. For clean one-record-per-day series, see Follower history and Daily stats.

Snapshots are captured roughly every 23 hours. For networks with multiple placements (Facebook pages, LinkedIn organizations, Telegram channels, Google Business locations), each placement has its own timeseries — placement_id is required so the response is scoped to a single placement.

NameTypeRequiredDescription
idstringYesProfile id
NameTypeRequiredDescription
placement_idstringConditionalRequired for facebook, linkedin, telegram, and google_business profiles. The platform-specific ID returned by List placements. Omit (or ignored) for other networks.
fromstringNoISO 8601 timestamp — only include snapshots recorded at or after this time.
tostringNoISO 8601 timestamp — only include snapshots recorded at or before this time.
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof_li_001/stats?placement_id=108520199&from=2026-04-01T00:00:00Z" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"data": {
"profile_id": "prof_li_001",
"platform": "linkedin",
"placement_id": "108520199",
"records": [
{
"stats": {
"followerCount": 4500,
"shareCount": 8,
"likeCount": 80,
"allPageViews": 12000
},
"recorded_at": "2026-05-09T08:00:00Z"
},
{
"stats": {
"followerCount": 4520,
"shareCount": 9,
"likeCount": 90,
"allPageViews": 12400
},
"recorded_at": "2026-05-10T08:00:00Z"
},
{
"stats": {
"followerCount": 4567,
"shareCount": 10,
"likeCount": 99,
"allPageViews": 12728
},
"recorded_at": "2026-05-11T08:00:00Z"
}
]
}
}
FieldTypeDescription
data.profile_idstringProfile ID.
data.platformstringNetwork name (facebook, linkedin, bluesky, etc.).
data.placement_idstring|nullThe placement filter that was applied (echo of the request). null for non-placement networks.
data.recordsarraySnapshots ordered by recorded_at ascending.
records[].statsobjectPlatform-specific metrics. See Stats fields by network.
records[].recorded_atstringISO 8601 timestamp when the snapshot was captured.

Missing placement_id for a placement network (400):

{
"error": "placement_id is required for linkedin profiles"
}

Profile not found (404):

{
"error": "Not found"
}

GET /api/profiles/:id/follower_history

Daily follower movement for a date range, one record per day. This reads a daily rollup refreshed once a day per network — distinct from the raw snapshot timeseries served by Profile stats.

Follower data is collected for facebook, instagram, youtube, and tiktok profiles.

NameTypeRequiredDescription
idstringYesProfile id
NameTypeRequiredDescription
placement_idstringConditionalRequired for placement networks (facebook, linkedin, telegram, google_business) — the placement ID from List placements. Of these, only Facebook reports follower data.
fromstringNoISO 8601 date. Defaults to 29 days before to.
tostringNoISO 8601 date. Defaults to yesterday.

The range is capped at 366 days.

Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof_fb_001/follower_history?placement_id=108520199&from=2026-08-01&to=2026-08-24" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"data": {
"profile_id": "prof_fb_001",
"platform": "facebook",
"placement_id": "108520199",
"from": "2026-08-01",
"to": "2026-08-24",
"records": [
{
"date": "2026-08-01",
"followers_gained": 12,
"followers_lost": 3,
"followers_change": 9,
"followers_total": 5210
}
]
}
}
FieldTypeDescription
data.placement_idstring|nullThe placement filter that was applied (echo of the request). null for non-placement networks.
data.from / data.tostringThe date range that was applied, including defaults.
records[].datestringISO 8601 date the record describes.
records[].followers_gainedinteger|nullNew followers that day.
records[].followers_lostinteger|nullUnfollows that day.
records[].followers_changeinteger|nullNet movement. Derived as followers_gained − followers_lost when the platform reports both but no net figure.
records[].followers_totalinteger|nullTotal follower count as of that day.

Metrics a platform does not expose are null; days with no follower data at all are omitted rather than returned as all-null records.

Platformfollowers_gainedfollowers_lostfollowers_changefollowers_total
Facebookcomputedmost recent day only
Instagram✓ (gains only)most recent day only
YouTubemost recent day only
TikTok✓ (net day-over-day change)✓ (daily)

Notes:

  • Days are bucketed as the platform reports them — Meta and YouTube use US Pacific time, TikTok uses UTC.
  • History depth varies by platform: YouTube reaches back years, Facebook roughly 90 days, Instagram 30 days, and TikTok accumulates forward from when daily collection started for the profile.
  • Google Business has no follower data — a Business Profile has no audience count.

Missing placement_id for a placement network (400):

{
"error": "placement_id is required for facebook profiles"
}

from after to (400):

{
"error": "from must be on or before to"
}

Range longer than 366 days (422):

{
"error": "date range cannot exceed 366 days"
}

GET /api/profiles/:id/daily_stats

Per-day account metrics for a date range, with a choice of attribution:

  • attribution=received (default) — engagement bucketed by the day it arrived. A like landing today on last month’s post counts today. This is the right mode for “activity during this period”.
  • attribution=publish — each post’s lifetime totals summed onto its publish day, computed from the latest post snapshots at read time.

Account-level daily metrics (views, platform_stats) are collected for facebook, instagram, youtube, tiktok, and google_business profiles; the received-attribution engagement metrics (*_received) work for every network with post insights.

NameTypeRequiredDescription
idstringYesProfile id
NameTypeRequiredDescription
placement_idstringConditionalRequired for placement networks (facebook, linkedin, telegram, google_business) — the placement ID from List placements. For Google Business it’s the location’s placement id.
attributionstringNoreceived (default) or publish.
fromstringNoISO 8601 date. Defaults to 29 days before to.
tostringNoISO 8601 date. Defaults to yesterday.

The range is capped at 366 days.

Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof_fb_001/daily_stats?placement_id=108520199&from=2026-08-01&to=2026-08-24" \
-H "Authorization: Bearer YOUR_API_KEY"

Response (attribution=received):

{
"data": {
"profile_id": "prof_fb_001",
"platform": "facebook",
"placement_id": "108520199",
"attribution": "received",
"from": "2026-08-01",
"to": "2026-08-24",
"data_timezone": "America/Los_Angeles",
"records": [
{
"date": "2026-08-01",
"views": 12556,
"viewers": 8210,
"viewers_7d": 31400,
"viewers_28d": 90210,
"likes_received": 41,
"comments_received": 7,
"shares_received": 3,
"views_received": 9800,
"platform_stats": {
"page_views_total": 120,
"page_post_engagements": 210
}
}
],
"metrics": {
"views": { "supported": true, "source": "native", "platform_metric": "page_media_view", "additive": true, "missing_day": "unavailable", "coverage_start": "2026-06-09", "provisional_from": "2026-08-23" },
"viewers": { "supported": true, "source": "native", "platform_metric": "page_total_media_view_unique", "window": "1d", "additive": false, "missing_day": "unavailable", "coverage_start": "2026-06-09", "provisional_from": "2026-08-23" },
"viewers_7d": { "supported": true, "source": "native", "platform_metric": "page_total_media_view_unique", "window": "7d", "additive": false, "missing_day": "unavailable", "coverage_start": "2026-06-09", "provisional_from": "2026-08-23" },
"viewers_28d": { "supported": true, "source": "native", "platform_metric": "page_total_media_view_unique", "window": "28d", "additive": false, "missing_day": "unavailable", "coverage_start": "2026-06-09", "provisional_from": "2026-08-23" },
"likes_received": { "supported": true, "source": "derived", "additive": true, "missing_day": "zero", "coverage_start": "2026-07-14", "provisional_from": "2026-08-23" },
"comments_received": { "supported": true, "source": "derived", "additive": true, "missing_day": "zero", "coverage_start": "2026-07-14", "provisional_from": "2026-08-23" },
"shares_received": { "supported": true, "source": "derived", "additive": true, "missing_day": "zero", "coverage_start": "2026-07-14", "provisional_from": "2026-08-23" },
"views_received": { "supported": true, "source": "derived", "additive": true, "missing_day": "zero", "coverage_start": "2026-07-14", "provisional_from": "2026-08-23" }
}
}
}
FieldTypeDescription
records[].datestringISO 8601 date the record describes.
records[].viewsinteger|nullAccount-level views that day, as the platform reports them.
records[].viewers / viewers_7d / viewers_28dinteger|nullUnique viewers as computed by the platform for the window ending on that day: the day itself, the trailing 7 days, the trailing 28 days. null where the platform has no unique-viewer metric (see the table below).
records[].likes_received / comments_received / shares_received / views_receivedinteger|nullThat day’s increase in engagement, summed across the profile’s tracked posts.
records[].platform_statsobjectNetwork-native daily extras (open map, varies by platform — see below).
data_timezonestring|nullThe timezone the platform buckets days in (null for networks without a daily series).
metricsobjectPer-key metadata — see Metric metadata.

Unique counts are not additive — never sum viewers* values across days; read the _7d/_28d value on the last day of your range instead. An unavailable value is always null, never 0.

On placement networks the *_received metrics are profile-level (summed across all of the connection’s placements) and are merged into the requested placement’s records by date, while views, viewers*, and platform_stats are scoped to the requested placement.

Posts are polled at full cadence for their first week, then daily up to 30 days old; beyond that a post’s later engagement no longer registers in the *_received series.

Platformviewers (1d)viewers_7dviewers_28d
Facebookpage_total_media_view_unique (period=day)✓ (period=week)✓ (period=days_28)
Instagramreach (period=day, last 30 days)✓ (period=week)✓ (period=days_28)
YouTube— (the Analytics API has no unique-viewer metric)
TikTok
Google Business

metrics has one entry per top-level record key (views, viewers*, *_received, or *_published under attribution=publish):

FieldMeaning
supportedWhether this profile can ever carry a value for the key. When false the record value is always null and reason says why: platform (the network has no such metric), plan (the account’s plan doesn’t include insights, or insights are switched off), permission (the connected account didn’t grant the insights permission).
sourcenative — stored as the platform reports it. derived — computed by Postproxy (TikTok views from cumulative video totals, every *_received / *_published key from post snapshots).
platform_metricThe platform metric the key is read from (native keys only).
windowFor unique-viewer keys, the window the value covers, ending on the record’s day: 1d, 7d, 28d.
additivetrue when values can be summed across days; false for unique-viewer keys.
missing_dayWhat a day missing from records, or a null value, means on or after coverage_start: unavailable (not reported by the platform for that day) or zero (*_received — an absent day had no tracked activity). Before coverage_start everything is unavailable.
coverage_startFirst day Postproxy holds a value for this key on this profile; null when supported but nothing has been stored yet (for example an Instagram account under 100 followers, which Meta refuses to serve series for).
provisional_fromFirst day whose values may still be revised on a later pull — Meta finalises daily numbers within ~48 hours, YouTube Analytics within ~72 hours, *_received for the trailing 2 days. null when values are final as soon as they appear (TikTok).

records[] instead carries likes_published, comments_published, shares_published, and views_published — each post’s latest lifetime totals bucketed onto its publish day. platform_stats is empty in this mode. metrics describes those keys: all derived, missing_day: "zero", coverage_start is the profile’s earliest publish day, and provisional_from reaches back 30 days because a post’s totals keep moving for as long as it is polled.

NetworkTypical fields
facebookpage_views_total, page_post_engagements
instagram
youtubeestimatedMinutesWatched, likes, comments, shares
tiktoktotal_video_views_cum, total_video_likes_cum, total_video_comments_cum, total_video_shares_cum, video_count (cumulative totals across the account’s video list)
google_businessThe raw Business Profile Performance metrics: business_impressions_desktop_search, business_impressions_desktop_maps, business_impressions_mobile_search, business_impressions_mobile_maps, website_clicks, call_clicks, business_direction_requests, business_conversations, business_bookings, business_food_orders, business_food_menu_clicks

A key only appears when the platform returned a value for it, so fields can come and go between days.

Same as Follower history, plus:

Invalid attribution (400):

{
"error": "attribution must be received or publish"
}

The stats object’s keys come straight from each platform’s API — they are not normalized into a common schema, so each network exposes a different set.

NetworkPlacement-scoped?Typical fields
facebookYes (per page)fan_count, followers_count, plus daily page insights: page_media_view (Meta’s unified “Views”), page_views_total, page_post_engagements, page_daily_follows, page_daily_unfollows, page_daily_follows_unique, page_daily_unfollows_unique
linkedinYes (per organization)followerCount, shareCount, likeCount, commentCount, clickCount, engagement, allPageViews, overviewPageViews, aboutPageViews, careersPageViews, peoplePageViews, insightsPageViews
telegramYes (per channel)followers_count, channel_title, channel_username
instagramNofollowers_count, follows_count, media_count, plus per-window insights suffixed with _1d, _7d, _14d, _30d: views_*, reach_*, profile_views_*, accounts_engaged_*, total_interactions_*, website_clicks_*
threadsNofollowers_count, views, likes, replies, reposts, quotes
youtubeNosubscriberCount, viewCount, videoCount, plus per-window Analytics metrics suffixed with _7d, _28d, _90d: views_*, estimatedMinutesWatched_*, averageViewDuration_*, averageViewPercentage_*, subscribersGained_*, subscribersLost_*, likes_*, dislikes_*, comments_*, shares_*, videosAddedToPlaylists_*, videosRemovedFromPlaylists_*
twitterNofollowers_count, following_count, tweet_count, listed_count, like_count
tiktokNofollower_count, following_count, likes_count, video_count
pinterestNofollower_count, following_count, pin_count, board_count, monthly_views, analytics_30d (nested 30-day rollup)
blueskyNofollowersCount, followsCount, postsCount
google_businessYes (per location)views (all four impression metrics summed), plus the raw Business Profile Performance metrics: business_impressions_desktop_search, business_impressions_desktop_maps, business_impressions_mobile_search, business_impressions_mobile_maps, website_clicks, call_clicks, business_direction_requests, business_conversations, business_bookings, business_food_orders, business_food_menu_clicks

Notes:

  • LinkedIn page-view metrics are filtered down to the rollups (we drop redundant mobile/desktop splits and dead sections like productsPageViews / lifeAtPageViews).
  • Non-numeric fields (e.g. Telegram’s channel_title) appear in latest_stats[].stats but are omitted from summary_stats.stats, which sums numeric values only.
  • A stats key only appears in a snapshot if the platform returned a value for it on that polling cycle, so fields can come and go between records.
  • Instagram metrics vary by account. Instagram serves a different account-level metric set per account, so an absent key means that account can’t report it — not that the metric is gone.
  • Google Business has no followers. A Business Profile has no audience count, so no follower field appears. Snapshots are trailing 30-day totals per location, and Google exposes no per-post analytics for local posts. See Google Business stats.

GET /api/profiles/:id/participants/:user_id

Instagram only. Returns whether a user follows the profile’s Instagram account (and vice versa), plus their verified badge and follower count. The data comes live from Instagram’s messaging user lookup.

The user_id is the Instagram-scoped user id you receive when the user interacts with your account — the sender id on a DM chat (participant_external_id) or the author id on a comment (author_external_id). Instagram only resolves users who have messaged or commented on your account; arbitrary Instagram accounts can’t be looked up.

NameTypeRequiredDescription
idstringYesProfile id (must be an Instagram profile)
user_idstringYesInstagram-scoped user id (DM sender id or comment author id)
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof123abc/participants/1254459754837075" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"is_verified_user": false,
"is_user_follow_business": true,
"is_business_follow_user": false,
"follower_count": 1234,
"fetched_at": "2026-08-28T10:00:00Z"
}
FieldTypeDescription
is_user_follow_businessbooleanThe user follows your Instagram account
is_business_follow_userbooleanYour Instagram account follows the user
is_verified_userbooleanThe user has a verified badge
follower_countintegerThe user’s follower count
fetched_atstringISO 8601 timestamp of when the data was fetched from Instagram

Lookups are rate-limited to one Instagram call per user per second. Within that window, repeat requests return the same payload from cache. The X-Cached response header says which happened: false — fetched live from Instagram; true — served from cache (fetched_at shows the original fetch time).

Non-Instagram profile (400):

{
"error": "participant status is only supported for Instagram profiles"
}

Unknown user id (404) — Instagram can’t resolve the id, or the user is outside your account’s messaging scope:

{
"error": "Not found"
}

Instagram rate limit hit (429): retry after the Retry-After header.


GET /api/profiles/:id/resolve_mention

LinkedIn only. Turns a LinkedIn company page URL into the URN LinkedIn needs to render an @-mention. Company, showcase, and school pages all resolve; the page does not have to be one this connection administers.

Mentions are written into post text as @[Display Name](urn) — for example @[Postproxy](urn:li:organization:110810308). Without the URN the mention publishes as plain text: no link, no notification. Postproxy resolves the URN; composing the text is yours. See Mentions.

Person mentions (linkedin.com/in/...) can’t be resolved yet — person lookup is coming soon.

NameTypeRequiredDescription
idstringYesProfile id (must be a LinkedIn profile)
NameTypeRequiredDescription
urlstringYesCompany page URL (https://www.linkedin.com/company/postproxy) or just its name (postproxy)
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof_li_001/resolve_mention?url=https://www.linkedin.com/company/postproxy" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"urn": "urn:li:organization:110810308",
"type": "organization",
"name": "Postproxy",
"vanity_name": "postproxy"
}
FieldTypeDescription
urnstringLinkedIn URN to put inside @[...](...)
typestringAlways organization today
namestringThe page’s name — a sensible default for the mention’s display text
vanity_namestringThe slug that matched, to confirm the right page came back

Results are cached for 1 hour per company page, shared across profiles — a page’s URN doesn’t change, and the lookup draws on the same LinkedIn quota as publishing. Pages that resolve to nothing are cached for the same hour, so a bad URL in a loop won’t hammer LinkedIn.

Non-LinkedIn profile (400):

{
"error": "mention resolution is only supported for LinkedIn profiles"
}

Person page URL (400):

{
"error": "only organization mentions can be resolved; person pages are not supported"
}

Unknown company page (404):

{
"error": "Not found"
}

Postproxy mirrors posts published natively on a platform into your account, so they show up in history and can receive insights and comments. Every one of those pulls is recorded as a post sync: the sync fired when the profile connects, the recurring poll (every 3 hours; 23 hours on X), and any backfill you start.

FieldTypeDescription
idstringUnique sync identifier
profile_idstringProfile this run belongs to
kindstringAlways posts today
triggerstringconnect, scheduled, or backfill
statusstringpending, running, completed, or failed
started_atstring|nullISO 8601 timestamp when the run began
completed_atstring|nullISO 8601 timestamp when it finished or failed
posts_seenintegerPosts the platform returned across the run
posts_importedintegerPosts that were new and got created. Lower than posts_seen whenever the run re-read posts you already have
backfill_fromstring|nullThe date floor requested. null for connect and scheduled runs
oldest_posted_atstring|nullPublish date of the oldest post the run reached — how far back it has walked
errorstring|nullPlatform error message when status is failed
created_atstringISO 8601 timestamp

Runs are kept for 30 days. A profile’s page in the app shows the same list in its Post sync section.


POST /api/profiles/:id/backfill_posts

Walks the profile’s feed backwards from the newest post, importing in batches of 25, until it reaches from or the platform stops returning posts. Runs in the background — the response is the post sync record, which you poll for progress.

Only one backfill runs per profile at a time. A backfill always walks from now backwards, so an in-flight one already covers any window a second request could ask for; starting another returns 409 with the running one’s id.

ParameterTypeRequiredDescription
fromstringYesISO 8601 date/time, or a bare date meaning that date’s start of day. The run stops at the first post published before this
Terminal window
curl -X POST "https://api.postproxy.dev/api/profiles/prof123abc/backfill_posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "from": "2025-01-01" }'

Response (202 Accepted):

{
"id": "sync456def",
"profile_id": "prof123abc",
"kind": "posts",
"trigger": "backfill",
"status": "pending",
"started_at": null,
"completed_at": null,
"posts_seen": 0,
"posts_imported": 0,
"backfill_from": "2025-01-01T00:00:00.000Z",
"oldest_posted_at": null,
"error": null,
"created_at": "2026-08-06T09:15:00.000Z"
}
  • How far back you can reach depends on the platform, not on Postproxy. Where a platform’s API pages through history, the backfill follows it; where it doesn’t, the run ends early with whatever it got and status: "completed".
  • Imported posts arrive with source: "imported" and fire the post.imported webhook, the same as posts picked up by the recurring poll.
  • Posts you already have are skipped, so overlapping backfills are safe — they just report a posts_imported lower than posts_seen.

Missing or unparseable from (400):

{
"status": 400,
"error": "Bad Request",
"message": "from"
}

Backfill already running (409):

{
"error": "A posts backfill is already running for this profile",
"profile_sync_id": "sync456def"
}

Profile can’t be backfilled (422) — disconnected, in a rate-limit cooldown, or an X profile on a plan without pull access:

{
"error": "This profile can't be backfilled right now"
}

GET /api/profiles/:id/post_syncs

Retrieves post sync runs for a profile, newest first.

ParameterTypeRequiredDefaultDescription
triggerstringNo-Filter by connect, scheduled, or backfill
statusstringNo-Filter by pending, running, completed, or failed
pageintegerNo1Page number (1-based). Passing 0 returns page 1
per_pageintegerNo25Results per page (maximum 100)
Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof123abc/post_syncs?trigger=backfill" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"total": 1,
"page": 1,
"per_page": 25,
"data": [
{
"id": "sync456def",
"profile_id": "prof123abc",
"kind": "posts",
"trigger": "backfill",
"status": "running",
"started_at": "2026-08-06T09:15:02.000Z",
"completed_at": null,
"posts_seen": 150,
"posts_imported": 143,
"backfill_from": "2025-01-01T00:00:00.000Z",
"oldest_posted_at": "2025-11-04T18:22:00.000Z",
"error": null,
"created_at": "2026-08-06T09:15:00.000Z"
}
]
}

GET /api/profiles/:id/post_syncs/:post_sync_id

Retrieves a single run — poll this to follow a backfill to completion. Returns a post sync object; the run is finished when status is completed or failed.

Terminal window
curl -X GET "https://api.postproxy.dev/api/profiles/prof123abc/post_syncs/sync456def" \
-H "Authorization: Bearer YOUR_API_KEY"

DELETE /api/profiles/:id

Disconnects and removes a profile from the account. This does not affect posts already published through this profile.

NameTypeRequiredDescription
idstringYesProfile id
Terminal window
curl -X DELETE "https://api.postproxy.dev/api/profiles/prof123abc" \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"success": true
}

Some platforms issue access tokens that expire. The expires_at field indicates when the connection will expire and require re-authentication.

BehaviorDescription
expires_at: nullToken does not expire or has a refresh token
expires_at: "2024-..."Token expires at the specified time

When a token expires:

  • Posts to that profile will fail
  • The user needs to reconnect the profile through the web dashboard
  • Use the Initialize Connection endpoint to generate a new connection URL

Profiles cannot be created directly via the API. To connect a new social media account:

  1. Use the Initialize Connection endpoint to get an OAuth URL
  2. Redirect the user to that URL to authenticate
  3. User is redirected back to your redirect_url after authentication
  4. The profile is automatically created and associated with the profile group

When creating posts, reference profiles by:

  1. Profile ID: Use the id id directly
  2. Platform name: Use the platform string (e.g., "twitter") to automatically select the profile for that platform
{
"profiles": ["prof123abc", "twitter", "linkedin"]
}

If multiple profiles exist for the same platform in a profile group, using the platform name selects the first one. Use the profile ID for explicit selection.