Webhooks
Execute a webhook
Post a message with a POST to the webhook’s URL, then edit or delete it with the same URL. The request body, mentions, rate limits and errors, with examples in curl, JavaScript and Python.
Post a message
POST https://api.klang.gg/api/webhooks/{webhook.id}/{webhook.token}No session, no cookies and no Authorization header: the token in the URL is the
authorization. Send the body as JSON with Content-Type: application/json. A
multipart/form-data request with the JSON in a payload_json field works too, because some
libraries send it that way, but files aren’t supported yet.
Call it from a server or a script. Browsers block the request from a web page, and that’s on purpose: the URL is a secret and doesn’t belong in a page’s code.
curl -X POST "$KLANG_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{"content": "Backup finished: 2.4 GB in 3 min.", "username": "Backup bot"}'const response = await fetch(process.env.KLANG_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: 'Backup finished: 2.4 GB in 3 min.',
username: 'Backup bot',
}),
});
if (!response.ok) {
throw new Error(`Klang answered ${response.status}: ${await response.text()}`);
}import os
import requests
response = requests.post(
os.environ["KLANG_WEBHOOK_URL"],
json={"content": "Backup finished: 2.4 GB in 3 min.", "username": "Backup bot"},
)
response.raise_for_status()The request body
| Field | Type | What it does |
|---|---|---|
content |
string | The message’s text, up to 4,000 characters. Markdown, mentions and links work as in any message. |
username |
string | A name for this message only, instead of the webhook’s. If it isn’t allowed (it contains “Klang”, for example), the message goes out with the webhook’s name, without an error. |
avatar_url |
string | An avatar for this message only: an https link to an image. |
embeds |
array | Up to 10 rich embeds. See Embeds. |
allowed_mentions |
object | Which mentions in content notify. See Mentions. |
flags |
integer | Only SUPPRESS_EMBEDS (1 << 2, that is 4): no link previews for this message. Its own embeds still show. Other flags are ignored. |
Every message needs content, at least one embed, or both. Fields Klang doesn’t know are
ignored, as in Discord, so a body written for Discord works as it is.
Not supported yet
Files and attachments, components, poll and tts are ignored, so an integration that sends
them still gets its message posted (a file in a multipart/form-data request is skipped and the
rest is posted). Klang has no threads, so thread_name and the thread_id query parameter get a
400: posting somewhere other than where the integration expects would be worse.
Query parameters
| Parameter | Type | What it does |
|---|---|---|
wait |
boolean | With true, Klang answers with the message it posted. Without it, it answers 204 No Content as soon as the message is in. |
The response
Without ?wait=true, a 204 No Content with an empty body: the message is posted. With it, a
200 with the message, in Discord’s shape and field names:
{
"id": "1304817203949158400",
"channel_id": "1288405532170813440",
"webhook_id": "1301562948702683136",
"content": "Backup finished: 2.4 GB in 3 min.",
"timestamp": "2026-11-08T18:30:00.000Z",
"author": {
"id": "1301562948702683136",
"username": "Backup bot",
"avatar": null,
"bot": true
},
"embeds": []
}IDs come as strings. Keep id if you want to edit or delete the
message later.
Mentions
Mentions go in content as in any Klang message: <@user_id> for a person, <@&role_id> for a
role, and @everyone and @here.
- People are notified as in any message.
- Roles are notified only if the role is mentionable.
@everyoneand@herenotify only if the webhook has Can mention @everyone turned on. It’s off by default, and only people who can mention everyone in that channel can turn it on. Without it the text shows, but nobody gets notified, so a leaked URL can’t ping the whole server.
allowed_mentions narrows that down further, as in Discord:
| Field | Type | What it does |
|---|---|---|
parse |
array of strings | The kinds of mention that notify: "users", "roles" and "everyone" (@everyone and @here). |
users |
array of IDs | Only these people are notified. Don’t send it together with "users" in parse. |
roles |
array of IDs | Only these roles are notified. Don’t send it together with "roles" in parse. |
allowed_mentions can only take notifications away. It can’t make a role that isn’t
mentionable notify, nor @everyone on a webhook that doesn’t allow it.
{
"content": "<@1201933402114236416> your server is back up. @everyone can join now.",
"allowed_mentions": { "parse": ["users"] }
}Here the person is notified and @everyone is just text.
Edit or delete a message
A webhook can edit and delete the messages it posted, with the same URL and the message’s ID:
PATCH https://api.klang.gg/api/webhooks/{webhook.id}/{webhook.token}/messages/{message.id}
DELETE https://api.klang.gg/api/webhooks/{webhook.id}/{webhook.token}/messages/{message.id}To edit, send the fields to change: content, embeds or allowed_mentions. The name and
avatar the message was posted with stay as they were. Deleting removes the message for
everyone. Only that webhook’s own messages can be edited or deleted; any other ID is refused.
# Post and keep the message's ID ("id" in the response).
curl -X POST "$KLANG_WEBHOOK_URL?wait=true" \
-H "Content-Type: application/json" \
-d '{"content": "Deploying..."}'
# Edit it.
curl -X PATCH "$KLANG_WEBHOOK_URL/messages/1304817203949158400" \
-H "Content-Type: application/json" \
-d '{"content": "Deployed in 1 min 42 s."}'
# Delete it.
curl -X DELETE "$KLANG_WEBHOOK_URL/messages/1304817203949158400"const url = process.env.KLANG_WEBHOOK_URL;
const headers = { 'Content-Type': 'application/json' };
// Post and keep the message's ID.
const message = await fetch(`${url}?wait=true`, {
method: 'POST',
headers,
body: JSON.stringify({ content: 'Deploying...' }),
}).then((response) => response.json());
// Edit it.
await fetch(`${url}/messages/${message.id}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ content: 'Deployed in 1 min 42 s.' }),
});
// Delete it.
await fetch(`${url}/messages/${message.id}`, { method: 'DELETE' });import os
import requests
url = os.environ["KLANG_WEBHOOK_URL"]
# Post and keep the message's ID.
message = requests.post(url, params={"wait": "true"}, json={"content": "Deploying..."}).json()
# Edit it.
requests.patch(f"{url}/messages/{message['id']}", json={"content": "Deployed in 1 min 42 s."})
# Delete it.
requests.delete(f"{url}/messages/{message['id']}")Get the webhook
GET https://api.klang.gg/api/webhooks/{webhook.id}/{webhook.token}Answers with the webhook, without its token. It’s a cheap way to check that a URL still works:
a regenerated or deleted one answers 404.
Rate limits
| Limit | How much |
|---|---|
| Per webhook | 5 requests every 2 seconds. Posting, editing and deleting share it. |
| Per channel | 30 messages a minute, between all the channel’s webhooks. |
| Requests with a URL that fails | 30 a minute per IP address. A wrong ID or token counts here. |
They’re the same on every plan, and the first two are Discord’s. Over a limit, Klang answers
429 Too Many Requests with a Retry-After header and these fields in the body:
| Field | Type | What it is |
|---|---|---|
retry_after |
number | Seconds to wait before trying again. |
global |
boolean | Always false: these limits are never global. |
Wait that long and send it again. Discord webhook libraries already do this on their own. If you post in bursts, a small queue that sends one message every half second stays under the limits.
import time
import requests
def post(url, body):
response = requests.post(url, json=body)
if response.status_code == 429:
time.sleep(response.json()["retry_after"])
response = requests.post(url, json=body)
response.raise_for_status()
return responseErrors
Errors come with their HTTP status and a JSON body with a code and a message:
| Status | When |
|---|---|
400 Bad Request |
The body isn’t valid: neither content nor embeds, a text over its limit, JSON that doesn’t parse. message says what’s wrong. |
404 Not Found |
The webhook doesn’t exist or the token is wrong, and Klang doesn’t say which. It’s also the answer once the token is regenerated or the webhook deleted. |
429 Too Many Requests |
Over a rate limit. |
Decide on the HTTP status. message is written for people, and can change.