Skip to content

Webhooks

A webhook posts messages into one channel under a name and avatar of its own. Its URL holds an id and a token, and the token is all it takes to post, so a deploy script or a monitoring job can send messages without a bot.

import asyncio
import os

import bostie
from bostie import Embed, color, ui


async def announce(url: str, version: str) -> None:
    async with bostie.WebhookClient(url) as webhook:
        message = await webhook.execute(
            embeds=[Embed(title=f"Deploying {version}", color=color.YELLOW)],
            username="Deploy bot",
            avatar_url="https://example.com/deploy.png",
        )
        await webhook.edit_message(
            message.id,
            embeds=[Embed(title=f"Deployed {version}", color=color.GREEN)],
        )
        await webhook.execute(
            components=[ui.text(f"Release notes for {version} are out.")],
            thread_name=f"Release {version}",
            with_components=True,
        )


def announce_from_a_script(url: str) -> None:
    webhook = bostie.SyncWebhookClient(url)
    message = webhook.execute("The nightly backup is running.")
    webhook.edit_message(message.id, content="The nightly backup finished.")


if __name__ == "__main__":
    asyncio.run(announce(os.environ["WEBHOOK_URL"], "2.0"))

WebhookClient(url) parses the id and token from a webhook URL, and raises ValueError for anything else. Use it inside async with, which opens and closes its HTTP client. SyncWebhookClient(url) has the same methods for code without an event loop; each call runs its own.

Method Does
execute(content, **parts) posts a message and returns it once Discord saved it
edit_message(message_id, **parts) edits a message the webhook sent; a part left out stays, None clears it
fetch_message(message_id), delete_message(message_id) fetch or delete one
fetch(), edit(name=..., avatar=...), delete() the webhook itself
execute_slack(payload), execute_github(payload) post a payload in the formats Discord's Slack-compatible and GitHub-compatible endpoints accept

execute takes the message parts and also:

  • username and avatar_url, shown instead of the webhook's own for this message;
  • thread_id, to post in a thread of the channel; the message methods take it too;
  • thread_name, to start a thread in a forum or media channel, with applied_tags;
  • with_components=True, needed for components on a webhook that no app owns.

The clients take api_url, http_settings and allowed_mentions like the bot; by default a webhook can ping users and roles but not @everyone.

When Discord answers 404 Unknown Webhook, the webhook was deleted. bostie then answers every later request for it with the same 404 without sending it, so a dead webhook does not count against the invalid request limit.

Managing webhooks with a bot

bot.rest.webhooks creates and lists a channel's or guild's webhooks, which needs the Manage Webhooks permission, and has the token methods too:

  • create(channel_id, name=..., avatar=File(...)) returns a Webhook whose url you can hand to a WebhookClient.
  • list(channel_id), list_guild(guild_id), fetch(webhook_id), edit(webhook_id, ...) and delete(webhook_id) work with the bot's token.
  • execute(webhook_id, token, ...) and the other _with_token and message methods are what WebhookClient calls.

The WebhooksUpdate event says that a channel's webhooks changed. A message a webhook sent arrives as a WebhookMessage, whose author is a PartialUser and whose webhook_id is set.