# Scheduling

2 endpoints.

## Suggested publish times per channel

```http
GET https://orbitwrite.com/api/v1/scheduling/suggestions
```

Candidate times to schedule a post at, one entry per connected channel. The slot strategies return the workspace's configured posting slots that nothing is scheduled on yet — the same placement the composer's queue makes, so a caller and the calendar agree on which slots are free. `best-time` instead ranks a static, research-informed engagement profile for the platform; it is a heuristic, not measured analytics (`researched` says whether the platform has a profile of its own or the generic fallback). Requires `postingSchedule:read` and `account:read`; the slot strategies also require `post:read`, since occupancy is post data.

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

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `accountIds` (string | string[], optional) — Repeat to scope to particular channels. Omitted means every connected channel in the workspace.
- `strategy` (string, optional) — `first-available` (default) takes the soonest free slot; `queue` appends after everything already booked, so a run of posts keeps its order; `best-time` ignores the configured schedule and ranks the platform's engagement profile.
- `count` (integer, optional) — Candidates per channel. Defaults to 1.
- `after` (string, optional) — Search from this instant instead of now — for planning a batch beyond what is already scheduled.

### Responses

#### 200

Suggested times, one entry per channel

- `suggestions` (AccountTimeSuggestion[], required)
  - `accountId` (string, required)
  - `platform` (string, required)
  - `timezone` (string, required) — IANA zone the times were placed in — the workspace's, falling back to the channel's and then UTC.
  - `times` (SuggestedTime[], required) — Soonest first for the slot strategies, strongest first for best-time. Empty when the channel has no free slot inside the two-week lookahead.
    - `at` (string, required) — ISO 8601 instant to schedule at.
    - `source` (string, required, one of `slot`, `best-time`) — `slot` — a configured posting slot with nothing on it. `best-time` — a peak cell of the platform's engagement profile.
  - `reason` (string | any | null, required) — Why these times, for a best-time answer. Null for the slot strategies, where the reason is the workspace's own configuration.
  - `researched` (boolean, required) — Whether `reason` reflects a profile researched for this platform or the generic fallback window. Best-time data is a heuristic either way — there is no per-account analytics behind it yet, so do not report it to a person as measured engagement.

#### 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)

## Slots for a run of posts at a cadence

```http
GET https://orbitwrite.com/api/v1/scheduling/plan
```

Places a run of posts — a campaign — into the workspace's free posting slots: one instant per post, free on every named channel where their schedules allow it, spread across the range's days at `perWeek`, or month by month at `perMonth`. Ranges up to a year. The same slots and occupancy `/scheduling/suggestions` reads, so a plan never books a slot the calendar shows as taken. Nothing is written. Requires `account:read`, `postingSchedule:read` and `post:read`.

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

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `accountIds` (string | string[], required) — Repeat for each channel the run goes out on.
- `count` (integer, optional) — How many posts. Defaults to `perWeek` × the range in weeks.
- `perWeek` (integer, optional) — Posts per week. Defaults to 3.
- `perMonth` (integer, optional) — Posts per month, for a slower run: placed within each month of the range, counted from `startDate`. Instead of `perWeek`.
- `startDate` (string, optional) — First day, YYYY-MM-DD in the workspace's zone. Defaults to today.
- `endDate` (string, optional) — Last day (inclusive), YYYY-MM-DD in the workspace's zone.

### Responses

#### 200

The placed slots

- `timezone` (string, required) — IANA zone the days were counted in — the workspace's.
- `startDate` (string, required) — First day searched, YYYY-MM-DD.
- `endDate` (string, required) — Last day searched, YYYY-MM-DD.
- `perWeek` (integer | null, required) — Posts per week, when the run was placed weekly.
- `perMonth` (integer | null, required) — Posts per month, when the run was placed month by month.
- `requested` (integer, required) — Posts asked for; `slots` is shorter when the range ran out of free slots.
- `slots` (PlannedSlot[], required)
  - `at` (string, required) — ISO 8601 instant to schedule at.
  - `accountIds` (string[], required) — The channels this instant is a free posting slot on.
  - `partial` (boolean, required) — True when the slot is free on only some of the channels — their schedules don't share enough free times in the range.

#### 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)
