API reference
16 endpoints, served at https://orbitwrite.com/api/v1. Each one lists the credentials it accepts, its parameters and every response it can give.
/api/v1/analytics/resultsGET/api/v1/analytics/overviewGET/api/v1/analytics/seriesGET/api/v1/analytics/followersGET/api/v1/analytics/best-timesGET/api/v1/analytics/postsGET/api/v1/analytics/breakdownGET/api/v1/analytics/insightsGET/api/v1/analytics/sync-statusGET/api/v1/posts/{groupId}/metricsGET/api/v1/posts/{groupId}/benchmarkGET/api/v1/campaigns/{campaignId}/reportGET/api/v1/campaigns/{campaignId}/summaryPOST/api/v1/campaigns/{campaignId}/summaryGET/api/v1/analytics/summaryPOST/api/v1/analytics/summaryThe window's published posts (newest first, at most limit, each thread counted once) with their latest numbers, the totals, the post with the most engagements, and each channel's follower change from the daily snapshots. Numbers come from the analytics history the worker keeps; a post it hasn't read yet is read from the platform. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
daysintegerHow far back to look, ending now (or at to). Defaults to 7.
Between 1 and 90.
fromstringStart of the window, an ISO instant. Overrides days.
tostringEnd of the window, an ISO instant. Defaults to now.
limitintegerMost posts to read. Defaults to 15.
Between 1 and 25.
accountIdsstring | string[]Repeat to narrow to these channels.
200WorkspaceResultsThe window's results
fromstringRequiredtostringRequiredpostsobject[]RequiredpostIdstringRequiredThe post row — what GET /posts/{postId}/analytics and the inbox's postId filter take.
groupIdstring | any | nullRequiredThe composer group — what every post-addressed route (read, score, schedule) takes.
accountIdstringRequiredplatformstringRequiredchannelstring | any | nullRequiredtextstringRequiredpublishedAtstringRequiredplatformUrlstring | any | nullRequiredstatusstringRequiredunsupported: the network reports no post metrics. no_data_yet: no platform id was captured. unavailable: the platform didn't answer (auth, rate limit, outage).
"ok""unsupported""no_data_yet""unavailable"metricsobject[]Requiredengagementsnumber | any | nullRequiredReactions, comments, reposts, quotes and saves summed. Null when the post has no numbers.
truncatedbooleanRequiredThe window held more posts than limit.
topPostIdstring | any | nullRequiredThe row id of the post with the most engagements; null when no post has positive summed engagements (none reported numbers, or every total is zero).
topGroupIdstring | any | nullRequiredThat same post's composer group — the id to read, score or reschedule it by.
historyDaysinteger | any | nullRequiredSet when the plan's analytics history cut the window short: how many days back it reaches.
curl -X GET "https://orbitwrite.com/api/v1/analytics/results" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"from": "string",
"to": "string",
"posts": [
{
"postId": "string",
"groupId": "string",
"accountId": "string",
"platform": "string",
"channel": "string",
"text": "string",
"publishedAt": "string",
"platformUrl": "string",
"status": "ok",
"metrics": [
{
"key": "string",
"label": "string",
"value": 0,
"format": "count"
}
],
"engagements": 0
}
],
"truncated": true,
"totals": [
{
"key": "string",
"label": "string",
"value": 0
}
],
"topPostId": "string",
"topGroupId": "string",
"historyDays": 0,
"followers": [
{
"accountId": "string",
"platform": "string",
"channel": "string",
"start": 0,
"end": 0,
"change": 0,
"from": "string",
"to": "string"
}
]
}Each metric summed over the posts published in the window (each post's latest numbers, a thread counted once), with the same totals for a comparison window and the change. The comparison is compareFrom/compareTo when given, else the equal stretch just before the window. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
periodstringA calendar period in timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
"this_month""last_month"daysinteger | nullHow far back to look, ending now (or at to). At most 366; the plan's analytics history may cut it shorter.
fromstringStart of the window: an ISO instant, or a YYYY-MM-DD day in timezone. Overrides days.
tostringEnd of the window: an ISO instant, or a YYYY-MM-DD day in timezone (included whole). Defaults to now.
accountIdsstring | string[]Repeat to narrow to these channels.
timezonestringIANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
campaignIdstringOnly this campaign's posts.
tagIdsstring | string[]Repeat to narrow to posts carrying any of these tags. With campaignId, posts must match both.
compareFromstringStart of the window to compare against: an ISO instant or a YYYY-MM-DD day. Defaults to the same length of time just before the window.
comparestringWithout compareFrom/compareTo: previous (the default, the same length of time just before the window) or year (the same window a year earlier).
"previous""year"compareTostringEnd of the window to compare against: an ISO instant or a YYYY-MM-DD day (included whole). Required with compareFrom.
200AnalyticsOverviewThe window's totals
fromstringRequiredtostringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history cut the window short: how many days back it reaches.
timezonestringRequiredpreviousobject | any | nullRequiredThe window compared against: compareFrom/compareTo when given, else the same length of time just before the window. Null when the plan's history doesn't reach it.
metricsobject[]RequiredmetricstringRequired"engagements""impressions""reactions""comments""reposts""quotes""saves""clicks""profileClicks""videoViews"labelstringRequiredvaluenumber | any | nullRequiredNull when no post in the window reported it.
previousnumber | any | nullRequiredchangePercentnumber | any | nullRequiredcurl -X GET "https://orbitwrite.com/api/v1/analytics/overview" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"from": "string",
"to": "string",
"historyDays": 0,
"timezone": "string",
"previous": {
"from": "string",
"to": "string"
},
"posts": {
"current": 0,
"previous": 0
},
"metrics": [
{
"metric": "engagements",
"label": "string",
"value": 0,
"previous": 0,
"changePercent": 0
}
]
}How much of a metric the workspace's posts received on each day (or week, starting Monday) of the window, posts of any age: each post's daily change between reads, summed. Everything a post earned before its first read is dated on the day it went out. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
periodstringA calendar period in timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
"this_month""last_month"daysinteger | nullHow far back to look, ending now (or at to). At most 366; the plan's analytics history may cut it shorter.
fromstringStart of the window: an ISO instant, or a YYYY-MM-DD day in timezone. Overrides days.
tostringEnd of the window: an ISO instant, or a YYYY-MM-DD day in timezone (included whole). Defaults to now.
accountIdsstring | string[]Repeat to narrow to these channels.
timezonestringIANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
campaignIdstringOnly this campaign's posts.
tagIdsstring | string[]Repeat to narrow to posts carrying any of these tags. With campaignId, posts must match both.
metricstringDefaults to engagements.
"engagements""impressions""reactions""comments""reposts""quotes""saves""clicks""profileClicks""videoViews"intervalstringDefaults to day.
"day""week"200AnalyticsSeriesOne point per day or week
fromstringRequiredtostringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history cut the window short: how many days back it reaches.
timezonestringRequiredmetricstringRequired"engagements""impressions""reactions""comments""reposts""quotes""saves""clicks""profileClicks""videoViews"intervalstringRequired"day""week"pointsobject[]RequiredtotalnumberRequiredcurl -X GET "https://orbitwrite.com/api/v1/analytics/series" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"from": "string",
"to": "string",
"historyDays": 0,
"timezone": "string",
"metric": "engagements",
"interval": "day",
"points": [
{
"date": "string",
"value": 0
}
],
"total": 0
}Each channel's follower count per day in the window (the day's last reading; channels are read four times a day, X once), with the first, the last and the change. A channel with no reading in the window is absent. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
periodstringA calendar period in timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
"this_month""last_month"daysinteger | nullHow far back to look, ending now (or at to). At most 366; the plan's analytics history may cut it shorter.
fromstringStart of the window: an ISO instant, or a YYYY-MM-DD day in timezone. Overrides days.
tostringEnd of the window: an ISO instant, or a YYYY-MM-DD day in timezone (included whole). Defaults to now.
accountIdsstring | string[]Repeat to narrow to these channels.
timezonestringIANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
200AnalyticsFollowersFollower series per channel
fromstringRequiredtostringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history cut the window short: how many days back it reaches.
timezonestringRequiredcurl -X GET "https://orbitwrite.com/api/v1/analytics/followers" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"from": "string",
"to": "string",
"historyDays": 0,
"timezone": "string",
"channels": [
{
"accountId": "string",
"platform": "string",
"channel": "string",
"points": [
{
"date": "string",
"followers": 0
}
],
"start": 0,
"end": 0,
"change": 0
}
]
}An hour-of-week heatmap of engagement (the same rows the scheduling heatmap uses), from the posts published in the window, 90 days by default. Below threshold posts it is the researched heuristic for the channels' platform, and source says so. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
periodstringA calendar period in timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
"this_month""last_month"daysinteger | nullHow far back to look, ending now (or at to). At most 366; the plan's analytics history may cut it shorter.
fromstringStart of the window: an ISO instant, or a YYYY-MM-DD day in timezone. Overrides days.
tostringEnd of the window: an ISO instant, or a YYYY-MM-DD day in timezone (included whole). Defaults to now.
accountIdsstring | string[]Repeat to narrow to these channels.
timezonestringIANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
campaignIdstringOnly this campaign's posts.
tagIdsstring | string[]Repeat to narrow to posts carrying any of these tags. With campaignId, posts must match both.
200AnalyticsBestTimesThe heatmap
fromstringRequiredtostringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history cut the window short: how many days back it reaches.
timezonestringRequiredsourcestringRequiredmeasured: from these channels' own posts. heuristic: a researched per-platform profile, used until there are threshold posts. Never describe a heuristic matrix as data about this audience.
"measured""heuristic"sampleSizeintegerRequiredthresholdintegerRequireddaysstring[]Requiredmatrixnumber[][]Required[day][row], Monday first, 0..1 with the strongest cell 1.
countsinteger[][] | any | nullRequiredPosts behind each cell (measured only). A 0 cell with 0 posts is empty, not bad.
platformstring | any | nullRequiredresearchedboolean | any | nullRequiredreasonstring | any | nullRequiredcurl -X GET "https://orbitwrite.com/api/v1/analytics/best-times" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"from": "string",
"to": "string",
"historyDays": 0,
"timezone": "string",
"source": "measured",
"sampleSize": 0,
"threshold": 0,
"days": [
"string"
],
"rows": [
{
"minutes": 0,
"label": "string"
}
],
"matrix": [
[
0
]
],
"counts": [
[
0
]
],
"platform": "string",
"researched": true,
"reason": "string"
}Posts published in the window, best first by metric (each post's channels summed, from their latest numbers), cursor-paged. A post where no channel reported the metric is left out rather than ranked as zero. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
periodstringA calendar period in timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
"this_month""last_month"daysinteger | nullHow far back to look, ending now (or at to). At most 366; the plan's analytics history may cut it shorter.
fromstringStart of the window: an ISO instant, or a YYYY-MM-DD day in timezone. Overrides days.
tostringEnd of the window: an ISO instant, or a YYYY-MM-DD day in timezone (included whole). Defaults to now.
accountIdsstring | string[]Repeat to narrow to these channels.
timezonestringIANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
campaignIdstringOnly this campaign's posts.
tagIdsstring | string[]Repeat to narrow to posts carrying any of these tags. With campaignId, posts must match both.
metricstringDefaults to engagements.
"engagements""impressions""reactions""comments""reposts""quotes""saves""clicks""profileClicks""videoViews"limitinteger | nullPosts per page, at most 50. Defaults to 20.
cursorstringnextCursor from the previous page.
200AnalyticsTopPostsA page of ranked posts
fromstringRequiredtostringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history cut the window short: how many days back it reaches.
metricstringRequired"engagements""impressions""reactions""comments""reposts""quotes""saves""clicks""profileClicks""videoViews"entriesobject[]RequiredgroupIdstring | any | nullRequiredpostIdsstring[]RequiredtextstringRequiredpublishedAtstringRequiredchannelsobject[]RequiredaccountIdstringRequiredplatformstringRequiredchannelstring | any | nullRequiredplatformUrlstring | any | nullRequiredmediaobject | any | nullRequiredThe post's first attachment, for a thumbnail, and how many it has. Null without media.
valuenumberRequiredThe ranked metric, summed over the post's channels.
metricsAnalyticsMetricValuesRequiredEach metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
engagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredreactionsnumber | any | nullRequiredcommentsnumber | any | nullRequiredrepostsnumber | any | nullRequiredquotesnumber | any | nullRequiredsavesnumber | any | nullRequiredclicksnumber | any | nullRequiredprofileClicksnumber | any | nullRequiredvideoViewsnumber | any | nullRequirednextCursorstring | any | nullRequiredcurl -X GET "https://orbitwrite.com/api/v1/analytics/posts" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"from": "string",
"to": "string",
"historyDays": 0,
"metric": "engagements",
"entries": [
{
"groupId": "string",
"postIds": [
"string"
],
"text": "string",
"publishedAt": "string",
"channels": [
{
"accountId": "string",
"platform": "string",
"channel": "string",
"platformUrl": "string"
}
],
"media": {
"url": "string",
"mimeType": "string",
"count": 0
},
"tags": [
{
"id": "string",
"name": "string",
"color": "string"
}
],
"value": 0,
"metrics": {
"engagements": 0,
"impressions": 0,
"reactions": 0,
"comments": 0,
"reposts": 0,
"quotes": 0,
"saves": 0,
"clicks": 0,
"profileClicks": 0,
"videoViews": 0
}
}
],
"nextCursor": "string"
}Posts published in the window split by platform or by format (video, thread, image carousel, single image, document, poll, quote, link, text only), each group with its post count, total and average engagements per post, impressions and engagement rate. Ranked by the average, so a busy channel or format doesn't win on volume. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
periodstringA calendar period in timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
"this_month""last_month"daysinteger | nullHow far back to look, ending now (or at to). At most 366; the plan's analytics history may cut it shorter.
fromstringStart of the window: an ISO instant, or a YYYY-MM-DD day in timezone. Overrides days.
tostringEnd of the window: an ISO instant, or a YYYY-MM-DD day in timezone (included whole). Defaults to now.
accountIdsstring | string[]Repeat to narrow to these channels.
timezonestringIANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
campaignIdstringOnly this campaign's posts.
tagIdsstring | string[]Repeat to narrow to posts carrying any of these tags. With campaignId, posts must match both.
bystringRequired"platform""format""topic"200AnalyticsBreakdownThe groups, best first
fromstringRequiredtostringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history cut the window short: how many days back it reaches.
timezonestringRequiredbystringRequired"platform""format""topic"minPostsintegerRequiredGroups with fewer posts than this are flagged few.
rowsobject[]RequiredkeystringRequiredA platform id (twitter, linkedin_page, …) or a format (video, thread, …).
labelstringRequiredpostsintegerRequiredengagementsnumber | any | nullRequiredavgEngagementsnumber | any | nullRequiredEngagements per post that reported any. What the rows are ranked by.
impressionsnumber | any | nullRequiredengagementRatenumber | any | nullRequiredEngagements ÷ impressions over the posts that report impressions, as a percentage. Null where none do (Bluesky).
fewbooleanRequiredToo few posts to judge; show it greyed rather than as a finding.
curl -X GET "https://orbitwrite.com/api/v1/analytics/breakdown?by=by" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"from": "string",
"to": "string",
"historyDays": 0,
"timezone": "string",
"by": "platform",
"minPosts": 0,
"rows": [
{
"key": "string",
"label": "string",
"posts": 0,
"engagements": 0,
"avgEngagements": 0,
"impressions": 0,
"engagementRate": 0,
"few": true
}
]
}Patterns in the posts published in the window (default the last 30 days), strongest first: which format, content type (announcements, tutorials, events, case studies, …), tone, opening-line length, post length, weekday, time of day, links, hashtags, questions and tags did better or worse than the rest. Each post is judged against its own channel's median, so a pattern isn't just the bigger channel winning. Also timing: days and times that are working, not working, or untried. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
periodstringA calendar period in timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
"this_month""last_month"daysinteger | nullHow far back to look, ending now (or at to). At most 366; the plan's analytics history may cut it shorter.
fromstringStart of the window: an ISO instant, or a YYYY-MM-DD day in timezone. Overrides days.
tostringEnd of the window: an ISO instant, or a YYYY-MM-DD day in timezone (included whole). Defaults to now.
accountIdsstring | string[]Repeat to narrow to these channels.
timezonestringIANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
campaignIdstringOnly this campaign's posts.
tagIdsstring | string[]Repeat to narrow to posts carrying any of these tags. With campaignId, posts must match both.
limitinteger | nullHow many insights, 1–10. Defaults to 3.
200AnalyticsInsightsThe insights, strongest first
fromstringRequiredtostringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history cut the window short: how many days back it reaches.
timezonestringRequiredpostsAnalysedintegerRequiredPosts published in the window with numbers to judge, each against its own channel.
minPostsintegerRequiredEither side of a comparison needs at least this many posts.
insightsobject[]RequiredidstringRequireddimensionstringRequiredWhat the posts were grouped by: format, topic (what the post is about: announcement, tutorial, event, case study, …), sentiment (its tone), hook (opening-line length), length, day, time (of day), link, hashtags, question, or tag.
"format""topic""sentiment""hook""length""day""time""link""hashtags""question""tag"groupstringRequiredThe group within the dimension: a format, a bucket, a weekday (0 = Sunday), a tag id, or yes.
directionstringRequired"better""worse"metricstringRequired"impressions""engagements"titlestringRequiredsummarystringRequiredOne sentence with the numbers.
liftnumberRequiredThe group's average against the rest's, each post measured against its channel's median: 2 is twice as well, 0.5 half.
postsintegerRequiredcomparedPostsintegerRequiredconfidencestringRequired"low""medium""high"exampleGroupIdsstring[]RequiredUp to three of the group's posts that show it best.
timingobjectRequiredWhich days and times work, which don't, and which haven't been tried, each post judged against its channel.
curl -X GET "https://orbitwrite.com/api/v1/analytics/insights" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"from": "string",
"to": "string",
"historyDays": 0,
"timezone": "string",
"postsAnalysed": 0,
"minPosts": 0,
"insights": [
{
"id": "string",
"dimension": "format",
"group": "string",
"direction": "better",
"metric": "impressions",
"title": "string",
"summary": "string",
"lift": 0,
"posts": 0,
"comparedPosts": 0,
"confidence": "low",
"exampleGroupIds": [
"string"
]
}
],
"timing": {
"days": [
{
"key": "string",
"label": "string",
"posts": 0,
"lift": 0
}
],
"times": [
{
"key": "string",
"label": "string",
"posts": 0,
"lift": 0
}
],
"best": {
"day": {
"key": "string",
"label": "string",
"posts": 0,
"lift": 0
},
"time": {
"key": "string",
"label": "string",
"posts": 0,
"lift": 0
}
},
"experiments": [
{
"kind": "weak",
"dimension": "day",
"key": "string",
"title": "string",
"summary": "string"
}
]
}
}Per channel: whether its posts are collected, the last and next post read, the last follower reading, and any pause. Tells a quiet chart from a stalled one. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
200AnalyticsSyncStatusCollection status per channel
channelsobject[]RequiredaccountIdstringRequiredplatformstringRequiredchannelstring | any | nullRequiredcollectedbooleanRequiredWhether the worker re-reads this channel's posts at all.
lastPolledAtstring | any | nullRequirednextPollAtstring | any | nullRequiredfollowersAtstring | any | nullRequiredcurl -X GET "https://orbitwrite.com/api/v1/analytics/sync-status" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"channels": [
{
"accountId": "string",
"platform": "string",
"channel": "string",
"collected": true,
"lastPolledAt": "string",
"nextPollAt": "string",
"followersAt": "string",
"paused": {
"until": "string",
"reason": "string"
}
}
],
"x": {
"enabled": true,
"budget": "open",
"resetsAt": "string"
}
}Every read of the post's numbers, per channel, oldest first: what a chart of how it grew is drawn from. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
groupIdstringRequiredorganizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
200PostMetricSeriesThe post's reads
groupIdstringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history hid this post's earlier reads.
channelsobject[]RequiredaccountIdstringRequiredpostIdstringRequiredplatformstringRequiredchannelstring | any | nullRequiredpointsobject[]RequiredfetchedAtstringRequiredmetricsobjectRequiredimpressionsnumber | any | nullRequiredreactionsnumber | any | nullRequiredcommentsnumber | any | nullRequiredrepostsnumber | any | nullRequiredquotesnumber | any | nullRequiredsavesnumber | any | nullRequiredclicksnumber | any | nullRequiredprofileClicksnumber | any | nullRequiredvideoViewsnumber | any | nullRequiredengagementRatenumber | any | nullRequiredcurl -X GET "https://orbitwrite.com/api/v1/posts/{groupId}/metrics" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"groupId": "string",
"historyDays": 0,
"channels": [
{
"accountId": "string",
"postId": "string",
"platform": "string",
"channel": "string",
"points": [
{
"fetchedAt": "string",
"metrics": {
"impressions": 0,
"reactions": 0,
"comments": 0,
"reposts": 0,
"quotes": 0,
"saves": 0,
"clicks": 0,
"profileClicks": 0,
"videoViews": 0,
"engagementRate": 0
}
}
]
}
]
}Per channel of the post: its latest engagements, impressions and engagement rate against the channel's other posts published in the 90 days before it, every channel on the same network, and posts of the same format. Peers are read as they stood at this post's age, so a two-day-old post is measured against other posts at two days, and peers too young to have reached that age are left out. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
groupIdstringRequiredorganizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
200PostBenchmarkThe post against its peers
groupIdstringRequiredhistoryDaysinteger | any | nullRequiredSet when the post is older than the plan's analytics history reaches: it then has no comparisons.
lookbackDaysintegerRequiredchannelsobject[]RequiredaccountIdstringRequiredpostIdstringRequiredplatformstringRequiredchannelstring | any | nullRequiredpublishedAtstring | any | nullRequiredageDaysinteger | any | nullRequiredmetricsobjectRequiredengagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredengagementRatenumber | any | nullRequiredcomparisonsobject[]RequiredscopestringRequiredchannel: this channel's other posts. platform: every channel of the workspace on the same network (only when there is more than one). format: this channel's posts of the same format.
"channel""platform""format"postsintegerRequiredfewbooleanRequiredToo few peers for the comparison to say much.
engagementsobject | any | nullRequiredvaluenumberRequiredThis post's number.
peersintegerRequiredPeer posts that reported this number.
mediannumberRequiredaveragenumberRequiredpercentileintegerRequiredThe share of those peers this post did better than, 0–100 (ties count half).
valuesnumber[]RequiredThe peers' own numbers, lowest first, for drawing where this post sits. Evenly thinned to at most 120.
impressionsobject | any | nullRequiredvaluenumberRequiredThis post's number.
peersintegerRequiredPeer posts that reported this number.
mediannumberRequiredaveragenumberRequiredpercentileintegerRequiredThe share of those peers this post did better than, 0–100 (ties count half).
valuesnumber[]RequiredThe peers' own numbers, lowest first, for drawing where this post sits. Evenly thinned to at most 120.
engagementRateobject | any | nullRequiredvaluenumberRequiredThis post's number.
peersintegerRequiredPeer posts that reported this number.
mediannumberRequiredaveragenumberRequiredpercentileintegerRequiredThe share of those peers this post did better than, 0–100 (ties count half).
valuesnumber[]RequiredThe peers' own numbers, lowest first, for drawing where this post sits. Evenly thinned to at most 120.
curl -X GET "https://orbitwrite.com/api/v1/posts/{groupId}/benchmark" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"groupId": "string",
"historyDays": 0,
"lookbackDays": 0,
"channels": [
{
"accountId": "string",
"postId": "string",
"platform": "string",
"channel": "string",
"publishedAt": "string",
"ageDays": 0,
"format": {
"key": "string",
"label": "string"
},
"metrics": {
"engagements": 0,
"impressions": 0,
"engagementRate": 0
},
"comparisons": [
{
"scope": "channel",
"posts": 0,
"few": true,
"engagements": {
"value": 0,
"peers": 0,
"median": 0,
"average": 0,
"percentile": 0,
"values": [
0
]
},
"impressions": {
"value": 0,
"peers": 0,
"median": 0,
"average": 0,
"percentile": 0,
"values": [
0
]
},
"engagementRate": {
"value": 0,
"peers": 0,
"median": 0,
"average": 0,
"percentile": 0,
"values": [
0
]
}
}
]
}
]
}What a campaign published and how it did: totals, its best platform and format (by average engagements per post), its top posts, its longest thread, what stood out against each channel's usual, and every published post with its numbers. Membership decides what counts, not dates, so a post that went out after the campaign ended is still in it. A null metric means nothing reported it, never zero. Requires analytics:read and campaign:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
campaignIdstringRequiredorganizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
200CampaignReportThe campaign's report
campaignobjectRequiredidstringRequiredtitlestring | any | nullRequiredstartDatestring | any | nullRequiredendDatestring | any | nullRequiredstatusstringRequiredFrom its dates: scheduled before it starts (or with no start date), active while it runs (no end date means it runs on), completed once its end date has passed.
"scheduled""active""completed"durationDaysinteger | any | nullRequiredDays from its start (or first post) to its end, or to now while it runs.
timezonestringRequiredhistoryDaysinteger | any | nullRequiredSet when the plan's analytics history doesn't reach the campaign's earliest posts: they are listed, but their numbers are left out.
minPostsintegerRequiredpublishedobjectRequiredpostsintegerRequiredPosts that went out, one per post however many channels it reached.
channelPostsintegerRequiredThe same posts counted once per channel.
measuredintegerRequiredPosts with engagement numbers from at least one channel.
firstAtstring | any | nullRequiredlastAtstring | any | nullRequiredactiveDaysintegerRequiredDistinct days, in timezone, that something went out.
pipelineobject | any | nullRequiredPosts that haven't gone out: scheduled, and everything else (drafts, awaiting approval). Null without post:read.
scheduledintegerRequiredunscheduledintegerRequiredtotalsAnalyticsMetricValuesRequiredEach metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
engagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredreactionsnumber | any | nullRequiredcommentsnumber | any | nullRequiredrepostsnumber | any | nullRequiredquotesnumber | any | nullRequiredsavesnumber | any | nullRequiredclicksnumber | any | nullRequiredprofileClicksnumber | any | nullRequiredvideoViewsnumber | any | nullRequiredengagementsPerPostnumber | any | nullRequiredplatformsCampaignBreakdownRow[]RequiredBest first by average engagements per post, rows too small to judge last.
keystringRequiredlabelstringRequiredpostsintegerRequiredengagementsnumber | any | nullRequiredavgEngagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredengagementRatenumber | any | nullRequiredfewbooleanRequiredFewer than minPosts posts: too few for the average to be a finding.
formatsCampaignBreakdownRow[]RequiredkeystringRequiredlabelstringRequiredpostsintegerRequiredengagementsnumber | any | nullRequiredavgEngagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredengagementRatenumber | any | nullRequiredfewbooleanRequiredFewer than minPosts posts: too few for the average to be a finding.
bestPlatformCampaignBreakdownRow | any | nullRequiredThe leading platform, preferring one with at least minPosts posts. Check few before calling it a finding.
keystringRequiredlabelstringRequiredpostsintegerRequiredengagementsnumber | any | nullRequiredavgEngagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredengagementRatenumber | any | nullRequiredfewbooleanRequiredFewer than minPosts posts: too few for the average to be a finding.
bestFormatCampaignBreakdownRow | any | nullRequiredkeystringRequiredlabelstringRequiredpostsintegerRequiredengagementsnumber | any | nullRequiredavgEngagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredengagementRatenumber | any | nullRequiredfewbooleanRequiredFewer than minPosts posts: too few for the average to be a finding.
topPostsCampaignReportPost[]RequiredUp to 5 posts, best first by engagements; a post with none never ranks.
groupIdstring | any | nullRequiredpostIdsstring[]RequiredtextstringRequiredpublishedAtstringRequiredchannelsobject[]RequiredaccountIdstringRequiredplatformstringRequiredchannelstring | any | nullRequiredplatformUrlstring | any | nullRequiredmediaobject | any | nullRequiredthreadLengthintegerRequiredPosts in its longest channel's thread; 1 for a single post.
metricsAnalyticsMetricValuesRequiredEach metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
engagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredreactionsnumber | any | nullRequiredcommentsnumber | any | nullRequiredrepostsnumber | any | nullRequiredquotesnumber | any | nullRequiredsavesnumber | any | nullRequiredclicksnumber | any | nullRequiredprofileClicksnumber | any | nullRequiredvideoViewsnumber | any | nullRequiredlongestThreadCampaignReportPost | any | nullRequiredThe post with the longest thread; null when nothing was a thread.
groupIdstring | any | nullRequiredpostIdsstring[]RequiredtextstringRequiredpublishedAtstringRequiredchannelsobject[]RequiredaccountIdstringRequiredplatformstringRequiredchannelstring | any | nullRequiredplatformUrlstring | any | nullRequiredmediaobject | any | nullRequiredthreadLengthintegerRequiredPosts in its longest channel's thread; 1 for a single post.
metricsAnalyticsMetricValuesRequiredEach metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
engagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredreactionsnumber | any | nullRequiredcommentsnumber | any | nullRequiredrepostsnumber | any | nullRequiredquotesnumber | any | nullRequiredsavesnumber | any | nullRequiredclicksnumber | any | nullRequiredprofileClicksnumber | any | nullRequiredvideoViewsnumber | any | nullRequiredinsightsobject[]RequiredidstringRequireddimensionstringRequiredgroupstringRequireddirectionstringRequired"better""worse"metricstringRequiredtitlestringRequiredsummarystringRequiredliftnumberRequiredpostsintegerRequiredcomparedPostsintegerRequiredconfidencestringRequired"low""medium""high"exampleGroupIdsstring[]RequiredpostsCampaignReportPost[]RequiredEvery published post, newest first, at most 200.
groupIdstring | any | nullRequiredpostIdsstring[]RequiredtextstringRequiredpublishedAtstringRequiredchannelsobject[]RequiredaccountIdstringRequiredplatformstringRequiredchannelstring | any | nullRequiredplatformUrlstring | any | nullRequiredmediaobject | any | nullRequiredthreadLengthintegerRequiredPosts in its longest channel's thread; 1 for a single post.
metricsAnalyticsMetricValuesRequiredEach metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
engagementsnumber | any | nullRequiredimpressionsnumber | any | nullRequiredreactionsnumber | any | nullRequiredcommentsnumber | any | nullRequiredrepostsnumber | any | nullRequiredquotesnumber | any | nullRequiredsavesnumber | any | nullRequiredclicksnumber | any | nullRequiredprofileClicksnumber | any | nullRequiredvideoViewsnumber | any | nullRequiredtruncatedbooleanRequiredcurl -X GET "https://orbitwrite.com/api/v1/campaigns/{campaignId}/report" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"campaign": {
"id": "string",
"title": "string",
"startDate": "string",
"endDate": "string",
"status": "scheduled",
"durationDays": 0
},
"timezone": "string",
"historyDays": 0,
"minPosts": 0,
"published": {
"posts": 0,
"channelPosts": 0,
"measured": 0,
"firstAt": "string",
"lastAt": "string",
"activeDays": 0,
"channels": [
{
"accountId": "string",
"platform": "string",
"channel": "string",
"posts": 0
}
]
},
"pipeline": {
"scheduled": 0,
"unscheduled": 0
},
"totals": {
"engagements": 0,
"impressions": 0,
"reactions": 0,
"comments": 0,
"reposts": 0,
"quotes": 0,
"saves": 0,
"clicks": 0,
"profileClicks": 0,
"videoViews": 0
},
"engagementsPerPost": 0,
"platforms": [
{
"key": "string",
"label": "string",
"posts": 0,
"engagements": 0,
"avgEngagements": 0,
"impressions": 0,
"engagementRate": 0,
"few": true
}
],
"formats": [
{
"key": "string",
"label": "string",
"posts": 0,
"engagements": 0,
"avgEngagements": 0,
"impressions": 0,
"engagementRate": 0,
"few": true
}
],
"bestPlatform": {
"key": "string",
"label": "string",
"posts": 0,
"engagements": 0,
"avgEngagements": 0,
"impressions": 0,
"engagementRate": 0,
"few": true
},
"bestFormat": {
"key": "string",
"label": "string",
"posts": 0,
"engagements": 0,
"avgEngagements": 0,
"impressions": 0,
"engagementRate": 0,
"few": true
},
"topPosts": [
{
"groupId": "string",
"postIds": [
"string"
],
"text": "string",
"publishedAt": "string",
"channels": [
{
"accountId": "string",
"platform": "string",
"channel": "string",
"platformUrl": "string"
}
],
"media": {
"url": "string",
"mimeType": "string",
"count": 0
},
"format": {
"key": "string",
"label": "string"
},
"threadLength": 0,
"metrics": {
"engagements": 0,
"impressions": 0,
"reactions": 0,
"comments": 0,
"reposts": 0,
"quotes": 0,
"saves": 0,
"clicks": 0,
"profileClicks": 0,
"videoViews": 0
}
}
],
"longestThread": {
"groupId": "string",
"postIds": [
"string"
],
"text": "string",
"publishedAt": "string",
"channels": [
{
"accountId": "string",
"platform": "string",
"channel": "string",
"platformUrl": "string"
}
],
"media": {
"url": "string",
"mimeType": "string",
"count": 0
},
"format": {
"key": "string",
"label": "string"
},
"threadLength": 0,
"metrics": {
"engagements": 0,
"impressions": 0,
"reactions": 0,
"comments": 0,
"reposts": 0,
"quotes": 0,
"saves": 0,
"clicks": 0,
"profileClicks": 0,
"videoViews": 0
}
},
"insights": [
{
"id": "string",
"dimension": "string",
"group": "string",
"direction": "better",
"metric": "string",
"title": "string",
"summary": "string",
"lift": 0,
"posts": 0,
"comparedPosts": 0,
"confidence": "low",
"exampleGroupIds": [
"string"
]
}
],
"posts": [
{
"groupId": "string",
"postIds": [
"string"
],
"text": "string",
"publishedAt": "string",
"channels": [
{
"accountId": "string",
"platform": "string",
"channel": "string",
"platformUrl": "string"
}
],
"media": {
"url": "string",
"mimeType": "string",
"count": 0
},
"format": {
"key": "string",
"label": "string"
},
"threadLength": 0,
"metrics": {
"engagements": 0,
"impressions": 0,
"reactions": 0,
"comments": 0,
"reposts": 0,
"quotes": 0,
"saves": 0,
"clicks": 0,
"profileClicks": 0,
"videoViews": 0
}
}
],
"truncated": true
}The stored summary, if one has been written, and whether it has fallen behind the campaign's numbers. Never calls a model. Requires analytics:read and campaign:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
campaignIdstringRequiredorganizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
200CampaignSummaryReadThe stored summary
summaryCampaignSummary | any | nullRequiredtextstringRequiredstatusstringRequiredThe phase it was written for: active is a running campaign's update, completed the analysis once it ended.
"scheduled""active""completed"generatedAtstringRequiredstalebooleanRequiredThe campaign's numbers or phase have moved since it was written.
summarisablebooleanRequiredWhether a summary can be written now: AI is available and at least one of the campaign's posts has numbers.
duebooleanRequiredSummarisable, and there is no summary for the phase the campaign is in. A page may write one unasked (ifMissing).
curl -X GET "https://orbitwrite.com/api/v1/campaigns/{campaignId}/summary" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"summary": {
"text": "string",
"status": "scheduled",
"generatedAt": "string",
"stale": true
},
"summarisable": true,
"due": true
}Writes a short summary from the campaign's report and stores it: an update while the campaign runs, an analysis once it has ended. Spends one AI assist. With ifMissing, returns the stored summary instead when there is one for the campaign's current phase. Requires analytics:read and campaign:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
campaignIdstringRequiredorganizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
ifMissingbooleanOnly write one when the campaign has no summary for the phase it is in.
200objectThe summary, and whether it was written just now
summaryCampaignSummaryRequiredtextstringRequiredstatusstringRequiredThe phase it was written for: active is a running campaign's update, completed the analysis once it ended.
"scheduled""active""completed"generatedAtstringRequiredstalebooleanRequiredThe campaign's numbers or phase have moved since it was written.
generatedbooleanRequired422ErrorNothing to summarise yet: none of the campaign's posts have numbers
curl -X POST "https://orbitwrite.com/api/v1/campaigns/{campaignId}/summary" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ifMissing": true
}'{
"summary": {
"text": "string",
"status": "scheduled",
"generatedAt": "string",
"stale": true
},
"generated": true
}The stored analysis of the workspace's last 28 days, if one has been written, and whether a new one is due (one a day at most). Never calls a model. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
200objectThe stored analysis
summaryWorkspaceAnalyticsSummary | any | nullRequiredtextstringRequiredfromstringRequiredStart of the window it was written about.
tostringRequiredgeneratedAtstringRequirednextAtstring | any | nullRequiredWhen it may next be rewritten (once a day at most); null when it may be now.
duebooleanRequiredNo analysis yet, or the last one is a day old, and something was published in the last 28 days: a page may write one unasked.
availablebooleanRequiredWhether this deployment can write one at all (an AI provider is configured).
curl -X GET "https://orbitwrite.com/api/v1/analytics/summary" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"summary": {
"text": "string",
"from": "string",
"to": "string",
"generatedAt": "string",
"nextAt": "string"
},
"due": true,
"available": true
}Writes a short analysis of the workspace's last 28 days against the 28 before, and stores it. At most once a day: within a day of the last one, this returns that one (generated: false) and spends nothing. Otherwise spends one AI assist. Requires analytics:read.
An API key or OAuth access token as a bearer token, or a signed-in session.
organizationIdstringSession callers only — an API key already determines the workspace.
At least 1 character.
200objectThe analysis, and whether it was written just now
summaryWorkspaceAnalyticsSummaryRequiredtextstringRequiredfromstringRequiredStart of the window it was written about.
tostringRequiredgeneratedAtstringRequirednextAtstring | any | nullRequiredWhen it may next be rewritten (once a day at most); null when it may be now.
generatedbooleanRequired422ErrorNothing to analyse: no posts with numbers in the last 28 days
curl -X POST "https://orbitwrite.com/api/v1/analytics/summary" \
-H "Authorization: Bearer $ORBITWRITE_API_KEY"{
"summary": {
"text": "string",
"from": "string",
"to": "string",
"generatedAt": "string",
"nextAt": "string"
},
"generated": true
}