The Discord API has a shortcut most platforms do not offer. You can post to a channel with a single HTTP request, no bot, no OAuth, and no application review, using a webhook URL that a server admin can create in about thirty seconds.
That makes Discord one of the two easiest networks to publish to programmatically, alongside Telegram. It also has the tightest character limit of any major platform at 2,000, which is worth knowing before you pipe content into it.
This guide covers webhooks, the bot alternative, embeds, file uploads, and the rate limit behaviour that catches people out at volume.
The 30-Second Answer
To post to a Discord channel programmatically, create a webhook in the channel settings, then send a POST request to https://discord.com/api/webhooks/{webhook.id}/{webhook.token} with a JSON body containing content.
Discord's documentation states you must provide a value for at least one of content, embeds, components, file, or poll. Message content supports up to 2,000 characters, and a message can include up to 10 embed objects.
No OAuth, no app review, and no per-message fee applies.
Webhook or Bot: Which Do You Need
This is the first decision and most people over-engineer it.
Use a webhook if you only need to post messages into one channel. A webhook is a URL with an embedded token. Anyone holding it can post to that channel, and nothing else.
It cannot read messages, cannot see the member list, and cannot act anywhere else in the server. For publishing content, that limited scope is exactly what you want.
Use a bot if you need to read messages, respond to commands, manage roles, react to events, or post across many channels with one identity. A bot requires creating an application, inviting it to the server with a permissions scope, and handling a gateway connection if you need real-time events.
For the job of "publish our content to a Discord channel on a schedule", a webhook is correct and a bot is unnecessary complexity, in the same way a scheduling tool beats building a queue for most teams.
The rest of this guide focuses on webhooks, and notes where the bot path differs. Our comparison of social media APIs covers where Discord sits relative to other networks.
Create a Webhook URL
In Discord, open the target channel, choose Edit Channel, then Integrations, then Webhooks, then New Webhook. Give it a name and avatar, both of which appear on the messages it posts, and copy the webhook URL.
If you manage several communities, our multi-account tooling keeps the destinations separate.
The URL has the form https://discord.com/api/webhooks/{webhook.id}/{webhook.token}. That token is the entire authentication story, so treat the URL as a secret. Anyone who obtains it can post to your channel as that webhook until it is deleted or regenerated.
Store it in an environment variable and never expose it in client-side code.
Creating a webhook requires the Manage Webhooks permission in the server, so if you are building for someone else's community, a server admin has to do this step and hand you the URL.
Post a Message
The Execute Webhook endpoint takes POST /webhooks/{webhook.id}/{webhook.token}. The minimal request:
curl -X POST "$DISCORD_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"content": "New post is live: https://example.com/post"
}'
In Node.js, with the error handling you actually need:
const res = await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: 'New post is live: https://example.com/post',
username: 'Content Bot',
}),
})
if (res.status === 429) {
const { retry_after } = await res.json()
throw new Error(`Rate limited, retry after ${retry_after}s`)
}
if (!res.ok) {
throw new Error(`Discord error ${res.status}: ${await res.text()}`)
}
A successful execute returns 204 No Content with an empty body by default, so do not try to parse JSON from it. If you need the created message back, append ?wait=true to the URL and Discord returns the message object instead.
The username and avatar_url fields override the webhook's configured name and avatar per message, which is useful if one webhook serves several content sources.
Publishing to more than Discord? One endpoint reaches every top platform including Discord, Telegram, Instagram, LinkedIn and X, with per-platform limits applied for you. See the API docs.
Rich Embeds
Plain content is limited to 2,000 characters and renders as plain text. Embeds are the better format for content distribution: they render as a card with a title, description, colour bar, image and footer.
curl -X POST "$DISCORD_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "How the Instagram Algorithm Works in 2026",
"description": "What actually drives reach this year, with sources.",
"url": "https://example.com/post",
"color": 5814783,
"image": { "url": "https://example.com/hero.jpg" },
"footer": { "text": "Published 1 September" }
}]
}'
A message can carry up to 10 embed objects. The color field is a decimal integer rather than a hex string, which is a common source of confusion: #58B9FF becomes 5814783.
Embed fields have their own separate character limits which are more generous than the 2,000 for content, so if you are pushing long-form summaries, the description field of an embed is the place to put them rather than the message body.
Uploading Files
Discord's documentation directs you to its uploading files reference for attachments, which use multipart/form-data rather than JSON.
curl -X POST "$DISCORD_WEBHOOK_URL" \
-F 'payload_json={"content":"This week in numbers"}' \
-F 'files[0]=@chart.png'
The JSON body goes in a payload_json part and each file gets its own part. You can reference an uploaded file from an embed using the attachment://filename.png scheme, which is how you get an image into an embed without hosting it publicly first.
For most publishing workflows, passing a public image URL in an embed is simpler and avoids the multipart handling entirely. Our publishing layer does the same thing across networks so the media path is consistent.
Discord Limits and Media Specs
These are the constraints our own platform rules enforce for Discord, which is to say the ones that cause real failures:
| Constraint | Value |
|---|---|
| Message character limit | 2,000 |
| Embeds per message | 10 |
| Image file size | 25 MB |
| Image formats | JPG, PNG, WebP, GIF |
| Video file size | 25 MB |
| Video max duration | 3,600 seconds |
| Video formats | MP4, MOV, WebM (H.264) |
The 2,000 character limit is the lowest of any network we publish to. The Telegram Bot API allows 4,096, LinkedIn 3,000, Instagram 2,200 and X 25,000. If you are cross-posting one piece of copy everywhere, Discord is almost always the constraint that forces a truncation, so write to its limit or maintain a separate short variant.
File size limits are also affected by the server's boost level, which can raise the ceiling above the base figure. Do not assume a larger upload will succeed just because it worked on a boosted server.
Rate Limits and Retries
Discord is unusually transparent about rate limiting, and building against it properly is straightforward if you read the headers.
Per its rate limit documentation, all bots can make up to 50 requests per second globally, independent of per-route limits. Exceeding this consistently can result in temporary IP bans, so this is not a limit to probe.
Responses carry X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Reset-After and X-RateLimit-Bucket. On a 429 you also get X-RateLimit-Scope, whose value is user, global or shared.
The correct handling is to rely on the Retry-After header or the retry_after field in the body to decide when to retry, rather than a fixed backoff of your own design.
Reading X-RateLimit-Remaining and pausing before you hit zero is better still, since it avoids the 429 entirely.
How Discord's 50-per-second compares with the other networks is set out in our API rate limit reference. The X-RateLimit-Bucket value matters if you post to many channels: limits are tracked per bucket rather than globally per route, so a queue that treats all sends as one limit will either be too slow or too aggressive.
Our guide to API posting automation covers queue design that handles this pattern.
Posting to Forum Channels
Forum and media channels behave differently and this catches people out. Per Discord's webhook documentation, posting to one requires either a thread_id query parameter to post into an existing thread, or thread_name in the request body to create a new one.
Omit both and the request fails. If your community uses forum channels for announcements, which many do, use thread_name so each post creates its own discussion thread, and see our scheduling API guide for how to store that alongside pending posts:
{
"thread_name": "September product update",
"content": "Here is what shipped this month."
}
When to Use a Publishing API Instead
For Discord alone, use the webhook. It is free, takes minutes, and has no meaningful downside. Building a whole abstraction over a single webhook call would be silly. If you want to schedule Discord messages without writing code at all, PostEverywhere covers that from a calendar.
The calculation changes once Discord is one destination among several. Each network you add brings a different auth model, a different media pipeline and a different set of limits. Telegram uses a static bot token, Discord uses a webhook URL, Meta requires OAuth plus app review, TikTok requires approval, and X now charges per call. Nothing transfers between them except your own scheduling code.
A unified publishing API turns that into one endpoint and one auth model, with per-platform formatting handled for you, including the 2,000 character truncation Discord will otherwise impose without warning. Scheduling becomes a parameter rather than infrastructure you build, run and monitor.
If an assistant rather than a service drives your publishing, our connectors wire Claude, ChatGPT and Cursor directly into posting, and the agent framework comparison covers building it yourself.
All top platforms, one integration. PostEverywhere handles auth, media, per-platform limits and retries. From $9/mo, 7-day trial, card required. See pricing.
FAQ: Discord API Posting
How do I post to a Discord channel with an API?
Create a webhook in the channel settings, then send a POST request to https://discord.com/api/webhooks/{webhook.id}/{webhook.token} with a JSON body containing content. No bot, OAuth flow or application review is required, and there is no per-message fee.
Do I need a bot to post to Discord?
No. A webhook is sufficient for posting messages into a single channel and is the simpler option. You need a bot only if you also want to read messages, respond to commands, manage roles, or act across many channels under one identity.
What is the Discord message character limit?
Message content supports up to 2,000 characters, the tightest limit of any major social platform. Embed description fields allow more, so long summaries belong in an embed rather than the message body. A message can include up to 10 embed objects.
Is the Discord API free?
Yes. Discord does not charge for API or webhook access and there is no per-message fee. Rate limits apply, with bots limited to 50 requests per second globally, but no billing is attached to sending messages.
How do I handle Discord rate limits?
Read the X-RateLimit-Remaining and X-RateLimit-Reset-After headers and pause before the remaining count reaches zero. If you do receive a 429, use the Retry-After header or the retry_after field in the body to decide when to retry rather than a fixed backoff. Limits are tracked per bucket, indicated by X-RateLimit-Bucket.
Why does my webhook fail on a forum channel?
Forum and media channels require either a thread_id query parameter to post into an existing thread, or thread_name in the request body to create a new one. A request omitting both will fail. Standard text channels do not need either field.
Can I schedule posts with a Discord webhook?
No. The webhook endpoint publishes immediately and has no scheduling parameter, so future-dated posting requires your own cron job or queue plus storage for pending messages. A scheduling platform handles timing as a parameter instead.
What does a successful Discord webhook request return?
By default it returns 204 No Content with an empty body, so attempting to parse JSON from the response will fail. Append ?wait=true to the webhook URL if you need the created message object returned instead.

Founder & CEO of PostEverywhere. Writing about social media strategy, publishing workflows, and analytics that help brands grow faster.



