ReferenceFoundation/7 min read

Discord webhooks: post to a channel without running a bot

What a Discord webhook is, how to create one from the channel settings or through OAuth, the exact POST that publishes a message, and the five limits that break integrations in production.

Kavish Shah

Founder of Cadencz. Writes the Academy from the work of building it.

Published

Class notes

What you will be able to do

  • A webhook is a per-channel URL that accepts a POST and publishes the result. No bot, no gateway connection, no hosting — which is why it is the right tool for scheduled publishing and the wrong one for anything interactive.
  • Add `?wait=true` to the URL or Discord answers 204 with an empty body and you never learn the message id. Without that id you cannot edit or delete the message later.
  • The message cap is 2,000 characters. Go over and the message is silently converted into a file attachment, which reads to everyone in the channel as a broken post rather than a long one.
  • A webhook cannot upload a file without a multipart request, but it can render up to ten images inline by sending one embed per image — same result, one round trip, no file handling.
  • Webhooks are write-only. There is no webhook endpoint that returns reactions, replies, or view counts, so any Discord analytics you want needs a real bot token and channel scope instead.

A Discord webhook is a URL tied to one channel. POST JSON to it and the JSON becomes a message in that channel. There is no bot to write, no gateway socket to keep open, no process to host, and no token that can read anything. That narrowness is the whole point: it makes publishing to Discord roughly as hard as calling any other HTTP endpoint.

Everything below comes from building the Discord integration in Cadencz, which publishes scheduled posts to customers' channels this way. The traps in the last section are the ones that cost us real debugging time, not a list transcribed from the documentation.

Webhook or bot? Answer this first

Almost every Discord integration question resolves to one decision, and getting it wrong costs weeks. A webhook can only speak. A bot can listen, respond, read history, and see reactions — and in exchange it needs a token, a hosting story, a gateway connection, and Discord's verification process once it reaches 100 servers.

Reviewed September 5, 2026 against Discord's developer documentation. Verify against the linked sources before building on any specific behaviour.
You want to…WebhookBot
Post a scheduled or automated messageYes — this is exactly what it is forWorks, but is far more machinery than the job needs
Post as a custom name and avatar per messageYes, set `username` and `avatar_url` in the bodyNo — a bot posts as itself
Read replies, reactions, or member countsNo. There is no read endpoint at allYes, with the right gateway intents
Respond to a slash commandNoYes
Upload a fileOnly via a multipart requestYes
Be installed by a non-technical userYes — the OAuth flow hands back a ready webhookRequires the bot to be invited and permissioned
Survive the person who set it up leavingYes, the URL is not tied to a user accountYes

Creating one: two routes

By hand, for your own server: open the channel, Edit Channel, Integrations, Webhooks, New Webhook. Copy the URL. It looks like `https://discord.com/api/webhooks/{id}/{token}` and it is a credential — anyone holding it can post to that channel, so treat it exactly like an API key.

In a product, where the user is installing your integration: use the OAuth2 flow with the `webhook.incoming` scope. Discord shows the user a channel picker as part of authorization, and the token response comes back with a fully-formed webhook object — id, channel, and URL — already created against the channel they chose. You never handle their credentials and they never paste a URL. This is how Cadencz connects Discord: the user picks a channel, and what we store is the webhook URL itself.

The request that publishes a message

One POST, JSON body, no authentication header — the token is already in the URL. The `content` field is the message text. `username` and `avatar_url` override the webhook's configured identity for that single message, which is how one webhook can post as several different senders.

  • Always send `?wait=true`. Discord's default is fire-and-forget: 204, no body, no id. With it you get 200 and the full message object.
  • `content` is optional if you send `embeds`, and vice versa — but a request with neither is rejected.
  • A 2xx means Discord accepted the message, not that every member can see it; channel permissions still apply.

The minimum that works

POST https://discord.com/api/webhooks/{id}/{token}?wait=true Content-Type: application/json { "content": "Deploy finished. 4 minutes, no errors." }

With a per-message identity and inline images

{ "content": "This week's shipped list", "username": "Release Bot", "avatar_url": "https://example.com/icon.png", "embeds": [ { "image": { "url": "https://example.com/chart.png" } } ] }

The version that loses the message id

POST https://discord.com/api/webhooks/{id}/{token} { "content": "Deploy finished." } → 204 No Content, empty body. The message posted, but you have no id, so you can never edit or delete it programmatically.

The five limits that actually break things

  • 2,000 characters. Over that, Discord converts the message into a `message.txt` attachment rather than rejecting it. Nothing errors, and the channel sees a file where a post should be — so validate length before sending, not after.
  • Ten embeds per message. An eleventh is dropped silently. If you are rendering a gallery, cap it yourself so the omission is a decision rather than a surprise.
  • No file upload from a plain JSON request. Attaching a real file needs `multipart/form-data`. The shortcut that avoids it entirely: host the image and send one embed per image URL, which renders inline and costs no extra round trip.
  • Rate limits are per webhook and per channel, and Discord answers a 429 with a `retry_after` value in the body. Honour that number; retrying immediately extends the block rather than clearing it.
  • Deleting the channel deletes the webhook. The URL then returns 404 forever, which is indistinguishable from a revoked webhook — so surface it to the user as "reconnect this channel" rather than as a transient failure.

Why you get no Discord analytics

This surprises people who have wired up publishing and then go looking for engagement numbers. There is no webhook endpoint that returns reactions, replies, or views — not one that is undocumented or rate-limited, one that does not exist. Reading anything back from a Discord channel requires a bot token with the appropriate gateway intents, which is a different integration with a different install flow.

That is a permanent property of the transport, not a gap someone will close. It is worth knowing before you promise a customer a Discord engagement chart. Cadencz records it explicitly for that reason: Discord is a publish-only channel, and the analytics page says so rather than showing an empty graph.

Sources used

Sources and freshness

Sources were reviewed on . Product features, prices, and platform rules change; follow the source links before relying on a volatile detail.

  1. 01Intro to WebhooksDiscord Support
  2. 02Webhook Resource: Execute WebhookDiscord Developer Documentation
  3. 03OAuth2: WebhooksDiscord Developer Documentation
  4. 04API Reference: Uploading FilesDiscord Developer Documentation
  5. 05API Reference: Rate LimitsDiscord Developer Documentation
  6. 06Sending MessagesDiscord Support

Keep learning

01Social media character limits in 2026: every platform, one sourced tableFit every platform's real limit without truncating a master caption.
02Social media platform playbook: what works on every Cadencz channelChoose the right content shape and interaction goal for each channel.
03Telegram channel marketing for startups: run an opt-in publicationSet up and grow a startup Telegram channel that readers keep.