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.
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.channelfilters results, not media. scheduledcounts results that have not started sending yet.successRateispublished / (published + failed).failuresByClasscounts failed results by their failure class.scheduleAdherencecovers 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).mediaPreflightcounts uploads checked against at least one platform and how many a platform objected to.- A ratio or percentile with nothing to measure is
null, never0.
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.)
groupIdis the post. Rows with the samegroupIdare one post on several channels.textis 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 isnullfor a post with no text.scopesays where the number was counted:appview_global,home_instance,relay_set,channel_admin_pushorfirst_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.
firstSixHoursholds the hourly readings from the first six hours after posting. Platforms keep no such history, so this is sampled by OmniPosts.lifetimeholds 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
/extendedcall 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.
totalis the sum, over the posts that have a reading, of each post's latest value.postsObservedsays how many posts that was.interactionsis reactions + comments + reposts + quotes + shares + saves.rateisinteractionsdivided bydenominator, which is impressions if the platform has them, otherwise views. It is worked out only over theengagement.postsposts that carry the denominator, so the two are never mixed. With no denominator,rateisnull.topPostshas 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.