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
parsecontainsusers, or, whenuserslists IDs, only those IDs; @everyoneand@hereare kept only whenparsecontainseveryone;- a non-empty
parsetogether with a non-emptyusersorroleslist is rejected with 400PARSE_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).
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:
incident, when present. It renders only whenincident.nameis non-empty.- Otherwise
maintenance, when present. It renders only whenmaintenance.nameis non-empty. - Otherwise
component_updateorcomponent.
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.