# Slack

9 endpoints.

## The browser URL that installs the Slack app

```http
GET https://orbitwrite.com/api/v1/integrations/slack/install-url
```

Returns the Slack OAuth URL a person must open to authorize the app — the one step of connecting that cannot happen over an API. A caller that already holds its own bot token should `POST /integrations/slack` instead and skip the browser entirely. Requires `integration:connect`.

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

### Query parameters

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

### Responses

#### 200

The install URL

- `url` (string, required) — Open this in a browser. On completion the workspace's install is written and the caller is returned to the app.
- `scopes` (string[], required) — The bot scopes the install requests.

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

#### 501

Slack OAuth is not configured on this deployment

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

## Read a workspace's Slack integration

```http
GET https://orbitwrite.com/api/v1/integrations/slack
```

Returns the Slack install (if any), the notification routes, the channels the bot can post to, and the workspace's tags for the optional tag filter. Requires `integration:read`, which every role holds. Channel listing failures degrade to an empty list.

**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 Slack integration state

- `installation` (SlackInstallation | any | null, required)
- `routes` (SlackNotificationRoute[], required)
  - `id` (string, required)
  - `channelId` (string, required)
  - `channelName` (string | null, required)
  - `events` (string[], required)
  - `tagId` (string | null, required)
  - `enabled` (boolean, required)
- `channels` (object[], required)
  - `id` (string, required)
  - `name` (string, required)
  - `isPrivate` (boolean, required)
- `tags` (object[], required)
  - `id` (string, required)
  - `name` (string, required)
  - `color` (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)

## Install Slack for a workspace from a bot token

```http
POST https://orbitwrite.com/api/v1/integrations/slack
```

Establishes the Slack install without a browser — for a CLI, an MCP tool, or anyone running their own Slack app. Re-running it refreshes the token in place rather than stacking a second install. The interactive alternative is the OAuth flow whose URL `GET /integrations/slack/install-url` returns. Requires `integration:connect`.

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

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

- `organizationId` (string, optional) — Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't.
- `botToken` (string, required) — A Slack bot token (`xoxb-…`). Which workspace it installs into is read from the token itself, so there is nothing else to supply — and nothing a caller could claim.

### Responses

#### 200

Slack is installed for the workspace

- `ok` (boolean, required, one of `true`)
- `installation` (SlackInstallation, required)
  - `id` (string, required)
  - `slackTeamId` (string, required)
  - `slackTeamName` (string | null, required)
  - `missingScopes` (string[], required) — Bot scopes this app now requests that the install was not granted. Non-empty means the install predates them and features needing them (approving from Slack needs `users:read` and `users:read.email`; DMing reviewers their approval requests needs those plus `im:write`) won't work until Slack is reconnected. Empty when Slack reported no scopes at install.

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

#### 502

Slack API unreachable while verifying the token

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

## Disconnect the workspace's Slack install

```http
DELETE https://orbitwrite.com/api/v1/integrations/slack
```

Removes the installation; its notification routes cascade. Requires `integration:disconnect`.

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

### Query parameters

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

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

## Connect a Slack channel as a posting channel

```http
POST https://orbitwrite.com/api/v1/integrations/slack/channels
```

Adds a Slack channel to the workspace's channels, so posts can be published to it. Requires the workspace to have Slack installed, and `account:connect`. Re-connecting a channel that is already there refreshes it in place and keeps the schedule its owner chose.

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

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

- `organizationId` (string, optional) — Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't.
- `channelId` (string, required) — A Slack channel id (`C…`). Its name is read from Slack, not from this request. Public channels are joined by the bot so media posts work.

### Responses

#### 200

The channel is connected

- `ok` (boolean, required, one of `true`)
- `channel` (ConnectedChannel, required)
  - `id` (string, required)
  - `displayHandle` (string, required)
  - `created` (boolean, required)
  - `restored` (boolean, required)
  - `becamePrimary` (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)

#### 502

Slack API unreachable while verifying the channel

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

## Add a notification route

```http
POST https://orbitwrite.com/api/v1/integrations/slack/routes
```

Routes the chosen events (optionally filtered by tag) to a Slack channel. The channel is resolved server-side against what the bot can see, so the stored channel name is ours rather than the caller's. Requires `integration:update`.

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

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

- `organizationId` (string, optional) — Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't.
- `channelId` (string, required)
- `events` (string[], required)
- `tagId` (string | null, optional) — Narrow the rule to posts carrying this tag. Null (or omitted) means all posts; a tag from another workspace is rejected.

### Responses

#### 200

The route was added

- `ok` (boolean, required, one of `true`)
- `route` (SlackNotificationRoute, required)
  - `id` (string, required)
  - `channelId` (string, required)
  - `channelName` (string | null, required)
  - `events` (string[], required)
  - `tagId` (string | null, required)
  - `enabled` (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)

#### 502

Slack API unreachable while verifying the channel

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

## Enable or pause a notification route

```http
POST https://orbitwrite.com/api/v1/integrations/slack/routes/{routeId}/toggle
```

Requires `integration:update`.

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

### Path parameters

- `routeId` (string, required)

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

- `organizationId` (string, optional) — Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't.
- `enabled` (boolean, 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)

## Remove a notification route

```http
DELETE https://orbitwrite.com/api/v1/integrations/slack/routes/{routeId}
```

Requires `integration:update`.

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

### Path parameters

- `routeId` (string, required)

### Query parameters

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

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

## Slack interactive-message callback

```http
POST https://orbitwrite.com/api/v1/integrations/slack/interactivity
```

Slack posts here when someone clicks Approve or Reject on an approval message. The body is `application/x-www-form-urlencoded` with a single `payload` field carrying Slack's JSON interaction object. Not for general callers: the request is authenticated by Slack’s own `X-Slack-Signature` (HMAC over the raw body, five-minute replay window) rather than by a session or API key, and the URL is registered in the Slack app’s configuration. Decisions run through the same `approvalsService.decide` the app’s own approval endpoint uses.

### Responses

#### 200

Acknowledged. The decision is processed out of band and reported through Slack's `response_url`.

#### 401

The Slack signature did not verify
