# Analytics

16 endpoints.

## How the workspace's posts did over a window

```http
GET https://orbitwrite.com/api/v1/analytics/results
```

The 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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `days` (integer, optional) — How far back to look, ending now (or at `to`). Defaults to 7.
- `from` (string, optional) — Start of the window, an ISO instant. Overrides `days`.
- `to` (string, optional) — End of the window, an ISO instant. Defaults to now.
- `limit` (integer, optional) — Most posts to read. Defaults to 15.
- `accountIds` (string | string[], optional) — Repeat to narrow to these channels.

### Responses

#### 200

The window's results

- `from` (string, required)
- `to` (string, required)
- `posts` (object[], required)
  - `postId` (string, required) — The post row — what `GET /posts/{postId}/analytics` and the inbox's `postId` filter take.
  - `groupId` (string | any | null, required) — The composer group — what every post-addressed route (read, score, schedule) takes.
  - `accountId` (string, required)
  - `platform` (string, required)
  - `channel` (string | any | null, required)
  - `text` (string, required)
  - `publishedAt` (string, required)
  - `platformUrl` (string | any | null, required)
  - `status` (string, required, one of `ok`, `unsupported`, `no_data_yet`, `unavailable`) — `unsupported`: the network reports no post metrics. `no_data_yet`: no platform id was captured. `unavailable`: the platform didn't answer (auth, rate limit, outage).
  - `metrics` (object[], required)
    - `key` (string, required)
    - `label` (string, required)
    - `value` (number, required)
    - `format` (string, required, one of `count`, `percent`)
  - `engagements` (number | any | null, required) — Reactions, comments, reposts, quotes and saves summed. Null when the post has no numbers.
- `truncated` (boolean, required) — The window held more posts than `limit`.
- `totals` (object[], required)
  - `key` (string, required)
  - `label` (string, required)
  - `value` (number, required)
- `topPostId` (string | any | null, required) — The 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).
- `topGroupId` (string | any | null, required) — That same post's composer group — the id to read, score or reschedule it by.
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history cut the window short: how many days back it reaches.
- `followers` (object[], required)
  - `accountId` (string, required)
  - `platform` (string, required)
  - `channel` (string | any | null, required)
  - `start` (number, required)
  - `end` (number, required)
  - `change` (number, required)
  - `from` (string, required)
  - `to` (string, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Totals for the window, against the window before

```http
GET https://orbitwrite.com/api/v1/analytics/overview
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `period` (string, optional) — A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.
- `days` (integer | null, optional) — How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.
- `from` (string, optional) — Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.
- `to` (string, optional) — End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.
- `accountIds` (string | string[], optional) — Repeat to narrow to these channels.
- `timezone` (string, optional) — IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
- `campaignId` (string, optional) — Only this campaign's posts.
- `tagIds` (string | string[], optional) — Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.
- `compareFrom` (string, optional) — Start 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.
- `compare` (string, optional) — Without `compareFrom`/`compareTo`: `previous` (the default, the same length of time just before the window) or `year` (the same window a year earlier).
- `compareTo` (string, optional) — End of the window to compare against: an ISO instant or a YYYY-MM-DD day (included whole). Required with `compareFrom`.

### Responses

#### 200

The window's totals

- `from` (string, required)
- `to` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history cut the window short: how many days back it reaches.
- `timezone` (string, required)
- `previous` (object | any | null, required) — The 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.
- `posts` (object, required)
  - `current` (integer, required)
  - `previous` (integer | any | null, required)
- `metrics` (object[], required)
  - `metric` (string, required, one of `engagements`, `impressions`, `reactions`, `comments`, `reposts`, `quotes`, `saves`, `clicks`, `profileClicks`, `videoViews`)
  - `label` (string, required)
  - `value` (number | any | null, required) — Null when no post in the window reported it.
  - `previous` (number | any | null, required)
  - `changePercent` (number | any | null, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## A metric received per day or week

```http
GET https://orbitwrite.com/api/v1/analytics/series
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `period` (string, optional) — A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.
- `days` (integer | null, optional) — How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.
- `from` (string, optional) — Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.
- `to` (string, optional) — End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.
- `accountIds` (string | string[], optional) — Repeat to narrow to these channels.
- `timezone` (string, optional) — IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
- `campaignId` (string, optional) — Only this campaign's posts.
- `tagIds` (string | string[], optional) — Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.
- `metric` (string, optional) — Defaults to `engagements`.
- `interval` (string, optional) — Defaults to `day`.

### Responses

#### 200

One point per day or week

- `from` (string, required)
- `to` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history cut the window short: how many days back it reaches.
- `timezone` (string, required)
- `metric` (string, required, one of `engagements`, `impressions`, `reactions`, `comments`, `reposts`, `quotes`, `saves`, `clicks`, `profileClicks`, `videoViews`)
- `interval` (string, required, one of `day`, `week`)
- `points` (object[], required)
  - `date` (string, required) — YYYY-MM-DD in `timezone`; a week's Monday.
  - `value` (number, required)
- `total` (number, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Followers per channel over time

```http
GET https://orbitwrite.com/api/v1/analytics/followers
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `period` (string, optional) — A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.
- `days` (integer | null, optional) — How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.
- `from` (string, optional) — Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.
- `to` (string, optional) — End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.
- `accountIds` (string | string[], optional) — Repeat to narrow to these channels.
- `timezone` (string, optional) — IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.

### Responses

#### 200

Follower series per channel

- `from` (string, required)
- `to` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history cut the window short: how many days back it reaches.
- `timezone` (string, required)
- `channels` (object[], required)
  - `accountId` (string, required)
  - `platform` (string, required)
  - `channel` (string | any | null, required)
  - `points` (object[], required)
    - `date` (string, required)
    - `followers` (number, required)
  - `start` (number, required)
  - `end` (number, required)
  - `change` (number, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## When the channels' posts do best

```http
GET https://orbitwrite.com/api/v1/analytics/best-times
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `period` (string, optional) — A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.
- `days` (integer | null, optional) — How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.
- `from` (string, optional) — Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.
- `to` (string, optional) — End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.
- `accountIds` (string | string[], optional) — Repeat to narrow to these channels.
- `timezone` (string, optional) — IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
- `campaignId` (string, optional) — Only this campaign's posts.
- `tagIds` (string | string[], optional) — Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.

### Responses

#### 200

The heatmap

- `from` (string, required)
- `to` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history cut the window short: how many days back it reaches.
- `timezone` (string, required)
- `source` (string, required, one of `measured`, `heuristic`) — `measured`: 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.
- `sampleSize` (integer, required)
- `threshold` (integer, required)
- `days` (string[], required)
- `rows` (object[], required)
  - `minutes` (integer, required)
  - `label` (string, required)
- `matrix` (number[][], required) — [day][row], Monday first, 0..1 with the strongest cell 1.
- `counts` (integer[][] | any | null, required) — Posts behind each cell (measured only). A 0 cell with 0 posts is empty, not bad.
- `platform` (string | any | null, required)
- `researched` (boolean | any | null, required)
- `reason` (string | any | null, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Posts ranked by a metric

```http
GET https://orbitwrite.com/api/v1/analytics/posts
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `period` (string, optional) — A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.
- `days` (integer | null, optional) — How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.
- `from` (string, optional) — Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.
- `to` (string, optional) — End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.
- `accountIds` (string | string[], optional) — Repeat to narrow to these channels.
- `timezone` (string, optional) — IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
- `campaignId` (string, optional) — Only this campaign's posts.
- `tagIds` (string | string[], optional) — Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.
- `metric` (string, optional) — Defaults to `engagements`.
- `limit` (integer | null, optional) — Posts per page, at most 50. Defaults to 20.
- `cursor` (string, optional) — `nextCursor` from the previous page.

### Responses

#### 200

A page of ranked posts

- `from` (string, required)
- `to` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history cut the window short: how many days back it reaches.
- `metric` (string, required, one of `engagements`, `impressions`, `reactions`, `comments`, `reposts`, `quotes`, `saves`, `clicks`, `profileClicks`, `videoViews`)
- `entries` (object[], required)
  - `groupId` (string | any | null, required)
  - `postIds` (string[], required)
  - `text` (string, required)
  - `publishedAt` (string, required)
  - `channels` (object[], required)
    - `accountId` (string, required)
    - `platform` (string, required)
    - `channel` (string | any | null, required)
    - `platformUrl` (string | any | null, required)
  - `media` (object | any | null, required) — The post's first attachment, for a thumbnail, and how many it has. Null without media.
  - `tags` (object[], required)
    - `id` (string, required)
    - `name` (string, required)
    - `color` (string, required)
  - `value` (number, required) — The ranked metric, summed over the post's channels.
  - `metrics` (AnalyticsMetricValues, required) — Each metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
    - `engagements` (number | any | null, required)
    - `impressions` (number | any | null, required)
    - `reactions` (number | any | null, required)
    - `comments` (number | any | null, required)
    - `reposts` (number | any | null, required)
    - `quotes` (number | any | null, required)
    - `saves` (number | any | null, required)
    - `clicks` (number | any | null, required)
    - `profileClicks` (number | any | null, required)
    - `videoViews` (number | any | null, required)
- `nextCursor` (string | any | null, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Which platform, or which kind of post, does best

```http
GET https://orbitwrite.com/api/v1/analytics/breakdown
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `period` (string, optional) — A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.
- `days` (integer | null, optional) — How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.
- `from` (string, optional) — Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.
- `to` (string, optional) — End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.
- `accountIds` (string | string[], optional) — Repeat to narrow to these channels.
- `timezone` (string, optional) — IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
- `campaignId` (string, optional) — Only this campaign's posts.
- `tagIds` (string | string[], optional) — Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.
- `by` (string, required)

### Responses

#### 200

The groups, best first

- `from` (string, required)
- `to` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history cut the window short: how many days back it reaches.
- `timezone` (string, required)
- `by` (string, required, one of `platform`, `format`, `topic`)
- `minPosts` (integer, required) — Groups with fewer posts than this are flagged `few`.
- `rows` (object[], required)
  - `key` (string, required) — A platform id (`twitter`, `linkedin_page`, …) or a format (`video`, `thread`, …).
  - `label` (string, required)
  - `posts` (integer, required)
  - `engagements` (number | any | null, required)
  - `avgEngagements` (number | any | null, required) — Engagements per post that reported any. What the rows are ranked by.
  - `impressions` (number | any | null, required)
  - `engagementRate` (number | any | null, required) — Engagements ÷ impressions over the posts that report impressions, as a percentage. Null where none do (Bluesky).
  - `few` (boolean, required) — Too few posts to judge; show it greyed rather than as a finding.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## What's working: the strongest patterns in the window's posts

```http
GET https://orbitwrite.com/api/v1/analytics/insights
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `period` (string, optional) — A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.
- `days` (integer | null, optional) — How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.
- `from` (string, optional) — Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.
- `to` (string, optional) — End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.
- `accountIds` (string | string[], optional) — Repeat to narrow to these channels.
- `timezone` (string, optional) — IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.
- `campaignId` (string, optional) — Only this campaign's posts.
- `tagIds` (string | string[], optional) — Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.
- `limit` (integer | null, optional) — How many insights, 1–10. Defaults to 3.

### Responses

#### 200

The insights, strongest first

- `from` (string, required)
- `to` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history cut the window short: how many days back it reaches.
- `timezone` (string, required)
- `postsAnalysed` (integer, required) — Posts published in the window with numbers to judge, each against its own channel.
- `minPosts` (integer, required) — Either side of a comparison needs at least this many posts.
- `insights` (object[], required)
  - `id` (string, required)
  - `dimension` (string, required, one of `format`, `topic`, `sentiment`, `hook`, `length`, `day`, `time`, `link`, `hashtags`, `question`, `tag`) — What 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.
  - `group` (string, required) — The group within the dimension: a format, a bucket, a weekday (0 = Sunday), a tag id, or `yes`.
  - `direction` (string, required, one of `better`, `worse`)
  - `metric` (string, required, one of `impressions`, `engagements`)
  - `title` (string, required)
  - `summary` (string, required) — One sentence with the numbers.
  - `lift` (number, required) — The group's average against the rest's, each post measured against its channel's median: 2 is twice as well, 0.5 half.
  - `posts` (integer, required)
  - `comparedPosts` (integer, required)
  - `confidence` (string, required, one of `low`, `medium`, `high`)
  - `exampleGroupIds` (string[], required) — Up to three of the group's posts that show it best.
- `timing` (object, required) — Which days and times work, which don't, and which haven't been tried, each post judged against its channel.
  - `days` (object[], required) — Each weekday, Sunday first: its posts, and their engagement against the rest (null with too few posts to judge).
    - `key` (string, required)
    - `label` (string, required)
    - `posts` (integer, required)
    - `lift` (number | any | null, required)
  - `times` (object[], required) — Each time of day (night, morning, afternoon, evening), the same way.
    - `key` (string, required)
    - `label` (string, required)
    - `posts` (integer, required)
    - `lift` (number | any | null, required)
  - `best` (object, required)
    - `day` (object | any | null, required)
    - `time` (object | any | null, required)
  - `experiments` (object[], required) — Up to three things to try: days or times doing worse than the rest, then ones nothing has gone out on.
    - `kind` (string, required, one of `weak`, `untested`)
    - `dimension` (string, required, one of `day`, `time`)
    - `key` (string, required)
    - `title` (string, required)
    - `summary` (string, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## When each channel's numbers were and will be read

```http
GET https://orbitwrite.com/api/v1/analytics/sync-status
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

Collection status per channel

- `channels` (object[], required)
  - `accountId` (string, required)
  - `platform` (string, required)
  - `channel` (string | any | null, required)
  - `collected` (boolean, required) — Whether the worker re-reads this channel's posts at all.
  - `lastPolledAt` (string | any | null, required)
  - `nextPollAt` (string | any | null, required)
  - `followersAt` (string | any | null, required)
  - `paused` (object | any | null, required) — Set while the platform is refusing reads (`rate_limited`, `auth`, or X's `daily_cap`).
- `x` (object | any | null, required) — Only with an X channel: whether X is being read this month. X reads are billed, and capped.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## One post's numbers over time

```http
GET https://orbitwrite.com/api/v1/posts/{groupId}/metrics
```

Every read of the post's numbers, per channel, oldest first: what a chart of how it grew is drawn from. Requires `analytics:read`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `groupId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The post's reads

- `groupId` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history hid this post's earlier reads.
- `channels` (object[], required)
  - `accountId` (string, required)
  - `postId` (string, required)
  - `platform` (string, required)
  - `channel` (string | any | null, required)
  - `points` (object[], required)
    - `fetchedAt` (string, required)
    - `metrics` (object, required)
      - `impressions` (number | any | null, required)
      - `reactions` (number | any | null, required)
      - `comments` (number | any | null, required)
      - `reposts` (number | any | null, required)
      - `quotes` (number | any | null, required)
      - `saves` (number | any | null, required)
      - `clicks` (number | any | null, required)
      - `profileClicks` (number | any | null, required)
      - `videoViews` (number | any | null, required)
      - `engagementRate` (number | any | null, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## How one post compares with the others

```http
GET https://orbitwrite.com/api/v1/posts/{groupId}/benchmark
```

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`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `groupId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The post against its peers

- `groupId` (string, required)
- `historyDays` (integer | any | null, required) — Set when the post is older than the plan's analytics history reaches: it then has no comparisons.
- `lookbackDays` (integer, required)
- `channels` (object[], required)
  - `accountId` (string, required)
  - `postId` (string, required)
  - `platform` (string, required)
  - `channel` (string | any | null, required)
  - `publishedAt` (string | any | null, required)
  - `ageDays` (integer | any | null, required)
  - `format` (object | any | null, required)
  - `metrics` (object, required)
    - `engagements` (number | any | null, required)
    - `impressions` (number | any | null, required)
    - `engagementRate` (number | any | null, required)
  - `comparisons` (object[], required)
    - `scope` (string, required, one of `channel`, `platform`, `format`) — `channel`: 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.
    - `posts` (integer, required)
    - `few` (boolean, required) — Too few peers for the comparison to say much.
    - `engagements` (object | any | null, required)
    - `impressions` (object | any | null, required)
    - `engagementRate` (object | any | null, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## A campaign's report

```http
GET https://orbitwrite.com/api/v1/campaigns/{campaignId}/report
```

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`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `campaignId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The campaign's report

- `campaign` (object, required)
  - `id` (string, required)
  - `title` (string | any | null, required)
  - `startDate` (string | any | null, required)
  - `endDate` (string | any | null, required)
  - `status` (string, required, one of `scheduled`, `active`, `completed`) — From 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.
  - `durationDays` (integer | any | null, required) — Days from its start (or first post) to its end, or to now while it runs.
- `timezone` (string, required)
- `historyDays` (integer | any | null, required) — Set when the plan's analytics history doesn't reach the campaign's earliest posts: they are listed, but their numbers are left out.
- `minPosts` (integer, required)
- `published` (object, required)
  - `posts` (integer, required) — Posts that went out, one per post however many channels it reached.
  - `channelPosts` (integer, required) — The same posts counted once per channel.
  - `measured` (integer, required) — Posts with engagement numbers from at least one channel.
  - `firstAt` (string | any | null, required)
  - `lastAt` (string | any | null, required)
  - `activeDays` (integer, required) — Distinct days, in `timezone`, that something went out.
  - `channels` (object[], required)
    - `accountId` (string, required)
    - `platform` (string, required)
    - `channel` (string | any | null, required)
    - `posts` (integer, required)
- `pipeline` (object | any | null, required) — Posts that haven't gone out: scheduled, and everything else (drafts, awaiting approval). Null without `post:read`.
- `totals` (AnalyticsMetricValues, required) — Each metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
  - `engagements` (number | any | null, required)
  - `impressions` (number | any | null, required)
  - `reactions` (number | any | null, required)
  - `comments` (number | any | null, required)
  - `reposts` (number | any | null, required)
  - `quotes` (number | any | null, required)
  - `saves` (number | any | null, required)
  - `clicks` (number | any | null, required)
  - `profileClicks` (number | any | null, required)
  - `videoViews` (number | any | null, required)
- `engagementsPerPost` (number | any | null, required)
- `platforms` (CampaignBreakdownRow[], required) — Best first by average engagements per post, rows too small to judge last.
  - `key` (string, required)
  - `label` (string, required)
  - `posts` (integer, required)
  - `engagements` (number | any | null, required)
  - `avgEngagements` (number | any | null, required)
  - `impressions` (number | any | null, required)
  - `engagementRate` (number | any | null, required)
  - `few` (boolean, required) — Fewer than `minPosts` posts: too few for the average to be a finding.
- `formats` (CampaignBreakdownRow[], required)
  - `key` (string, required)
  - `label` (string, required)
  - `posts` (integer, required)
  - `engagements` (number | any | null, required)
  - `avgEngagements` (number | any | null, required)
  - `impressions` (number | any | null, required)
  - `engagementRate` (number | any | null, required)
  - `few` (boolean, required) — Fewer than `minPosts` posts: too few for the average to be a finding.
- `bestPlatform` (CampaignBreakdownRow | any | null, required) — The leading platform, preferring one with at least `minPosts` posts. Check `few` before calling it a finding.
- `bestFormat` (CampaignBreakdownRow | any | null, required)
- `topPosts` (CampaignReportPost[], required) — Up to 5 posts, best first by engagements; a post with none never ranks.
  - `groupId` (string | any | null, required)
  - `postIds` (string[], required)
  - `text` (string, required)
  - `publishedAt` (string, required)
  - `channels` (object[], required)
    - `accountId` (string, required)
    - `platform` (string, required)
    - `channel` (string | any | null, required)
    - `platformUrl` (string | any | null, required)
  - `media` (object | any | null, required)
  - `format` (object, required)
    - `key` (string, required)
    - `label` (string, required)
  - `threadLength` (integer, required) — Posts in its longest channel's thread; 1 for a single post.
  - `metrics` (AnalyticsMetricValues, required) — Each metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
    - `engagements` (number | any | null, required)
    - `impressions` (number | any | null, required)
    - `reactions` (number | any | null, required)
    - `comments` (number | any | null, required)
    - `reposts` (number | any | null, required)
    - `quotes` (number | any | null, required)
    - `saves` (number | any | null, required)
    - `clicks` (number | any | null, required)
    - `profileClicks` (number | any | null, required)
    - `videoViews` (number | any | null, required)
- `longestThread` (CampaignReportPost | any | null, required) — The post with the longest thread; null when nothing was a thread.
- `insights` (object[], required)
  - `id` (string, required)
  - `dimension` (string, required)
  - `group` (string, required)
  - `direction` (string, required, one of `better`, `worse`)
  - `metric` (string, required)
  - `title` (string, required)
  - `summary` (string, required)
  - `lift` (number, required)
  - `posts` (integer, required)
  - `comparedPosts` (integer, required)
  - `confidence` (string, required, one of `low`, `medium`, `high`)
  - `exampleGroupIds` (string[], required)
- `posts` (CampaignReportPost[], required) — Every published post, newest first, at most 200.
  - `groupId` (string | any | null, required)
  - `postIds` (string[], required)
  - `text` (string, required)
  - `publishedAt` (string, required)
  - `channels` (object[], required)
    - `accountId` (string, required)
    - `platform` (string, required)
    - `channel` (string | any | null, required)
    - `platformUrl` (string | any | null, required)
  - `media` (object | any | null, required)
  - `format` (object, required)
    - `key` (string, required)
    - `label` (string, required)
  - `threadLength` (integer, required) — Posts in its longest channel's thread; 1 for a single post.
  - `metrics` (AnalyticsMetricValues, required) — Each metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'.
    - `engagements` (number | any | null, required)
    - `impressions` (number | any | null, required)
    - `reactions` (number | any | null, required)
    - `comments` (number | any | null, required)
    - `reposts` (number | any | null, required)
    - `quotes` (number | any | null, required)
    - `saves` (number | any | null, required)
    - `clicks` (number | any | null, required)
    - `profileClicks` (number | any | null, required)
    - `videoViews` (number | any | null, required)
- `truncated` (boolean, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Orb AI's summary of a campaign

```http
GET https://orbitwrite.com/api/v1/campaigns/{campaignId}/summary
```

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`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `campaignId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The stored summary

- `summary` (CampaignSummary | any | null, required)
- `summarisable` (boolean, required) — Whether a summary can be written now: AI is available and at least one of the campaign's posts has numbers.
- `due` (boolean, required) — Summarisable, and there is no summary for the phase the campaign is in. A page may write one unasked (`ifMissing`).

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Write Orb AI's summary of a campaign

```http
POST https://orbitwrite.com/api/v1/campaigns/{campaignId}/summary
```

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`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `campaignId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Request body (`application/json`)

- `ifMissing` (boolean, optional) — Only write one when the campaign has no summary for the phase it is in.

### Responses

#### 200

The summary, and whether it was written just now

- `summary` (CampaignSummary, required)
  - `text` (string, required)
  - `status` (string, required, one of `scheduled`, `active`, `completed`) — The phase it was written for: `active` is a running campaign's update, `completed` the analysis once it ended.
  - `generatedAt` (string, required)
  - `stale` (boolean, required) — The campaign's numbers or phase have moved since it was written.
- `generated` (boolean, required)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 422

Nothing to summarise yet: none of the campaign's posts have numbers

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

#### 500

No AI provider configured

- `error` (string, required)
- `issues` (any[], optional)

#### 502

An upstream provider failed

- `error` (string, required)
- `issues` (any[], optional)

## Orb AI's analysis of the last 28 days

```http
GET https://orbitwrite.com/api/v1/analytics/summary
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The stored analysis

- `summary` (WorkspaceAnalyticsSummary | any | null, required)
- `due` (boolean, required) — No 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.
- `available` (boolean, required) — Whether this deployment can write one at all (an AI provider is configured).

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Write Orb AI's analysis of the last 28 days

```http
POST https://orbitwrite.com/api/v1/analytics/summary
```

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`.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The analysis, and whether it was written just now

- `summary` (WorkspaceAnalyticsSummary, required)
  - `text` (string, required)
  - `from` (string, required) — Start of the window it was written about.
  - `to` (string, required)
  - `generatedAt` (string, required)
  - `nextAt` (string | any | null, required) — When it may next be rewritten (once a day at most); null when it may be now.
- `generated` (boolean, required)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 422

Nothing to analyse: no posts with numbers in the last 28 days

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

#### 500

No AI provider configured

- `error` (string, required)
- `issues` (any[], optional)

#### 502

An upstream provider failed

- `error` (string, required)
- `issues` (any[], optional)
