Skip to content

Webhooks

A webhook posts messages into one guild channel under a name and avatar of its own.

Management routes take a user session token or a bot token. Every other webhook route uses the webhook ID and secret token in its own path as the complete credential; an Authorization header is not needed there and grants nothing. Using webhooks walks through a typical integration.

The Messages resource defines the message and embed objects.

Authorisation

A management route requires MANAGE_WEBHOOKS at guild level and, for a webhook with a channel, VIEW_CHANNEL and MANAGE_WEBHOOKS in that channel. A caller who is not a guild member, or who lacks either permission, receives 403 FORBIDDEN. A channel or webhook that does not exist returns 404 NOT_FOUND.

Webhooks exist only in guild text and voice channels. Naming a private channel returns 400 BAD_REQUEST.

Rate limit keying

Every webhook route bucket is keyed on the path parameter in its description (the guild, channel, or webhook ID) and is shared by every caller of that ID. Rate limits describes the response headers.

Execute webhook, the webhook message routes, and the GitHub, Slack and Instatus routes are exempt from the global limit and are bounded only by their own bucket. Authenticated management routes also count against the global limit.

Webhook object

A webhook belongs to one guild and posts into one channel of that guild. TEBHOS generates its token once at creation and never rotates it, so the pair of ID and token is a bearer credential for the lifetime of the webhook.

Structure

Field Type Description
id snowflake The ID of the webhook
guild_id snowflake The ID of the guild containing the webhook
channel_id snowflake The ID of the channel the webhook sends messages to
name string The name the webhook posts under (1-80 characters)
avatar ?string The avatar hash the webhook posts under, or null. It resolves like a user avatar, using the webhook ID as the user ID
token string The secret token that authorises execution (64 alphanumeric characters)
user ?object The account that created the webhook: {id, username, discriminator, global_name, avatar, bot}, or null when unknown. Absent from the token webhook object

Example

{
  "id": "1501314428688998182",
  "guild_id": "1501314428688990000",
  "channel_id": "1501314428688991111",
  "name": "Build bot",
  "avatar": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "token": "Xk3Qw9ZpL2vB7nR4tY6uI8oP0aS1dF5gH2jK4lZ9xC7vB1nM3qW5eR8tY0uI2oP4",
  "user": {
    "id": "1501314428688990001",
    "username": "ada",
    "discriminator": 0,
    "global_name": "Ada",
    "avatar": null,
    "bot": false
  }
}

Token webhook object

Token routes return this object. It has every field of the webhook object except user.

The token is returned in full

Every response that contains a webhook object includes the complete execution token, which grants full execution and token-management authority over that webhook.

Webhook message body

The JSON body Execute webhook accepts.

Field Type Description
content? string Message content. Leading and trailing whitespace is trimmed, and the result may hold at most 4,000 characters
embeds? array[rich embed input object] Rich embeds, with the same embed limits as a normal message
flags? integer Message flags stored on the message
username? string Per-message name override (1-80 characters after trimming). The stored webhook name does not change
allowed_mentions? object {parse?, users?, roles?, replied_user?}: see Mentions

A message needs non-empty content or at least one embed; otherwise the request fails with 400 CANNOT_SEND_EMPTY_MESSAGE.

Not supported on TEBHOS

attachments, message_reference, sticker_ids, nonce, tts and avatar_url are accepted and ignored. A webhook cannot attach files, reply, forward, send stickers or change its avatar per message, and multipart/form-data bodies are not accepted.

Mentions

TEBHOS parses user mentions written as <@id> or <@!id>, and @everyone / @here, from the content. Without allowed_mentions, the default is {"parse": []}: the mentions still render, but nobody is notified. With it:

  • users are kept when parse contains users, or, when users lists IDs, only those IDs;
  • @everyone and @here are kept only when parse contains everyone;
  • a non-empty parse together with a non-empty users or roles list is rejected with 400 PARSE_AND_USERS_OR_ROLES_CANNOT_BE_USED_TOGETHER.

Role mentions are not resolved for webhook messages.

Guild webhooks

List guild webhooks

GET /guilds/{guild_id}/webhooks

Returns an array of webhook objects in a guild. Requires guild-level MANAGE_WEBHOOKS. A webhook is included only when the caller also holds VIEW_CHANNEL and MANAGE_WEBHOOKS in its channel, and a webhook with no channel is never included.

Rate limit: 40 requests per 10 seconds for each guild, on the webhook:list_guild bucket.

Channel webhooks

List channel webhooks

GET /channels/{channel_id}/webhooks

Returns an array of webhook objects in a guild channel. Requires MANAGE_WEBHOOKS at guild level and VIEW_CHANNEL plus MANAGE_WEBHOOKS in the channel.

Rate limit: 40 requests per 10 seconds for each channel, on the webhook:list_channel bucket.

Create webhook

POST /channels/{channel_id}/webhooks

Creates a webhook and returns 200 with the new webhook object, including its token. Same permissions as listing channel webhooks.

JSON body

Field Type Description
name string Webhook name (1-80 characters after trimming)
avatar? ?string A base64 image or data: URL (PNG, JPEG, GIF or WebP, at most 10 MiB decoded), or null for no avatar

A channel holds at most 15 webhooks and a guild at most 1,000; creating one more returns 400 BAD_REQUEST. An invalid name or image also returns 400 BAD_REQUEST.

Side effects

Records a WEBHOOK_CREATE guild audit log entry and emits Webhooks Update with channel_id to the guild.

Rate limit: 10 requests per minute for each channel, on the webhook:create bucket.

curl -X POST https://tebhos.com/api/v1/channels/1501314428688991111/webhooks \
  -H "Authorization: Bearer $TEBHOS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Build bot"}'

Webhook management

Get webhook

GET /webhooks/{webhook_id}

Returns a webhook object. Requires MANAGE_WEBHOOKS at guild level and in the webhook's channel.

Rate limit: 100 requests per 10 seconds for each webhook, on the webhook:read bucket.

Update webhook

PATCH /webhooks/{webhook_id}

Updates a webhook and returns the modified webhook object. Same permissions as getting it.

Field Type Description
name? string Replacement name (1-80 characters after trimming)
avatar? ?string Replacement image in the same format as Create webhook, or null to remove the avatar. Omit to keep it
channel_id? snowflake Destination channel. Moving requires VIEW_CHANNEL and MANAGE_WEBHOOKS in the destination, which must be a guild channel

Records a WEBHOOK_UPDATE guild audit log entry with the changed fields and emits Webhooks Update naming the webhook's previous channel.

Rate limit: 20 requests per 10 seconds for each webhook, on the webhook:update bucket.

Delete webhook

DELETE /webhooks/{webhook_id}

Permanently deletes a webhook and returns 204. Same permissions as getting it. Records a WEBHOOK_DELETE guild audit log entry and emits Webhooks Update. Messages the webhook already posted remain in the channel.

Rate limit: 20 requests per 10 seconds for each webhook, on the webhook:delete bucket.

Token routes

The routes below take the webhook ID and token in their paths as the complete credential. A token that does not match the webhook returns 401 UNAUTHORIZED; a webhook ID that does not exist returns 404 NOT_FOUND.

Get webhook with token

GET /webhooks/{webhook_id}/{token}

Returns a token webhook object.

Rate limit: 100 requests per 10 seconds for each webhook, on the webhook:read bucket.

Update webhook with token

PATCH /webhooks/{webhook_id}/{token}

Updates name and avatar with the same rules as Update webhook and returns the modified token webhook object. Supplying channel_id returns 403 FORBIDDEN: a token alone cannot move a webhook. Emits Webhooks Update; no audit log entry is recorded.

Rate limit: 20 requests per 10 seconds for each webhook, on the webhook:update bucket.

Delete webhook with token

DELETE /webhooks/{webhook_id}/{token}

Permanently deletes the webhook and returns 204. Emits Webhooks Update; no audit log entry is recorded.

Any token holder can delete the webhook

The route evaluates no guild permission. Keep the token secret.

Rate limit: 20 requests per 10 seconds for each webhook, on the webhook:delete bucket.

Execute webhook

POST /webhooks/{webhook_id}/{token}

Creates a webhook-authored message in the webhook's channel. Send a webhook message body with Content-Type: application/json.

Query parameters

Field Type Description
wait? boolean true to receive the created message in the response (default false)

Response

Status Body Condition
204 empty Message was created and wait is false or omitted
200 message object Message was created and wait is true
400 error response Body is not valid JSON, the message is empty, the content is longer than 4,000 characters, an embed breaks the embed limits, or username is invalid
401 error response Token does not match the webhook
404 error response Webhook does not exist or has no channel

Side effects

The message is stored with webhook_id, the effective name, and the webhook's stored avatar. Every session that can read the channel receives Message Create with the full message object, whose author is built from the webhook (id = webhook ID, bot: true).

Rate limit

60 requests per minute for each webhook, on the webhook:execute bucket (shared with Execute Slack webhook).

curl -X POST "https://tebhos.com/api/v1/webhooks/1501314428688998182/$WEBHOOK_TOKEN?wait=true" \
  -H "Content-Type: application/json" \
  -d '{"content": "Deploy finished", "embeds": [{"title": "v2.4.0", "color": 3066993}]}'
import os, requests

url = f"https://tebhos.com/api/v1/webhooks/1501314428688998182/{os.environ['WEBHOOK_TOKEN']}"
res = requests.post(url, json={"content": "Deploy finished"})
res.raise_for_status()  # 204 No Content

Webhook message response

Execute webhook with wait=true, Get webhook message and Edit webhook message all return the standard message object, the same shape the Gateway delivers. Its author is the webhook (id is the webhook ID, username the name the message was posted under, bot is true), webhook_id is set, and embeds holds the stored embeds with their resolved media fields.

Webhook message routes

These routes address only messages the webhook itself posted in its current channel. A message that does not exist in that channel returns 404 UNKNOWN_MESSAGE; a message posted by anyone else returns 403 MISSING_PERMISSIONS.

Get webhook message

GET /webhooks/{webhook_id}/{token}/messages/{message_id}

Returns the message object.

Rate limit: 60 requests per minute for each webhook, on the webhook:message_get bucket.

Edit webhook message

PATCH /webhooks/{webhook_id}/{token}/messages/{message_id}

Edits a webhook message and returns the updated message object.

Field Type Description
content? string Replacement content (at most 4,000 characters after trimming)
embeds? array[rich embed input object] Replaces the message's rich embeds ([] removes them); link previews are kept. Same embed limits
flags? integer Message flags; only the sendable bits change
allowed_mentions? object Mention policy for the new content, as in Mentions

At least one of non-empty content, a non-empty embeds list or flags is required, otherwise the request fails with 400 CANNOT_SEND_EMPTY_MESSAGE. An embed that breaks the limits is reported per field (for example path embeds.0.title, code STRING_LENGTH_INVALID). Changing the content marks the message edited and recalculates its mentions. Emits Message Update.

Rate limit: 30 requests per minute for each webhook, on the webhook:message_edit bucket.

Delete webhook message

DELETE /webhooks/{webhook_id}/{token}/messages/{message_id}

Deletes a webhook message and returns 204. Emits Message Delete.

Rate limit: 30 requests per minute for each webhook, on the webhook:message_delete bucket.

Execute Slack webhook

POST /webhooks/{webhook_id}/{token}/slack

Accepts a Slack-compatible payload, converts it to TEBHOS content and embeds, and creates one webhook-authored message. Returns 200 with the literal body ok (text/html).

Slack payload object

Field Type Description
text? string Message content (trimmed, at most 4,000 characters). Mentions in it never notify anyone (allowed_mentions is {"parse": []})
username? string Per-message name override (1-80 characters)
icon_url? string Accepted and ignored
attachments? array[Slack attachment object] Converted to embeds

Slack attachment object

Field Type Maps to
pretext?, text? string Embed description, joined by a newline
fallback? string Embed description, used only when pretext and text are both absent
color? string Embed colour, when it is exactly six hexadecimal digits with an optional leading #
title? string Embed title
title_link? string Embed URL (http/https only)
fields? array[{title, value, short?}] Embed fields; an entry without a title or value is dropped, short maps to inline
footer? string Embed footer text
ts? number or string Embed timestamp from Unix seconds
author_name? string Embed author name (an author is emitted only when this is present)
author_link?, author_icon? string Embed author URL and icon (http/https only)
image_url?, thumb_url? string Embed image and thumbnail (http/https only)

An attachment that produces no embed property is dropped. A payload that yields neither content nor an embed returns 400 BAD_REQUEST. Every mention in the content is kept, as for Execute webhook without allowed_mentions. The payload has no nonce, so a repeated call creates a second message.

Rate limit: 60 requests per minute for each webhook, on the webhook:execute bucket.

Execute GitHub webhook

POST /webhooks/{webhook_id}/{token}/github

Accepts a GitHub webhook delivery and creates one formatted message when the event renders. Always returns 204, whether or not a message was created. Point a GitHub repository or organisation webhook at this URL with the content type application/json.

Request headers

Field Type Description
X-GitHub-Event string The GitHub event type. An absent or unknown value renders nothing
X-GitHub-Delivery? string The delivery identifier used for deduplication

Deduplication is per webhook and delivery

After a delivery renders a message, the same webhook ignores that delivery identifier for 24 hours. A delivery that renders nothing leaves the identifier free for a retry. An empty identifier disables deduplication.

The body is read as untyped JSON; a delivery with missing or malformed fields simply renders nothing.

Rendered message

A rendered event creates one message with no content and exactly one rich embed, posted under the name GitHub with no avatar. The embed author is the GitHub user named in the table below (login, profile URL and avatar). Mentions are never parsed, so nothing in a commit message or comment notifies anyone.

Embed titles are trimmed and truncated to 70 characters (256 for ordinary pushes and check runs/suites), and descriptions to 350 characters, with ... marking a cut. HTML entities are not decoded.

GitHub event types

Value Renders when Embed author
commit_comment action is created and the comment has commit_id Comment author
create ref_type is branch or tag Sender
delete ref_type is branch or tag Sender
fork forkee is present Sender
issue_comment action is created, with comment and issue Comment author
issues action is opened, closed or reopened (description only for opened) Sender
member action is added, with member Sender
public Always Sender
pull_request action is opened, closed or reopened (description only for opened) Pull request author
pull_request_review action is submitted, with review and pull_request Review author
pull_request_review_comment action is created, with comment and pull_request Comment author
push ref and compare are present and there is at least one commit; a forced push needs head_commit and renders a compare link Sender
release action is published, with release Sender
watch action is started Sender
check_run action is completed and the parent suite's conclusion is not skipped None
check_suite action is completed and the conclusion is not skipped None
discussion action is created, with discussion Discussion author
discussion_comment action is created, with comment and discussion Comment author
repository action is created Sender

Every event also needs repository. A push lists one line per commit, and a commit message sentence This reverts commit <40-character sha>. is turned into a link to the reverted commit. Check runs and suites render green for the conclusion success and red otherwise.

Rate limit: 200 requests per minute for each webhook, on the webhook:github bucket.

Execute Instatus webhook

POST /webhooks/{webhook_id}/{token}/instatus

Accepts an Instatus status-page notification and creates one formatted message when the payload renders. Always returns 204, whether or not a message was created.

Rendering

TEBHOS selects exactly one section, in this order:

  1. incident, when present. It renders only when incident.name is non-empty.
  2. Otherwise maintenance, when present. It renders only when maintenance.name is non-empty.
  3. Otherwise component_update or component.

Note

When an incident object is present but has no name, nothing is rendered; TEBHOS does not fall through to the maintenance or component sections.

The message has no content and one rich embed, posted under the name Instatus with no avatar. Mentions are never parsed.

Embed part Incident / maintenance Component transition
Title The name, truncated to 256 characters {component name} - {status}, with A component when the name is missing and updated when no status is known
URL The item's url, else page.url (http/https only) page.url
Description The markdown of the update with the latest created_at, truncated to 4,096 characters None
Fields Status (inline), Window (maintenance start and end as timestamp markup), Affected components None
Footer page.status_description, else the humanised page.status_indicator; Backfilled is appended when the item is backfilled Same, without Backfilled
Timestamp The latest update's created_at, then updated_at, then created_at component_update.created_at, then component.created_at
Colour From the status (see below); maintenance defaults to the maintenance colour From the new status

Instatus status values

A status is normalised by uppercasing it and removing every character outside A-Z, so degraded_performance becomes DEGRADEDPERFORMANCE.

Value Rendered label Colour
UP All systems operational Neutral (#5c6bc0)
HASISSUES Has issues Neutral
OPERATIONAL Operational Operational (#3ecf8e)
UNDERMAINTENANCE Under maintenance Maintenance (#3b82f6)
DEGRADEDPERFORMANCE Degraded performance Degraded (#f5a623)
PARTIALOUTAGE Partial outage Partial outage (#f97316)
MAJOROUTAGE Major outage Major outage (#e23c39)
INVESTIGATING Investigating Major outage
IDENTIFIED Identified Partial outage
MONITORING Monitoring Degraded
RESOLVED Resolved Operational
NOTSTARTEDYET Scheduled Maintenance
PLANNED Planned Maintenance
INPROGRESS In progress Maintenance
COMPLETED Completed Operational

Any other value is rendered unchanged with the neutral colour.

Not supported on TEBHOS

Instatus notifications are not deduplicated; a repeated notification creates another message.

Rate limit: 200 requests per minute for each webhook, on the webhook:instatus bucket.