# Discord

Source: https://staging.postgale.com/docs/discord

Publish messages, embeds, polls, forum posts and threads to Discord channels, send DMs, and manage pins, roles, members and scheduled events through Postgale's bot, via the Postgale API and MCP server.

Postgale runs one central Discord bot. A server admin installs it once, picks the channels Postgale may post to, and from then on you
publish over REST or MCP: messages with attachments and embeds, polls, forum posts, threads and announcement crossposts, with edits and
deletes. The same bot pins messages, manages roles and members, creates scheduled events and sends outbound DMs. You do not create a
Discord application, request intents or keep a gateway connection open.

Everything is available over the [REST API](https://staging.postgale.com/docs/api) (paths relative to `https://staging.postgale.com/api/v1`) and the [MCP server](https://staging.postgale.com/docs/mcp)
(`connect_account`, `create_post`, `manage_published_post` and the `discord_*` tools). Each connected channel is one account with
platform `discord`; its `platformAccountId` is the Discord channel id.

## Connect a server and its channels

1. In Postgale open **Dashboard → Accounts → Discord**, or ask for a link with `GET /connect/discord` (or `connect_account`).
2. On Discord, pick the **server** and authorize the bot. You need the *Manage Server* permission there.
3. Back in Postgale, pick the channels to connect: text, announcement and forum channels. Each one becomes a Discord account.

GET /api/v1/connect/discord:
```bash
curl https://staging.postgale.com/api/v1/connect/discord \
  -H 'Authorization: Bearer sk_…'

# {"authUrl":"https://postgale.com/api/oauth/discord/consent?state=…","expiresInSeconds":600}
```

To bind channels yourself (or add more later), send the server id and the channels to `POST /connect/discord`. It works for 30 minutes
after the install, and at any time once your organization has a Discord account in that server.

POST /api/v1/connect/discord:
```bash
curl -X POST https://staging.postgale.com/api/v1/connect/discord \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"guildId":"1098765432109876543","channelId":"1234567890123456789","profileId":"prof_…"}'

# {"message":"Discord channel connected successfully","account":{"accountId":"…","platform":"discord","displayName":"Acme HQ - #announcements","channelId":"1234567890123456789","guildId":"1098765432109876543","isActive":true}}
```

`channelIds` connects several channels at once and answers `accounts` and `failed` (422 `no_channels_connected` when none could be
connected); `redirect_url` comes back with `connected`, `profileId`, `accountId` and `accountIds` appended. Accounts are named
`<server> - #<channel>`.

The bot asks for these permissions at install time: Send Messages, Embed Links, Attach Files, Send Messages in Threads, Create Public
Threads, Manage Messages (pins, deleting messages), Manage Roles, Manage Events and Create Events, plus View Channel, Read Message History,
Send TTS Messages and Manage Webhooks. Posts go through a webhook the bot creates in each connected channel, which is what lets a post
carry its own name and avatar. Disconnecting the account deletes that webhook.

Channels connected before the bot existed (through a channel webhook, `webhook.incoming`) keep working. Connecting the same channel with
the bot takes that account over, with its post history.

## Publish

The post content is the message, up to 2000 characters. A message needs content, an attachment, an embed or a poll. Postgale uploads the
attachments to Discord itself (multipart), up to 10 files of up to 10 MB each and 25 MB per message in all; use the `publicUrl` of
[uploaded media](https://staging.postgale.com/docs/api) or any public https URL. Limits are checked when the post is created.

POST /api/v1/posts:
```bash
curl -X POST https://staging.postgale.com/api/v1/posts \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"content":"Release 2.4 is out 🎉","platforms":[{"accountId":"acc_dc","platformSpecificData":{"mediaUrls":["https://media.postgale.com/…/screenshot.png"],"embeds":[{"title":"Changelog","description":"Faster sync and dark mode.","url":"https://example.com/changelog","color":5793266,"fields":[{"name":"Version","value":"2.4","inline":true}],"image":{"url":"attachment://screenshot.png"}}],"threadFromMessage":{"name":"Discussion","autoArchiveDuration":1440}}}],"publishNow":true}'
```

| platformSpecificData | What it does |
| --- | --- |
| `mediaUrl` / `mediaUrls` | Up to 10 attachments (public https URLs, any file type) |
| `embeds` | Up to 10 [Discord embed objects](https://discord.com/developers/docs/resources/message#embed-object): `title` (≤256), `description` (≤4096), `url`, `color` (integer `0xRRGGBB`), `timestamp`, `footer {text, icon_url?}`, `image {url}`, `thumbnail {url}`, `author {name, url?, icon_url?}`, `fields [{name, value, inline?}]` (≤25). 6000 characters in all. Image URLs may be `attachment://<file name>` to show an attachment |
| `poll` | A native poll: `{question: {text}, answers: [{poll_media: {text}}], duration?, allow_multiselect?}`. Up to 10 answers, 1 to 768 hours (default 24). No attachments in the same message |
| `forumThreadName` | Forum channels (required there): the forum post's title; the content is its first message |
| `forumAppliedTags` | Up to 5 forum tag ids (read them from the forum channel's settings in Discord) |
| `threadFromMessage` | `{name, autoArchiveDuration?, rateLimitPerUser?}`: open a public thread under the published message. `autoArchiveDuration` is 60, 1440, 4320 or 10080 minutes; `rateLimitPerUser` is slow mode in seconds (0 to 21600) |
| `crosspost` | Announcement channels: publish the message to every server following the channel. Does nothing on other channels |
| `tts` | Text-to-speech message |
| `username` / `webhookUsername` | Post under this name (1-80 characters, cannot contain "discord" or "clyde"); overrides the account default |
| `avatarUrl` / `webhookAvatarUrl` | https URL of the avatar to post with; overrides the account default |
| `silent` | No push notification (like `@silent`) |
| `mentionEveryone` | Let `@everyone` and `@here` in the text ping. Off by default; user and role mentions always ping |
| `channelId` | Optional check: the account's own channel (each account posts to its channel) |

The target's `platformPostId` is the Discord message id and `url` the `discord.com/channels/…` link to the message. A thread or crosspost
that fails after the message went out is reported in the target's warnings; the post itself stays published.

## Edit and delete

| Method | Path | What it does |
| --- | --- | --- |
| POST | `/posts/{id}/edit` | `{content}`: new text of the message (≤2000), no time limit; attachments and embeds stay |
| POST | `/posts/{id}/unpublish` | Delete the message |
| DELETE | `/discord/channels/{channelId}/messages/{messageId}?accountId=` | Delete any message in the channel (moderation) |
| GET | `/accounts/{id}/health` | Whether the channel webhook still exists |

## Channels and settings

| Method | Path | What it does |
| --- | --- | --- |
| GET | `/accounts/{id}/discord-channels` | Text (0), announcement (5) and forum (15) channels of the server |
| GET | `/accounts/{id}/discord-settings` | The connected channel and server, and the default name and avatar of every post |
| PATCH | `/accounts/{id}/discord-settings` | `webhookUsername` and `webhookAvatarUrl` (an empty string resets them), and/or `channelId` to move the account to another channel of the same server |

## Server management

Every call below takes `accountId` (a Discord account of your organization in that server) as a query parameter, or in the body on the
event create and update calls. The paths mirror Discord's own API.

| Method | Path | What it does |
| --- | --- | --- |
| GET | `/discord/channels/{channelId}/pins` | Pinned messages, most recent first (at most 50) |
| PUT / DELETE | `/discord/channels/{channelId}/pins/{messageId}` | Pin / unpin (both idempotent; a 51st pin is a 400) |
| POST | `/discord/channels/{channelId}/threads` | `{name, messageId?, autoArchiveDuration?}`: a public thread from a message, or a standalone one |
| POST | `/discord/channels/{channelId}/messages/{messageId}/crosspost` | Crosspost an announcement-channel message (400 on other channels) |
| GET / POST | `/discord/guilds/{guildId}/roles` | List roles / create one (`name`, `color` as a decimal, `hoist`, `mentionable`, `permissions` as a bitfield string) → 201 |
| PATCH / DELETE | `/discord/guilds/{guildId}/roles/{roleId}` | Edit any of those fields / delete the role (from every member too) |
| GET | `/discord/guilds/{guildId}/members` | Members, `limit` 1-1000 (default 100); pass `pagination.nextCursor` as `after` until `hasMore` is false |
| GET | `/discord/guilds/{guildId}/members/search` | `query`: members whose username or nickname starts with it |
| GET | `/discord/guilds/{guildId}/members/{userId}` | One member |
| PUT / DELETE | `/discord/guilds/{guildId}/members/{userId}/roles/{roleId}` | Assign / remove a role (idempotent; `@everyone` is refused) |
| GET / POST | `/discord/guilds/{guildId}/events` | Scheduled events (`withUserCount=true` adds the RSVP count) / create one |
| GET / PATCH / DELETE | `/discord/guilds/{guildId}/events/{eventId}` | One event / update it (`status: "cancelled"` cancels it) / delete it |

Creating an event takes `name` (≤100), `description` (≤1000), `startsAt` (in the future) and `entity`: `{type: "external", location,
endsAt}` for something outside Discord, or `{type: "voice" | "stage", channelId, endsAt?}`. `imageDataUri` sets a cover image.

POST /api/v1/discord/guilds/1098765432109876543/events:
```bash
curl -X POST https://staging.postgale.com/api/v1/discord/guilds/{guildId}/events \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"accountId":"acc_dc","name":"Weekly AMA","startsAt":"2027-01-01T18:00:00Z","entity":{"type":"external","location":"https://zoom.us/j/123456789","endsAt":"2027-01-01T19:00:00Z"}}'
```

Roles follow Discord's hierarchy: the bot can only manage roles below its own highest role. Listing members needs the *Server Members
Intent* on the bot; an empty list with no error means it is off.

## Direct messages

`POST /discord/dms` sends a 1:1 message from the bot: `{accountId, userId, content?, embeds?, attachments?, tts?}`. `userId` is the
person's Discord id (find it with the member search); `attachments` are `{type: image | video | gif | document, url, filename?}`. DMs are
sent at once and not stored as posts. Discord only lets the bot DM people who share a server with it and accept DMs (403 otherwise).
Replies never come back: reading DMs needs a gateway connection, so Discord is not part of the inbox.

POST /api/v1/discord/dms:
```bash
curl -X POST https://staging.postgale.com/api/v1/discord/dms \
  -H 'Authorization: Bearer sk_…' \
  -H 'Content-Type: application/json' \
  -d '{"accountId":"acc_dc","userId":"2233445566778899001","content":"Welcome to Acme!"}'
```

## Approval and audit

Every write (posts, edits, deletes, pins, roles, events, threads, crossposts, DMs and settings) is queued for a person while approval
applies to the API key, the organization or the account: the API answers 202 `pending_approval` and the MCP tool returns the queued
action. Reads are never queued. Every write is recorded in the audit log.

## Discord's rules to know

- **Rate limits**: when Discord answers 429, Postgale retries the post after the `retry_after` Discord asks for; server calls answer 429.
- **Permissions**: Discord's 403 (a missing permission or the role hierarchy) is passed on as a 403. Servers that installed the bot before a permission was added must re-invite it, or grant the permission to the bot's role.
- **Deleted webhooks**: if a channel's webhook is deleted in Discord, the account becomes `needs_reconnect` (`account.disconnected`, reason `webhook_deleted`); connect the channel again.
- **Analytics**: Discord gives bots no views or reaction counts for messages, so post and account analytics answer `not_supported`.
- **File size**: 10 MB per file is Discord's default upload limit; boosted servers allow more, but Postgale keeps to the default.
