Mastodon and Bluesky Statistics
Engagement counters for published Mastodon and Bluesky posts, and follower counts for the connected accounts. Values are read from the platform when you ask for them and cached for about 2 hours.
Two endpoints:
POST https://api.publora.com/api/v1/post-statistics
POST https://api.publora.com/api/v1/profile-statisticsBoth require a plan that includes analytics (Pro or Premium). A Starter key receives 403 ANALYTICS_PLAN_REQUIRED.
LinkedIn analytics live on separate endpoints with a different contract — see LinkedIn Statistics.
Headers
| Header | Required | Description |
|---|---|---|
x-publora-key |
Yes | Your API key |
Content-Type |
Yes | application/json |
What can be queried
- Only posts Publora published for you. A
postedIdthat does not belong to one of your own published posts is answerednullwithout contacting the platform. That includes posts published from another account, invented IDs, and posts Publora published before it began storing platform IDs for these two platforms (2026-09-07) — those older posts have nopostedIdand cannot be queried. - Threads: the root post only.
GET /get-postreturns onepostedIdper platform post, which for a thread is the first part. The remaining parts are not addressable. - Take
postedIdfromGET /get-postorGET /list-posts, andplatformIdfromGET /platform-connections.
Post Statistics
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
posts |
object[] | Yes | 1–50 entries. Each entry is one post on one connection. |
posts[].platform |
string | Yes | mastodon or bluesky |
posts[].platformId |
string | Yes | Connection ID, with or without the platform prefix: bluesky-did:plc:abc123 or did:plc:abc123 |
posts[].postedId |
string | Yes | Platform post ID: a Bluesky AT-URI (at://did:plc:.../app.bsky.feed.post/3k...) or a Mastodon status ID (117232239423110999) |
One request may mix both platforms and several connections. Every string is capped at 512 characters.
{
"posts": [
{
"platform": "bluesky",
"platformId": "bluesky-did:plc:3xcxmi4aiok5zyghylsa4dzw",
"postedId": "at://did:plc:3xcxmi4aiok5zyghylsa4dzw/app.bsky.feed.post/3muxmedtwxd2k"
},
{
"platform": "mastodon",
"platformId": "mastodon-110300915972205108",
"postedId": "117232239423110999"
}
]
}Response
{
"success": true,
"stats": {
"at://did:plc:3xcxmi4aiok5zyghylsa4dzw/app.bsky.feed.post/3muxmedtwxd2k": {
"reactions": 42,
"comments": 3,
"reposts": 7,
"quotes": 1,
"saves": 2,
"impressions": null,
"reach": null,
"clicks": null
},
"117232239423110999": null
}
}| Field | Type | Description |
|---|---|---|
stats |
object | Keyed by postedId. The value is a metrics object, or null when there is no data right now. |
rateLimited |
boolean | Present and true only when at least one connection was in a rate-limit cooldown |
issues |
object | Present only when non-empty. Keyed by connection ID in the <platform>-<platformId> form, value is one of the codes below. |
null in stats is not an error. It means: the post was deleted on the platform, the ID is not one of your published posts, or the connection hit one of the conditions reported in issues. Retry later rather than treating it as a permanent zero.
Metrics
Every key is always present; a metric the platform does not expose is null, never 0.
| Field | Mastodon | Bluesky |
|---|---|---|
reactions |
Favourites | Likes |
comments |
Replies | Replies |
reposts |
Boosts | Reposts |
quotes |
Quotes (Mastodon 4.5+, otherwise null) |
Quote posts |
saves |
null |
Bookmarks |
impressions |
null |
null |
reach |
null |
null |
clicks |
null |
null |
Connection issues
| Code | Meaning | What to do |
|---|---|---|
CONNECTION_NOT_FOUND |
No connection of yours matches that platform + platformId |
Re-read GET /platform-connections |
AUTH_REVOKED |
Mastodon rejected the stored token (HTTP 401) | Reconnect the account; the connection is also flagged in GET /platform-connections |
FORBIDDEN |
Mastodon refused analytics access (HTTP 403, scope or moderation). Publishing is unaffected. | Reconnect if it persists; otherwise retry later |
RATE_LIMITED |
The platform is in a cooldown after answering 429 | Retry later. Mastodon cooldowns are per connection; Bluesky cooldowns apply to the whole platform. |
FETCH_FAILED |
A transient failure reaching the platform | Retry later |
Profile Statistics
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
platform |
string | Yes | mastodon or bluesky |
platformId |
string | Yes | Connection ID, prefix optional |
{
"platform": "mastodon",
"platformId": "mastodon-110300915972205108"
}Response
{
"success": true,
"profile": {
"followers": 1234,
"following": 321,
"posts": 987
},
"cached": true,
"fetchedAt": "2026-09-08T09:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
profile |
object | null | followers, following, posts. Each value is a number or null. profile itself is null when the account could not be read. |
cached |
boolean | true when the answer came from the ~2-hour cache |
fetchedAt |
string | null | ISO 8601 timestamp of the value that was returned |
rateLimited |
boolean | Present and true only during a cooldown |
unavailable |
string | Present only when the connection is marked unavailable: AUTH_REVOKED or FORBIDDEN |
Unlike post statistics, an unknown connection is a hard error here: 404 <platform> connection not found.
Caching and load
- Post and profile values are cached for about 2 hours per connection. Two Publora users who connected the same account share that cache.
- A post that no longer exists on the platform is remembered as missing for the same period.
- After a platform answers
429, Publora stops calling it until the reset time it reported (at most one hour) and answersrateLimited: truein the meantime.
Examples
JavaScript (fetch)
const response = await fetch('https://api.publora.com/api/v1/post-statistics', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
posts: [
{
platform: 'bluesky',
platformId: 'bluesky-did:plc:3xcxmi4aiok5zyghylsa4dzw',
postedId: 'at://did:plc:3xcxmi4aiok5zyghylsa4dzw/app.bsky.feed.post/3muxmedtwxd2k'
}
]
})
});
const data = await response.json();
for (const [postedId, metrics] of Object.entries(data.stats)) {
if (!metrics) {
console.log(`${postedId}: no data right now`);
continue;
}
console.log(`${postedId}: ${metrics.reactions} likes, ${metrics.comments} replies`);
}Python (requests)
import requests
response = requests.post(
'https://api.publora.com/api/v1/post-statistics',
headers={
'Content-Type': 'application/json',
'x-publora-key': 'YOUR_API_KEY'
},
json={
'posts': [
{
'platform': 'mastodon',
'platformId': 'mastodon-110300915972205108',
'postedId': '117232239423110999'
}
]
}
)
data = response.json()
for posted_id, metrics in data['stats'].items():
if metrics is None:
print(f'{posted_id}: no data right now')
else:
print(f"{posted_id}: {metrics['reactions']} favourites, {metrics['reposts']} boosts")
for connection_id, code in data.get('issues', {}).items():
print(f'{connection_id} needs attention: {code}')cURL
curl -X POST https://api.publora.com/api/v1/post-statistics \
-H "Content-Type: application/json" \
-H "x-publora-key: YOUR_API_KEY" \
-d '{
"posts": [
{
"platform": "bluesky",
"platformId": "bluesky-did:plc:3xcxmi4aiok5zyghylsa4dzw",
"postedId": "at://did:plc:3xcxmi4aiok5zyghylsa4dzw/app.bsky.feed.post/3muxmedtwxd2k"
}
]
}'curl -X POST https://api.publora.com/api/v1/profile-statistics \
-H "Content-Type: application/json" \
-H "x-publora-key: YOUR_API_KEY" \
-d '{
"platform": "mastodon",
"platformId": "mastodon-110300915972205108"
}'Node.js (axios) — stats for a page of published posts
const axios = require('axios');
const client = axios.create({
baseURL: 'https://api.publora.com/api/v1',
headers: { 'x-publora-key': process.env.PUBLORA_API_KEY }
});
async function statsForPublishedPage() {
const { data: list } = await client.get('/list-posts', {
params: { status: 'published', limit: 25 }
});
const posts = list.posts
.flatMap((group) => group.posts || [])
.filter((post) => ['mastodon', 'bluesky'].includes(post.platform) && post.postedId)
.map((post) => ({
platform: post.platform,
platformId: post.platformId,
postedId: post.postedId
}));
if (posts.length === 0) return {};
// Never send more than 50 in one request.
const { data } = await client.post('/post-statistics', { posts: posts.slice(0, 50) });
if (data.issues) console.warn('Connections needing attention:', data.issues);
return data.stats;
}Errors
| Status | Error | Cause |
|---|---|---|
| 400 | "posts array is required" |
posts missing, not an array, or empty |
| 400 | "Maximum 50 posts per request" |
More than 50 entries in posts |
| 400 | "Each post must have string platform, platformId and postedId" |
An entry is missing a field, has a non-string value, or exceeds 512 characters |
| 400 | "platform and platformId are required" |
Profile statistics called without both fields |
| 400 | "Unsupported platform \"x\". Supported: mastodon, bluesky" |
platform is neither mastodon nor bluesky |
| 401 | "Invalid API key" |
Bad or missing x-publora-key |
| 403 | "Analytics requires a Pro or Premium plan" |
Code ANALYTICS_PLAN_REQUIRED; the plan has no analytics feature |
| 404 | "mastodon connection not found" |
Profile statistics only — no connection of yours matches. Post statistics report this as issues[…] = "CONNECTION_NOT_FOUND" with 200. |
| 504 | "Analytics request timed out" |
Code ANALYTICS_REQUEST_TIMEOUT; the request exceeded its 90-second budget. Retry with fewer posts. |
| 500 | "Failed to fetch post statistics" |
Unexpected server error |
| 500 | "Failed to fetch profile statistics" |
Unexpected server error |
Per-connection failures are not errors: the response stays 200 and reports them in issues (post statistics) or unavailable / rateLimited (profile statistics).
MCP
The same two operations are available over the MCP server as post_stats and profile_stats — see the MCP Tools Reference.
Publora is built by Creative Content Crafts, Inc. Need AI-powered content creation for LinkedIn, Threads, and X? Try Co.Actor — the best AI service for authentic thought leadership at scale.