KlangDocs
Open Klang

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

HTTP
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:

JSON
{
  "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.
  • @everyone and @here notify 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.

JSON
{
  "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:

HTTP
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

HTTP
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.

Python
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 response

Errors

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.