Skip to main content

How to format a Discord webhook message

A Discord webhook message is a JSON body sent as an HTTP POST to the webhook URL. Plain text goes in content, and rich cards with a title, a color, and labelled fields go in an embeds array. Every message needs at least one of the two, and Discord answers a successful post with 204 No Content.

Sending webhooks?
Svix is the enterprise-ready webhook sending service. It handles signing, retries, and delivery observability, so you can ship a reliable webhook platform in minutes instead of months. Start sending webhooks with Svix.

What goes in the request body

You need a webhook URL first, which our guide on how to make a webhook in Discord walks through. Once you have one, the smallest message Discord accepts carries a single field:

curl -X POST \
-H 'Content-Type: application/json' \
--data '{"content": "Deploy 1.4.2 finished"}' \
https://discord.com/api/webhooks/example-hash

content is the message text, up to 2000 characters, and it renders with Discord's markdown, so bold text, backtick code spans, and fenced code blocks all work. Three optional fields sit alongside it. username and avatar_url override the name and icon you chose when creating the webhook, which is how one URL posts as "Deploy bot" for one message and "Alert bot" for the next. tts reads the message aloud for anyone who has text-to-speech enabled.

A POST carrying none of content, embeds, or a file attachment comes back as a 400 with "Cannot send an empty message". That is the most common first error, and it usually means a template rendered to an empty string.

Embeds turn a message into a card

An embed is the bordered card with a colored stripe down its left edge. Send an embeds array instead of content, or alongside it:

{
"username": "Deploy bot",
"embeds": [
{
"title": "Deploy 1.4.2 finished",
"description": "Rolled out to production in 4m 12s.",
"url": "https://ci.example.com/builds/1842",
"color": 5793266,
"timestamp": "2026-09-11T09:24:00.000Z",
"fields": [
{ "name": "Service", "value": "checkout-api", "inline": true },
{ "name": "Commit", "value": "a41f9c2", "inline": true },
{ "name": "Migrations", "value": "2 applied", "inline": false }
],
"footer": { "text": "example-ci" }
}
]
}

title is the bold heading and url turns it into a link. description is the body text and accepts markdown. color is the stripe, given as a decimal integer rather than a hex string, so #5865F2 becomes 5793266. timestamp must be ISO 8601 and renders as a relative time in the card's corner. fields are the name and value pairs: inline: true packs up to three of them side by side on a desktop client, and false gives a field its own row. footer, author, thumbnail, and image fill the remaining slots.

The limits that actually bite

Discord rejects the entire message when any one of these is exceeded, so budget for them before building a payload out of data you do not control:

  • content: 2000 characters
  • Embeds per message: 10
  • Combined text across every embed in one message: 6000 characters
  • Embed title: 256 characters, description: 4096 characters
  • Fields per embed: 25, with names capped at 256 characters and values at 1024
  • Requests per webhook: roughly 30 per minute, after which Discord replies 429 with a retry_after value

The 6000-character total is the one that surprises people, because each embed can pass its own check while the message still fails. Truncate long values yourself, with an ellipsis and a link to the full record, rather than letting a stack trace decide whether the alert arrives at all. Hitting the rate limit is a sign the sending side needs a queue and a rate limit of its own.

Stop a message from pinging everyone

Discord parses mentions out of content by default, so a message that echoes user input can ping a role or an entire server. allowed_mentions controls that independently of the text:

{
"content": "Alert from @everyone reported by <@1234567890>",
"allowed_mentions": { "parse": [] }
}

An empty parse array renders those mentions as ordinary text and notifies nobody. Pass {"parse": ["users"]} to allow user pings but never role or @everyone pings, or name specific IDs in users and roles. Setting this on every automated message is the cheapest way to avoid the incident where a log line wakes 400 people at 3am.

Getting the message back so you can edit it

The default 204 No Content leaves you nothing to work with. Add ?wait=true to the URL and Discord answers 200 with the full message object, including its id. Keep that id and you can rewrite the message later with a PATCH to /webhooks/{webhook_id}/{webhook_token}/messages/{message_id}, or remove it with a DELETE to the same path. That is how a status message updates in place from "deploying" to "deployed" instead of posting three separate times.

Two parameters change where a message lands rather than what it looks like: ?thread_id= in the query string posts into an existing thread, and thread_name in the body creates a new post in a forum channel.

Formatting for Discord is a bounded problem because Discord publishes the schema and enforces it the same way every time. Sending events to your own customers is the harder version of the same job, since you own the retries, the signatures, and the delivery log. Svix handles that side. If a GitHub integration is posting to your Discord webhook and the requests come back rejected, see webhook error 400 from GitHub to Discord.

Frequently asked questions

Can one Discord webhook post to more than one channel?

No. A webhook URL is bound to the channel chosen when it was created, and a channel field in the body is ignored. Use the thread_id query parameter to reach a thread inside that channel, or create a second webhook for a second channel.

Why does my embed have no color?

The color field takes a decimal integer, not a hex string. Convert the hex value first: #5865F2 is 5793266. A string like "#5865F2" will not render the stripe.

Do I need a bot token to send a Discord webhook message?

No. The token inside the webhook URL is the only credential, so a plain HTTP POST with no Authorization header works. That also means the URL is a secret: anyone holding it can post to the channel.

Ready to send webhooks?

Svix handles signing, retries, rate limiting, and delivery observability for the webhooks you send to your users, so your team can stay focused on your product.