# Discord

7 endpoints.

## The browser URL that invites the bot to a Discord server

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

Returns the Discord authorization URL a server owner must open to add the bot. This step cannot be performed over an API — Discord requires a human to choose the server — so the URL is what an API caller gets. The picker that callback leads to connects the first channel, which is what binds the server to this workspace; after that `GET /integrations/discord/guilds/{guildId}/channels` and `POST /integrations/discord/channels` are both headless. 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)
- `permissions` (string, required) — The Discord permissions bitmask 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

Discord OAuth is not configured on this deployment

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

## Read a workspace's Discord integration

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

Returns the connected guild(s), 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 Discord integration state

- `guilds` (object[], required)
  - `guildId` (string, required)
  - `guildName` (string | null, required)
- `routes` (DiscordNotificationRoute[], required)
  - `id` (string, required)
  - `guildId` (string, required)
  - `guildName` (string | null, 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)
  - `guildId` (string, 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)

## Connect a Discord channel as a posting channel

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

Adds a Discord channel to the workspace's channels, so posts can be published to it. The server must be one this workspace has already connected — Discord's bot token is shared across tenants, so an unscoped `guildId` would let any workspace attach another's channel and publish into it. Connecting a server for the first time therefore runs through the browser: `GET /integrations/discord/install-url` returns the URL, and the picker it leads to connects the first channel. Every channel after that is reachable from here. Requires `account: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.
- `guildId` (string, required) — The server the channel belongs to. Checked twice: that this workspace has connected it (404 otherwise, whether or not it exists), and that the channel really is in it.
- `channelId` (string, required) — A Discord channel id. Its name, and the server's, are read from Discord rather than from this request.

### Responses

#### 200

The channel is connected

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

#### 404

The resource does not exist

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

#### 502

Discord API unreachable while verifying the channel

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

## List the postable channels in a Discord server

```http
GET https://orbitwrite.com/api/v1/integrations/discord/guilds/{guildId}/channels
```

The text and announcement channels the bot can see in a server **this workspace has already connected**. Use it to pick a channel for `POST /integrations/discord/channels`. A server the workspace has not connected answers 404, whether or not it exists — the bot token is shared across tenants, so an unscoped lookup here would expose another workspace's channel names. The first channel in a new server is connected through the browser picker the install URL leads to. Requires `integration:connect`.

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

### Path parameters

- `guildId` (string, required)

### Query parameters

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

### Responses

#### 200

The guild's postable channels

- `channels` (object[], required)
  - `id` (string, required)
  - `name` (string, required)
  - `guildId` (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)

#### 502

Discord API unreachable while listing the channels

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

## Add a notification route

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

Routes the chosen events (optionally filtered by tag) to a Discord channel. The channel is resolved server-side against what the bot can see — the guild comes from the channel, not from the request. 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` (DiscordNotificationRoute, required)
  - `id` (string, required)
  - `guildId` (string, required)
  - `guildName` (string | null, 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

Discord 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/discord/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/discord/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)
