Edit and Delete Discord Webhook Messages (PATCH & DELETE)
How to edit and delete messages sent through Discord webhooks. Working PATCH and DELETE examples in Python, JavaScript and curl — including how to capture message IDs.
Most webhook tutorials stop at “send a message”. But once that message is in Discord, you often need to update it (live status board, build progress, evolving incident) or delete it (typo, retracted alert). Both are doable with the webhook URL alone — no bot token required — but the request shape is unusual.
This guide covers exactly how to PATCH and DELETE webhook messages, including the trick most people miss: you must send ?wait=true on the original POST to get the message ID back.
The Three Endpoints
Given a webhook URL https://discord.com/api/webhooks/{webhook.id}/{webhook.token}:
| Action | Method | URL |
|---|---|---|
| Send a message | POST | /webhooks/{id}/{token} |
| Edit a message | PATCH | /webhooks/{id}/{token}/messages/{message.id} |
| Delete a message | DELETE | /webhooks/{id}/{token}/messages/{message.id} |
You need the message ID for edit and delete. Discord doesn’t return it by default — you must ask for it.
Step 1: Get the Message ID Back
Append ?wait=true to the POST URL. Discord then returns the full message object (including id) in the response body, with status 200 instead of 204.
import requests
WEBHOOK_URL = "https://discord.com/api/webhooks/ID/TOKEN"
r = requests.post(
WEBHOOK_URL + "?wait=true",
json={"content": "Build started…"},
timeout=10,
)
r.raise_for_status()
message_id = r.json()["id"]
print(f"Sent message id={message_id}")
Without ?wait=true you get a 204 No Content and an empty body — your message went out but you have no handle on it.
Step 2: Edit the Message
PATCH with the same JSON shape you’d POST. Any field you include is overwritten; fields you omit are left unchanged.
edit_url = f"{WEBHOOK_URL}/messages/{message_id}"
r = requests.patch(
edit_url,
json={"content": "Build completed in 2m 14s"},
timeout=10,
)
r.raise_for_status()
You can edit:
contentembeds(replaces all embeds — pass empty array to clear)components(buttons / select menus)attachments(more on this below)allowed_mentions
You cannot edit username, avatar_url, or tts — these are locked at send time.
Live Status Board Example
import time
import requests
WEBHOOK_URL = "https://discord.com/api/webhooks/ID/TOKEN"
# Initial message
r = requests.post(
WEBHOOK_URL + "?wait=true",
json={
"embeds": [{
"title": "Deployment in progress",
"description": "⏳ Step 0 / 4 — preparing",
"color": 0xfee75c,
}]
},
)
mid = r.json()["id"]
steps = ["building Docker image", "running tests", "pushing to registry", "rolling restart"]
for i, step in enumerate(steps, 1):
time.sleep(5) # do real work here
requests.patch(
f"{WEBHOOK_URL}/messages/{mid}",
json={
"embeds": [{
"title": "Deployment in progress",
"description": f"⏳ Step {i} / {len(steps)} — {step}",
"color": 0xfee75c,
}]
},
)
# Final state
requests.patch(
f"{WEBHOOK_URL}/messages/{mid}",
json={
"embeds": [{
"title": "Deployment completed",
"description": "✅ All 4 steps finished",
"color": 0x57f287,
}]
},
)
This produces one Discord message that updates in place — no chat spam.
Step 3: Delete the Message
import requests
WEBHOOK_URL = "https://discord.com/api/webhooks/ID/TOKEN"
message_id = "1234567890123456789"
r = requests.delete(f"{WEBHOOK_URL}/messages/{message_id}", timeout=10)
r.raise_for_status() # 204 No Content on success
Delete is one-shot: there’s no “undelete”. Webhook deletes work even on messages older than 14 days (unlike bulk-delete via the bot API).
JavaScript / Node.js
const WEBHOOK = process.env.WEBHOOK_URL;
// Send + capture id
async function send(payload) {
const res = await fetch(`${WEBHOOK}?wait=true`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Send failed: ${res.status}`);
return (await res.json()).id;
}
// Edit
async function edit(messageId, payload) {
const res = await fetch(`${WEBHOOK}/messages/${messageId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Edit failed: ${res.status}`);
}
// Delete
async function del(messageId) {
const res = await fetch(`${WEBHOOK}/messages/${messageId}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
}
// Usage
const id = await send({ content: 'Working…' });
await new Promise(r => setTimeout(r, 5000));
await edit(id, { content: 'Done in 5s' });
curl
WEBHOOK="https://discord.com/api/webhooks/ID/TOKEN"
# Send and capture ID
MID=$(curl -s -H 'Content-Type: application/json' \
-d '{"content":"Initial"}' \
"$WEBHOOK?wait=true" | jq -r '.id')
# Edit
curl -X PATCH \
-H 'Content-Type: application/json' \
-d '{"content":"Updated"}' \
"$WEBHOOK/messages/$MID"
# Delete
curl -X DELETE "$WEBHOOK/messages/$MID"
jq makes parsing the JSON response easy. Without it, use python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])'.
Editing Attachments
Editing files is non-obvious. Two cases:
Case 1: Remove all attachments. Send attachments: [] in the PATCH body.
Case 2: Add a new file or replace existing. Use multipart/form-data (same as upload), and include in payload_json an attachments array describing what to keep:
# Keep existing attachment id=123, add new file
files = {"files[0]": ("new.png", open("new.png", "rb"), "image/png")}
payload_json = '{"attachments":[{"id":"123"},{"id":"0"}]}'
requests.patch(
f"{WEBHOOK_URL}/messages/{mid}",
data={"payload_json": payload_json},
files=files,
)
The new file’s id matches its files[N] index (so files[0] → id: "0").
Threading
If you sent the message into a thread (?thread_id={tid}), you must include ?thread_id= on edit/delete too:
requests.patch(
f"{WEBHOOK_URL}/messages/{mid}?thread_id={tid}",
json={"content": "Updated thread message"},
)
Otherwise Discord can’t locate the message and returns 404 Unknown Message.
Common Errors
| Status | Likely cause |
|---|---|
| 404 Unknown Message | Wrong message ID, wrong webhook, missing ?thread_id= |
| 401 Invalid Webhook Token | Webhook regenerated or revoked |
| 403 Cannot edit message authored by another user | Trying to edit a message not sent by this webhook |
| 400 Cannot send empty message | Edit must leave at least one of: content, embeds, attachments, components |
Persisting Message IDs
For long-running services (status pages, dashboards), save the ID to your DB or a file:
from pathlib import Path
import json, requests
STATE = Path("/var/lib/myapp/discord_state.json")
def get_or_create_status_message():
if STATE.exists():
data = json.loads(STATE.read_text())
return data["message_id"]
r = requests.post(WEBHOOK_URL + "?wait=true", json={"content": "Status: starting"})
mid = r.json()["id"]
STATE.write_text(json.dumps({"message_id": mid}))
return mid
Now you can PATCH the same message across process restarts.
Try It in the Builder
The Discord Webhook builder gives you a copy-able message ID after every send and supports edit/delete from the UI — useful for prototyping update flows before automating them.
For more advanced patterns, see scheduled messages and the Python integration guide.
References
- Discord docs: Edit Webhook Message
- Discord docs: Delete Webhook Message
Try it in our tool
Open Discord Webhook Builder