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 (paths relative to https://staging.postgale.com/api/v1) and the MCP server (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
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
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 or any public https URL. Limits are checked when the post is created.

POST /api/v1/posts
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}'
platformSpecificDataWhat it does
mediaUrl / mediaUrlsUp to 10 attachments (public https URLs, any file type)
embedsUp to 10 Discord embed objects: 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
pollA 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
forumThreadNameForum channels (required there): the forum post's title; the content is its first message
forumAppliedTagsUp 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)
crosspostAnnouncement channels: publish the message to every server following the channel. Does nothing on other channels
ttsText-to-speech message
username / webhookUsernamePost under this name (1-80 characters, cannot contain "discord" or "clyde"); overrides the account default
avatarUrl / webhookAvatarUrlhttps URL of the avatar to post with; overrides the account default
silentNo push notification (like @silent)
mentionEveryoneLet @everyone and @here in the text ping. Off by default; user and role mentions always ping
channelIdOptional 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

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

Channels and settings

MethodPathWhat it does
GET/accounts/{id}/discord-channelsText (0), announcement (5) and forum (15) channels of the server
GET/accounts/{id}/discord-settingsThe connected channel and server, and the default name and avatar of every post
PATCH/accounts/{id}/discord-settingswebhookUsername 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.

MethodPathWhat it does
GET/discord/channels/{channelId}/pinsPinned 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}/crosspostCrosspost an announcement-channel message (400 on other channels)
GET / POST/discord/guilds/{guildId}/rolesList 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}/membersMembers, limit 1-1000 (default 100); pass pagination.nextCursor as after until hasMore is false
GET/discord/guilds/{guildId}/members/searchquery: 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}/eventsScheduled 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
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
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.

On this page