Skip to main content

Discord Webhook Tester: Validate the URL and Send a Test Message

Discord webhook tester guide: validate the URL with GET, send a test message with curl or the online builder, read 400/404/429 errors and rotate leaked URLs.

8 min read Also available in Русский
On this page

You have a webhook URL and a payload that “should work”. Before wiring it into a CI pipeline, a monitoring script or a bot, run it through a short test loop: confirm the URL is alive, send one visible message, look at the exact status code and body Discord returns. This guide is that loop: first with curl, then with the online builder, followed by the failures you will actually hit and what to do when a URL leaks.

Replace https://discord.com/api/webhooks/ID/TOKEN in every example with your own URL. If you do not have one yet, see how to get a Discord webhook URL.

Step 1: Validate the URL without sending anything

A webhook URL has two parts: the numeric id and the token. A plain GET request to that URL asks Discord to describe the webhook. It does not post a message, so it is the safest first check:

curl -s https://discord.com/api/webhooks/ID/TOKEN

If the URL is valid, you get HTTP 200 and the webhook object:

{
  "type": 1,
  "id": "1234567890123456789",
  "name": "Deploy Notifier",
  "avatar": null,
  "channel_id": "9876543210987654321",
  "guild_id": "1122334455667788990",
  "application_id": null,
  "token": "abc…"
}

Three fields matter for testing. name confirms you grabbed the right webhook. channel_id tells you where messages will land; if that is not the channel you expect, stop here. type: 1 means an incoming webhook, the kind you can execute with a URL.

If the URL is wrong, truncated or the webhook was deleted, the answer is a 404 with a JSON body:

{ "message": "Unknown Webhook", "code": 10015 }

Whether one character of the token is missing or the whole webhook was removed, the fix is the same: re-copy the URL from Server Settings → Integrations → Webhooks before you debug anything else.

You can also paste the URL into a browser address bar: browsers send GET, so you get the same JSON with no side effects. Just never paste it anywhere public; the URL itself is the secret.

Step 2: Send a test message with curl

Once GET returns 200, send the smallest possible message. Use -i so curl prints the status line and headers along with the body:

curl -i -X POST https://discord.com/api/webhooks/ID/TOKEN \
  -H "Content-Type: application/json" \
  -d '{"content": "Webhook test, please ignore"}'

Success is 204 No Content with an empty body. By default Discord does not echo the message back; add ?wait=true to get the message object:

curl -s -X POST "https://discord.com/api/webhooks/ID/TOKEN?wait=true" \
  -H "Content-Type: application/json" \
  -d '{"content": "Webhook test with wait=true"}'

The response is now 200 with JSON containing the message id, its channel_id and the rendered content. Keep the id: you need it to edit or delete the message later via PATCH or DELETE on /messages/{message_id} (see editing and deleting webhook messages).

On Windows, run these in PowerShell as curl.exe (in Windows PowerShell 5.1 plain curl is an alias for Invoke-WebRequest), or use the native approach from Discord webhooks in PowerShell.

A reusable tester script

This script does both steps and prints only what you need: the status code and, on failure, the body. It needs curl and jq.

#!/usr/bin/env bash
# usage: ./webhook-test.sh "https://discord.com/api/webhooks/ID/TOKEN"
set -u
URL="$1"

echo "== GET (no message is sent)"
code=$(curl -s -o /tmp/wh.json -w "%{http_code}" "$URL")
echo "status: $code"
if [ "$code" != "200" ]; then cat /tmp/wh.json; echo; exit 1; fi
echo "name: $(jq -r .name /tmp/wh.json), channel_id: $(jq -r .channel_id /tmp/wh.json)"

echo "== POST test message"
code=$(curl -s -o /tmp/wh.json -w "%{http_code}" \
  -H "Content-Type: application/json" \
  -d '{"content": "✅ webhook test", "allowed_mentions": {"parse": []}}' \
  "$URL?wait=true")
echo "status: $code"
[ "$code" = "200" ] && echo "message id: $(jq -r .id /tmp/wh.json)" || cat /tmp/wh.json

allowed_mentions: {"parse": []} is there on purpose: even if a test string contains @everyone, nobody gets pinged.

Step 3: Test online without code

When you want to check a full embed layout, a file upload or a Components V2 container, curl gets tedious. The visual builder at discord-webhook.com runs the same loop from a browser: paste the URL, build the message with a live preview, hit send and look at the channel. It also exports the payload as JSON or as discord.js / discord.py code, so the exact message you tested is what ends up in your project. No signup is required.

Reading the response codes

Everything Discord tells you about a webhook request is in the status code and the JSON body. Memorise these five:

StatusMeaningWhat to do
204 No ContentMessage posted (default POST)Nothing; check the channel
200 OKMessage posted and returned (?wait=true), or the webhook object (GET)Store the message id if you need it
400 Bad RequestThe JSON is malformed or a field breaks a limitRead errors in the body; fix the named field
404, code 10015Unknown Webhook: bad id/token or deleted webhookRe-copy or recreate the URL
401, code 50027Invalid Webhook Token: the id exists but the token is wrongRe-copy the full URL
429 Too Many RequestsRate limitedWait retry_after seconds, then retry

A 400 body names the exact path of the offending field. An embed description longer than 4096 characters produces an error under embeds.0.description, a username containing “discord” is rejected under username, and a body with no content, embeds, files, poll or components comes back as “Cannot send an empty message”. The full catalogue is in Discord webhook errors explained.

Print the body on every non-2xx response. A surprising amount of “the webhook does not work” turns out to be a 400 that nobody read.

Test an embed before shipping it

Plain text hides most limit problems, so test the shape you will actually send. This embed uses the fields people get wrong most often: colour as a decimal integer, an ISO 8601 timestamp, inline fields.

curl -s -w "\nHTTP %{http_code}\n" \
  -X POST "https://discord.com/api/webhooks/ID/TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "Deploy Bot",
    "embeds": [{
      "title": "Test deploy",
      "description": "Build **#128** passed",
      "color": 5793266,
      "fields": [
        {"name": "Env", "value": "staging", "inline": true},
        {"name": "Duration", "value": "42s", "inline": true}
      ],
      "timestamp": "2026-09-17T10:00:00.000Z"
    }]
  }'

Expect an empty body and HTTP 204. If you see 400, the body printed above the status line will point at the field. The limits that trip tests most often: 256 characters for a title, 4096 for a description, 25 fields per embed, 6000 characters across all embeds in one message, 10 embeds per message. color must be a number like 5793266, not "#5865F2". Image and thumbnail URLs must be public https links.

To test a file upload, switch to multipart:

curl -s -w "\nHTTP %{http_code}\n" \
  -F 'payload_json={"content": "Log attached"}' \
  -F "files[0]=@./app.log" \
  https://discord.com/api/webhooks/ID/TOKEN

Common errors

{"message": "Unknown Webhook", "code": 10015}: the id or token is wrong, or the webhook was deleted. Common causes: a trailing newline or quote copied with the URL, a URL from a different server, or a teammate rotated the webhook. Run the GET check; if it fails, recreate the webhook.

400 with "Cannot send an empty message": the body has no content, embeds, files, poll or components. Often the JSON is fine but the field name is wrong ("text" instead of "content") or the shell mangled the quotes in -d.

400 pointing at embeds.0.…: a limit was exceeded or a type is wrong; fix the field named in the path. Limits are listed above and in embed limits.

400 on username: the override may not contain “clyde” or “discord” (case-insensitive) and is capped at 80 characters.

429 Too Many Requests: the body contains retry_after in seconds. Sleep that long and retry. Discord does not publish a single official number for webhook limits; see webhook rate limits before you tune a retry policy.

Sending JSON without the header: without Content-Type: application/json, curl labels the body application/x-www-form-urlencoded, Discord does not parse it as JSON and answers 400. Always set the header.

204 but nothing in the channel: you are looking at the wrong channel. channel_id from the GET response is authoritative. If you sent ?thread_id=…, the message is in that thread.

Interactive components ignored or rejected: a plain webhook may send only non-interactive components (link buttons, Components V2 layout) and only with ?with_components=true. Buttons that do something need an application; see interactive buttons with actions.

Test in a private channel first

A webhook posts to exactly one channel, and every member who can see that channel sees your tests. So:

  1. Create a private text channel (#webhook-tests) that only you and the people debugging with you can read.
  2. Create the webhook there and run the whole loop above: GET, text message, embed, file.
  3. When the payload is right, either create a second webhook in the production channel and swap the URL in your config, or move the existing webhook to the production channel in its settings.

Keep the test channel: it is the cheapest place to reproduce a failing payload later. And always send tests with "allowed_mentions": {"parse": []} so a stray @everyone in a copied template never pings the server.

Rotate a leaked URL

The token is the only thing standing between the internet and your channel. If the URL was pasted into a public chat, committed to a repository or shipped in a client-side bundle, treat it as compromised:

  1. Server Settings → Integrations → Webhooks → open the webhook → Delete. The old URL starts returning Unknown Webhook immediately.
  2. Create a new webhook in the same channel and copy the new URL.
  3. Update every place that used the old URL: environment variables, CI secrets, .env files, automation platforms.
  4. Re-run the GET check with the new URL, then one test message.

Confirm the rotation from the other side, too: a GET against the old URL must return 404. For the full checklist (secrets storage, scanning repositories, limiting blast radius) read Discord webhook security.

FAQ

Does a GET request to the webhook URL send a message?

No. GET returns the webhook object (name, channel_id, guild_id, token) and posts nothing. Only POST executes the webhook.

How do I test a Discord webhook online without curl?

Open the webhook URL in a browser to validate it (a GET), then compose and send the test message from the online builder described in Step 3: live preview, no signup, no code.

Why do I get 204 but see no message?

204 means Discord accepted the message. It went to the channel in channel_id from the GET response, or into the thread you passed as ?thread_id. Check that channel and the webhook’s channel setting.

What is the fastest way to tell if a webhook URL is still valid?

Send a GET request. 200 with a JSON webhook object means it works; 404 with "code": 10015 means it is wrong or deleted.

Next steps

The test loop is short: GET to validate, one POST to confirm, read the status and body on anything that is not 2xx. Do it in a private channel, and rotate the URL the moment it leaks. If you would rather skip the terminal, the Discord Webhook builder runs the same loop with a live preview and exports the tested payload as JSON or code.

Related reading: Discord webhook errors explained, how to get a Discord webhook URL, Discord webhook security.

Tags: discord webhook testertest discord webhookwebhook test messagecurldebuggingwebhook errorsdiscord webhook

Related articles

All articles

Build it in the visual editor

Embeds, Components V2, buttons and polls with a live Discord preview. Free, no signup.