Mastodon and Bluesky Statistics

View as Markdown

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-statistics

Both 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 postedId that does not belong to one of your own published posts is answered null without 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 no postedId and cannot be queried.
  • Threads: the root post only. GET /get-post returns one postedId per platform post, which for a thread is the first part. The remaining parts are not addressable.
  • Take postedId from GET /get-post or GET /list-posts, and platformId from GET /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 answers rateLimited: true in 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.