Docs › API reference › Reports

Reports API: delivery and engagement numbers

Read delivery reports from OmniPosts and engagement numbers from platforms, and how Not available always comes with a reason.

Updated

There are two kinds of report:

  • Delivery is about OmniPosts's own work: what you sent, what posted, what failed and how on time it was. It is computed from OmniPosts's records, so it is always available.
  • Metrics are counters the platforms report, such as reactions and reposts. A platform may not share a number, and OmniPosts will not invent one. See Not available, and why.

Common query parameters

Parameter Meaning
from, to The window, as UTC instants. The default is the last 30 days. from must be before to, and the window can be at most 400 days. Otherwise 400 invalid_request
channel Only this channel id. An id that is not in your organization is 404 on the audience report

All of these are GET requests and need no special permission, so a key works.

GET /v1/reports/delivery

Delivery report

GET /v1/reports/delivery counts results in the window and shows how reliable each channel was.

  • A result is in the window when its post's scheduledFor (or its creation time, for a post sent at once) falls in [from, to). Media is counted by upload time. channel filters results, not media.
  • scheduled counts results that have not started sending yet.
  • successRate is published / (published + failed). failuresByClass counts failed results by their failure class.
  • scheduleAdherence covers published results that had a time: how many seconds after the scheduled time they went out (p50Seconds, p95Seconds), how many were more than 5 minutes late, and how many scheduled results are over 5 minutes past their time and neither published nor cancelled (missed).
  • mediaPreflight counts uploads checked against at least one platform and how many a platform objected to.
  • A ratio or percentile with nothing to measure is null, never 0.
curl "https://omniposts.dev/v1/reports/delivery?from=2026-09-14T00:00:00Z&to=2026-10-14T00:00:00Z" \
  -H "Authorization: Bearer omniposts_..."
{
  "window": { "from": "2026-09-14T00:00:00Z", "to": "2026-10-14T00:00:00Z" },
  "totals": { "groups": 12, "targets": 30, "published": 27, "failed": 2, "cancelled": 0, "scheduled": 1 },
  "byChannel": [
    {
      "channel": "0192a3b4-0000-7000-8000-000000000001",
      "provider": "bluesky",
      "published": 14,
      "failed": 1,
      "successRate": 0.9333,
      "failuresByClass": { "RECONNECT": 0, "RETRYABLE": 1, "UNKNOWN": 0, "USER_ERROR": 0, "PLATFORM_BLOCKED": 0 },
      "scheduleAdherence": { "p50Seconds": 2, "p95Seconds": 41, "lateOver5min": 0, "missed": 0 },
      "cadence": { "postsPerWeek": 3.5 }
    }
  ],
  "mediaPreflight": { "attempted": 6, "rejected": 1, "rate": 0.1667 }
}

GET /v1/metrics/summary

Metrics summary

GET /v1/metrics/summary has one row per published result in the window, newest first. It takes the common parameters plus limit and cursor (Pagination) and metrics.

metrics Rows carry
default (the default) Five metrics, in this order: reactions, reposts, comments, quotes, zaps
extended All twelve post metrics: reactions, comments, reposts, quotes, shares, saves, zaps, views, impressions, reach, link_clicks, profile_visits

Any other value is 400 invalid_request.

(metrics is shortened. A real row lists every metric for its mode.)

  • groupId is the post. Rows with the same groupId are one post on several channels.
  • text is what the post said on that channel: its own text, else the post's. Spaces are collapsed and it is cut to 140 characters, with … counted in the 140. It is null for a post with no text.
  • scope says where the number was counted: appview_global, home_instance, relay_set, channel_admin_push or first_party. It is a footnote so you can say honestly what a number covers.
{
  "items": [
    {
      "targetId": "0192a3b4-0000-7000-8000-0000000000f2",
      "channelId": "0192a3b4-0000-7000-8000-000000000001",
      "providerId": "bluesky",
      "publishedAt": "2026-10-12T09:30:04Z",
      "externalUrl": "https://example.invalid/post/1",
      "groupId": "0192a3b4-0000-7000-8000-0000000000f1",
      "text": "We shipped version 2.",
      "scope": "appview_global",
      "metrics": [
        { "metric": "reactions", "value": 12, "availability": "observed", "reason": null, "reasonKind": null, "observedAt": "2026-10-14T08:00:00Z" },
        { "metric": "zaps", "value": null, "availability": "not_supported", "reason": "...", "reasonKind": "platform_hides", "observedAt": null }
      ]
    }
  ],
  "nextCursor": null
}

GET /v1/metrics/post/{targetId}

Metrics for one result

GET /v1/metrics/post/{targetId} returns the numbers for one published result over time. The id is a target id. GET /v1/metrics/post/{targetId}/extended does the same for all twelve metrics. An id that is not yours is 404.

  • firstSixHours holds the hourly readings from the first six hours after posting. Platforms keep no such history, so this is sampled by OmniPosts. lifetime holds every reading: hourly for the first 48 hours, then daily.
  • Values are cumulative totals at that moment. Subtract neighbouring points to get the change.
  • The default call lists the five default metrics. The /extended call lists all twelve.
{
  "firstSixHours": [
    {
      "metric": "reactions",
      "points": [ { "at": "2026-10-12T10:30:04Z", "value": 3 } ],
      "availability": "observed",
      "reason": null,
      "reasonKind": null
    }
  ],
  "lifetime": [ ]
}

GET /v1/reports/performance

Performance report

GET /v1/reports/performance rolls the numbers up per channel and lists the top posts. It always includes all twelve metrics.

  • total is the sum, over the posts that have a reading, of each post's latest value. postsObserved says how many posts that was.
  • interactions is reactions + comments + reposts + quotes + shares + saves.
  • rate is interactions divided by denominator, which is impressions if the platform has them, otherwise views. It is worked out only over the engagement.posts posts that carry the denominator, so the two are never mixed. With no denominator, rate is null.
  • topPosts has up to five posts by interactions, newest first on ties.
{
  "window": { "from": "2026-09-14T00:00:00Z", "to": "2026-10-14T00:00:00Z" },
  "channels": [
    {
      "channel": "0192a3b4-0000-7000-8000-000000000001",
      "provider": "bluesky",
      "posts": 15,
      "metrics": [
        { "metric": "reactions", "total": 120, "postsObserved": 15, "availability": "observed", "reason": null, "reasonKind": null }
      ],
      "engagement": { "interactions": 140, "denominator": "impressions", "denominatorTotal": 3000, "rate": 0.0467, "posts": 10 }
    }
  ],
  "topPosts": [
    { "targetId": "...", "channelId": "...", "providerId": "bluesky", "publishedAt": "2026-10-12T09:30:04Z", "externalUrl": null, "groupId": "...", "text": "We shipped version 2.", "interactions": 42, "denominator": "impressions", "denominatorValue": 900 }
  ]
}

GET /v1/reports/audience

Audience report

GET /v1/reports/audience returns a daily history of each channel's account numbers. Platforms do not keep this history, so OmniPosts samples it once a day per channel and this is the only record.

Each channel lists four metrics: followers, following, posts and likes_received. change is last minus first, or null with fewer than two points. Without channel, the report covers connected channels and any disconnected ones that left data in the window.

{
  "window": { "from": "2026-09-14T00:00:00Z", "to": "2026-10-14T00:00:00Z" },
  "channels": [
    {
      "channel": "0192a3b4-0000-7000-8000-000000000001",
      "provider": "bluesky",
      "metrics": [
        {
          "metric": "followers",
          "points": [ { "at": "2026-09-14T00:00:00Z", "value": 410 }, { "at": "2026-10-13T00:00:00Z", "value": 452 } ],
          "first": 410,
          "last": 452,
          "change": 42,
          "availability": "observed",
          "reason": null,
          "reasonKind": null
        }
      ]
    }
  ]
}

Not available, and why

OmniPosts never shows 0 for a number it does not have. Every metric has an availability:

availability Meaning value / total
observed OmniPosts has a reading The latest value, with observedAt
not_yet_collected A reading is expected but not in yet. Show "collecting" null
not_supported The platform does not report it, or OmniPosts has not confirmed it is reliable null. Show "Not available" and the reason

When a metric is not_supported, reason is a sentence for a person and reasonKind is a machine-readable kind, so you can group or style these cells without reading the sentence. reasonKind is null when the number is available or still being collected.

reasonKind Meaning
platform_hides The platform does not share this number with apps
not_collected OmniPosts does not collect this number yet
unverified Not yet confirmed as reliable. Every supported counter stays here while that platform's metrics switch is off
collection_stopped Collection ended for this post
window_closed The platform shares it only for a limited time, and that time passed. The last value seen is kept
private_post Not reported for private posts
reconnect Reconnect the account and allow statistics
paid_plan Only on the platform's paid plan
business_only Only for business accounts connected through the platform's business programme

Providers shows the same supported, reason and reasonKind per platform and per metric, in analytics (post metrics) and accountAnalytics (the audience metrics), before any post exists. For the idea behind this, see Honest numbers.