# Campaigns

10 endpoints.

## List a workspace's campaigns

```http
GET https://orbitwrite.com/api/v1/campaigns
```

Requires `campaign:read`. Returns the organization's campaigns, newest first, each with a post count and the next upcoming scheduled post.

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

### Query parameters

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

### Responses

#### 200

The workspace's campaigns

- `campaigns` (Campaign[], required)
  - `id` (string, required)
  - `title` (string | null, required)
  - `description` (string | null, required) — The campaign's context/source text.
  - `color` (string, required)
  - `icon` (string | null, required)
  - `startDate` (string | null, required) — ISO 8601
  - `endDate` (string | null, required) — ISO 8601
  - `postCount` (integer, required) — Every post generated for the campaign, any status
  - `nextScheduledAt` (string | null, required) — ISO 8601 of the soonest still-upcoming scheduled post
  - `firstScheduledAt` (string | null, required) — ISO 8601 of the earliest fire time among the campaign's posts, any status; null when none has one (drafts have no fire time). With `lastScheduledAt`, the span a calendar draws the campaign over when `startDate`/`endDate` are unset.
  - `lastScheduledAt` (string | null, required) — ISO 8601 of the latest fire time among the campaign's posts, any status; null when none has one.

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

## Create a bare campaign

```http
POST https://orbitwrite.com/api/v1/campaigns
```

Requires `campaign:create`. Creates a campaign with no posts. The AI wizard generates posts separately; this is the lightweight counterpart used by the create modal and inline composer form.

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

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

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `title` (string, required)
- `description` (string, optional)
- `color` (string, optional, one of `grey`, `dark-grey`, `purple`, `blue`, `teal`, `green`, `yellow`, `orange`, `pink`, `red`) — Palette color id (shares the tag palette). Omit to keep the default.
- `icon` (string | null, optional) — Avatar icon as a prefixed string: `emoji:🚀` for a raw emoji, or `hi:Rocket01` for a curated hugeicon. Null clears it back to the letter monogram.
- `startDate` (string | null, optional)
- `endDate` (string | null, optional)

### Responses

#### 200

The created campaign

- `ok` (boolean, required, one of `true`)
- `campaign` (CampaignRef, required)
  - `id` (string, required)
  - `title` (string | null, required)
  - `description` (string | null, required)
  - `color` (string, required)
  - `icon` (string | null, required)
  - `startDate` (string | null, required)
  - `endDate` (string | 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)

## Search campaigns by name

```http
GET https://orbitwrite.com/api/v1/campaigns/search
```

Requires `campaign:read`. Case-insensitive substring match against the campaign title, scoped to the organization. LIKE metacharacters in the query match literally. Powers the command menu's campaign search. Returns at most 20 campaigns, newest first.

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

### Query parameters

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

### Responses

#### 200

Matching campaigns, newest first

- `items` (SearchCampaignItem[], required)
  - `id` (string, required)
  - `title` (string | null, required)
  - `color` (string, required)
  - `icon` (string | null, required) — Prefixed avatar-icon string (`emoji:🚀` / `hi:Rocket01`), or null when the campaign has no custom icon.

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

## Get a campaign

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

Requires `campaign:read`. One campaign, with the same post count and next scheduled post the list carries. Its posts are `GET /posts?campaignId=`.

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

### Path parameters

- `campaignId` (string, required)

### Responses

#### 200

The campaign

- `id` (string, required)
- `title` (string | null, required)
- `description` (string | null, required) — The campaign's context/source text.
- `color` (string, required)
- `icon` (string | null, required)
- `startDate` (string | null, required) — ISO 8601
- `endDate` (string | null, required) — ISO 8601
- `postCount` (integer, required) — Every post generated for the campaign, any status
- `nextScheduledAt` (string | null, required) — ISO 8601 of the soonest still-upcoming scheduled post
- `firstScheduledAt` (string | null, required) — ISO 8601 of the earliest fire time among the campaign's posts, any status; null when none has one (drafts have no fire time). With `lastScheduledAt`, the span a calendar draws the campaign over when `startDate`/`endDate` are unset.
- `lastScheduledAt` (string | null, required) — ISO 8601 of the latest fire time among the campaign's posts, any status; null when none has one.

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

## Update a campaign's metadata

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

Requires `campaign:update`. Edit a campaign's name, description, colour, and date range.

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

### Path parameters

- `campaignId` (string, required)

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

- `title` (string, required)
- `description` (string, optional) — Omit to keep the stored description; send an empty string to clear it.
- `color` (string, optional, one of `grey`, `dark-grey`, `purple`, `blue`, `teal`, `green`, `yellow`, `orange`, `pink`, `red`) — Palette color id (shares the tag palette). Omit to keep the default.
- `icon` (string | null, optional) — Avatar icon as a prefixed string: `emoji:🚀` for a raw emoji, or `hi:Rocket01` for a curated hugeicon. Null clears it back to the letter monogram.
- `startDate` (string | null, optional)
- `endDate` (string | null, optional)

### Responses

#### 200

The mutation was applied

- `ok` (boolean, required, one of `true`)

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

## Delete a campaign

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

Requires `campaign:delete`. Removes the campaign and everything it generated — its posts (and their approval rows via cascade) and the orphaned per-topic composer groups.

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

### Path parameters

- `campaignId` (string, required)

### Responses

#### 200

The mutation was applied

- `ok` (boolean, required, one of `true`)

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

## Duplicate a campaign

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

Requires `campaign:create`. Copies the campaign's own fields (name, description, colour, date range) into a new campaign. Posts are not copied.

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

### Path parameters

- `campaignId` (string, required)

### Responses

#### 200

The new campaign

- `ok` (boolean, required, one of `true`)
- `campaign` (CampaignRef, required)
  - `id` (string, required)
  - `title` (string | null, required)
  - `description` (string | null, required)
  - `color` (string, required)
  - `icon` (string | null, required)
  - `startDate` (string | null, required)
  - `endDate` (string | 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)

## Delete multiple campaigns

```http
POST https://orbitwrite.com/api/v1/campaigns/batch-delete
```

Requires `campaign:delete`. Bulk-deletes campaigns owned by the given organization (with their posts and orphaned composer groups). Ids the organization doesn't own are skipped; `deletedIds` says what was removed.

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

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

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

### Responses

#### 200

The ids that were deleted

- `ok` (boolean, required, one of `true`)
- `deletedIds` (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)

## Approve or reject a campaign

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

Requires `approval:decide` — green-lighting a campaign is the same authority as approving a post. Approving releases the campaign's still-pending posts into the scheduled queue; rejecting drops them back to Drafts. Creating a campaign takes only `campaign:create`; deciding one is a separate authority.

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

### Path parameters

- `campaignId` (string, required)

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

- `decision` (string, required, one of `approved`, `rejected`)
- `comment` (string | null, optional)

### Responses

#### 200

The decision was recorded

- `ok` (boolean, required, one of `true`)
- `decision` (string, required, one of `approved`, `rejected`)
- `postCount` (integer, 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)

#### 409

Conflicts with an existing resource

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

## Generate a campaign's posts from an approved topic plan

```http
POST https://orbitwrite.com/api/v1/campaigns/generate
```

Requires `campaign:create`. Fans out AI generation across the selected accounts and persists a campaign with one composer group per topic, each post held in pending_approval until the campaign is approved.

**Authentication:** session cookie.

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

- `campaignId` (string | null, optional) — Attach the generated posts to this already-created bare campaign instead of minting a new one.
- `title` (string | null, optional)
- `source` (string, required) — The article URL or free-text brief the campaign promotes.
- `timelineType` (string, required, one of `date`, `evergreen`)
- `targetDate` (string | null, optional)
- `duration` (string, optional)
- `adaptPerPlatform` (boolean, optional)
- `accountIds` (string[], required)
- `topics` (object[], required)
  - `scheduledAt` (string, required)
  - `topic` (string, required)

### Responses

#### 200

The campaign and its generated posts were created

- `ok` (boolean, required, one of `true`)
- `campaignId` (string, required)
- `postCount` (integer, 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)
