Skip to content

Webhooks

Pavel Erokhin edited this page Oct 28, 2026 · 2 revisions

A webhook posts what happens on your server to a chat as it happens: who joined, who died and how, an advancement, a raid, a restart. WorldAPI writes to Discord, Slack (Mattermost and Rocket.Chat too), Telegram, Microsoft Teams, and Google Chat in English, Russian, Spanish, Portuguese, or German. Your own site or bot can get every event as signed JSON instead.

You make a webhook in one of four places:

  • In game, with buttons in chat: /worldapi webhook. The fastest way: the game asks for the URL in a window of its own, so it never shows in chat or the log.
  • In the admin panel: Access → Webhooks.
  • In config.yml, under webhooks.hooks, for setups you keep in files.
  • Through the API: POST /webhooks, see Access control.

Webhooks came in 4.3.0.

1. Get the address from your chat service

Each service gives you a URL, except Telegram, which gives you a bot token. Treat both like a password: anyone who has it can post to your channel.

Discord

  1. Open the channel's settings (the gear next to its name) and go to Integrations → Webhooks.
  2. Press New Webhook, give it a name and an avatar if you like.
  3. Press Copy Webhook URL. It looks like https://discord.com/api/webhooks/123…/AbC….

Slack, Mattermost, and Rocket.Chat

  1. Slack: open api.slack.com/apps, press Create New App → From scratch, and pick your workspace.
  2. Under Incoming Webhooks, turn them on, press Add New Webhook to Workspace, and pick the channel.
  3. Copy the URL, https://hooks.slack.com/services/….

Mattermost (Integrations → Incoming Webhooks → Add) and Rocket.Chat (Administration → Integrations → New → Incoming) take Slack's format: choose Slack in WorldAPI and paste their URL.

Telegram

  1. Write to @BotFather in Telegram, send /newbot, and pick a name and a username ending in bot.
  2. BotFather answers with a token like 123456789:AAH…. That's the bot token.
  3. Tell the bot where to post. For yourself, write /start to the bot. For a group, add the bot to it. For a channel, add the bot as an admin that may post.
  4. In WorldAPI, paste the token and press Find chats. The chats that wrote to the bot during the last day show up; pick one.

WorldAPI only posts through the bot. It reads the bot's recent updates once, when you press Find chats, to list the chats.

Microsoft Teams

  1. In the channel, open ⋯ → Workflows.
  2. Pick the template Post to a channel when a webhook request is received, name it, and choose the team and the channel.
  3. Copy the URL the last step shows.

Google Chat

  1. Open the space, click its name, and go to Apps & integrations → Webhooks.
  2. Press Add webhook, name it, and save.
  3. Copy the URL, https://chat.googleapis.com/v1/spaces/….

Webhooks in Google Chat need a Google Workspace account, not a personal Gmail.

Your own site or bot: JSON

Any https:// address you control gets each event as JSON, signed so you can tell it came from your server. Make this kind in the panel, through the API, or in config.yml; the in-game wizard offers only chat services. See “What a JSON webhook receives” below.

2. Connect it

In game

Every step is a button in chat, and the secret goes into a window. You need worldapi.admin, which operators have.

  1. Run /worldapi webhook. It lists your webhooks, each with [Test], [Pause], and [Delete]. Press [+ New webhook].
  2. Pick the service: Discord, Slack, Telegram, Microsoft Teams, or Google Chat.
  3. Pick the events: everything notable; joins, leaves, deaths, and advancements; server starts and stops, raids, and bosses; or chat.
  4. Pick the language.
  5. A window opens with one text field. Paste the URL (it starts with https://), or for Telegram the bot token, and press Connect. The window hands it straight to WorldAPI: it never passes through chat or a command, so neither other players, other plugins, nor the server log see it. Closed it by accident? [Open it again] in chat brings it back.
  6. Telegram only: write /start to the bot or add it to a group, then press [Find chats] and pick the chat.

WorldAPI names the webhook after the service (discord, discord-2, …), saves it, and sends a test message right away. If the test doesn't arrive, it says why. A started setup waits ten minutes after your last step; [Cancel] stops it sooner.

Change its events, language, or words later in the panel.

In the admin panel

Open Access → Webhooks and press New webhook.

The Webhooks page: Discord, Telegram, Slack, Microsoft Teams, Google Chat, and JSON webhooks, each with its events, language, and a switch

  1. Pick the Service and give it a Name: 1–32 letters, digits, dashes, or underscores.
  2. Paste the Webhook URL. For Telegram, paste the Bot token, press Find chats, and pick one.
  3. Under Events, press a preset, or open “Pick events one by one”.
  4. Pick the Language. Under “Your own words”, reword any message (see below).
  5. “What it may see” sets its permissions. The default is right for a shared channel; see “Security”.
  6. Press New webhook. A test message goes out at once.

A new Telegram webhook in the panel: the bot token, the chat found, the preset for joins, leaves, deaths, and advancements, and Russian

Each row has a switch that pauses the webhook and keeps its settings, a button for a test message, an editor, and a delete button. A test goes out at most once every 10 seconds per webhook. A JSON webhook's signing secret shows once, right after you make it; copy it then.

Webhooks from config.yml show a config.yml badge. You can test them here, but they change only in the file.

In config.yml

webhooks:
  enabled: true                  # false stops every webhook and lets nobody make one
  allowPrivateAddresses: false   # see “Security”
  hooks:
    - name: deaths
      format: discord            # json, discord, slack, telegram, teams, or google_chat
      url: https://discord.com/api/webhooks/123456789/AbCdEf
      topics: [player.join, player.leave, player.death]
      language: en               # en, ru, es, pt, or de
      messages:
        player.join: "🌲 {player} wandered into the server"
    - name: telegram
      format: telegram
      botToken: "123456789:AAH-your-token-from-BotFather"
      chatId: "-1001234567890"   # or a public channel's @name
      topics: ["*"]
      language: ru
    - name: site
      format: json
      url: https://example.com/hooks/minecraft
      topics: [player.join, player.leave, server.status]
      secret: "<64 random hex characters>"   # openssl rand -hex 32
      permissions: [viewer]

/worldapi reload applies the changes. Each webhook needs a name, a format, topics, and a url, or botToken and chatId for Telegram. Webhooks in the file may use http://, for a receiver on the same machine; made ones need https://. permissions defaults to server:read, players:read, and events:read; enabled: false pauses one. To find a Telegram chatId without the panel, make the webhook in the panel once and copy the number from its row, or ask the API: POST /webhooks/telegram/chats with {"botToken": "…"}.

Webhooks from config.yml and ones made elsewhere live side by side: the file holds its own, and the database holds the rest.

Events

topics takes the same ids as the WebSocket, in any mix:

You write It gets
* Every notable event: what the event history records, joins, leaves, kicks, deaths, advancements, chat, raids, boss kills, skipped nights, the server starting and stopping, and plugins' recorded events. Not the noisy ones such as positions, damage, or blocks
player.* Every topic that starts with player., but not the sampled ones and not player.command
player.death One topic
ext.mobosses.boss.spawned A plugin's topic, see Extensions

The presets in game and in the panel:

Preset Topics
Everything notable *
Joins, leaves, deaths, advancements player.join, player.leave, player.death, player.advancement
Server, raids, bosses server.status, raid.start, raid.finish, entity.boss_killed, world.night_skipped
Chat player.chat

Sampled topics, the values the WebSocket sends every few ticks (player.move, player.health, player.inventory, world.time, world.weather, server.performance), would flood a channel, so a webhook can't take them at all. Players' commands (player.command) come only when you name that topic yourself. A webhook takes at most 64 topics; * and player.* cover many.

A webhook gets a topic only when its permissions allow it, as a token's would. Chat needs chat:read, and events.chat: true in config.yml, which records chat at all. player.command needs logs:read. Topics with places, such as deaths, keep their coordinates only with players:private; positions alone never reach a webhook without it.

A chat service gets a message only for events worth one: a saved world or an unknown status says nothing. Topics without a message of their own come as “player.fish: Steve”.

Languages and your own words

A chat webhook writes in en, ru, es, pt, or de. JSON carries data only.

Any message can take your own words instead. In the panel, open “Your own words” in the webhook's editor; in config.yml, add messages; through the API, send messages. Each one takes up to 500 characters, and an empty field brings back the default. These are the keys, with their English defaults:

Key Default
player.join {player} joined the server
player.join.first {player} joined the server for the first time
player.leave {player} left the server
player.kick {player} was kicked: {reason}
player.death 💀 {message}
player.advancement 🏆 {player} made the advancement [{title}]
player.chat <{player}> {message}
player.level_up {player} reached level {to}
player.kill ⚔️ {player} killed {victimName}
player.gamemode {player} switched to {to} mode
player.world_change {player} went to {to}
entity.boss_killed 🐉 {boss} was defeated
world.night_skipped 🌅 The night was skipped
raid.start 🚩 A raid began
raid.finish 🚩 The raid ended: {result}
server.status.started ✅ The server started
server.status.stopping ⛔ The server is stopping
event {topic}, for any other event
event.player {topic}: {player}, for any other event about a player

Every message may use {player}, {world}, and {topic}, and any plain field of the event's data, as the WebSocket shows it: {message} for a death, {title} for an advancement. When an event lacks a field your words name, the webhook falls back to the language's default, so a message never shows a stray {placeholder}.

Security

  • Never more than its maker. A webhook gets only permissions the person who made it has, and changing or deleting one needs every permission it holds. Making webhooks needs webhooks:manage, which the admin and owner groups have.
  • No coordinates and no chat by default. A webhook usually posts to a shared channel, so it starts with server:read, players:read, and events:read. Add players:private for places and chat:read for chat only when everyone in that channel may see them.
  • HTTPS, and not into your network. A webhook made in game, in the panel, or through the API needs an https:// address, and can't post to the server's own machine, a private network, or a cloud's metadata address. WorldAPI checks when you make it and before each delivery, so a name that later points inside is caught too. webhooks.allowPrivateAddresses: true lifts both, for a bot on the same machine; webhooks in config.yml may always.
  • No pings, no hidden links. Players write some of the text, chat above all, so a message can't mention @everyone: Discord gets no mentions, Slack and Google Chat get their markup escaped, and Telegram gets plain text. A Markdown link a player writes, such as [free diamonds](https://…), arrives in Discord and Teams as plain text, not as a link.
  • Secrets stay secret. The API and the panel show only the host of an address (https://example.com/…), with the first path segment for Discord, Slack, and Google Chat (https://discord.com/api/…), and a Telegram webhook as its chat. The log names a webhook, never its URL. A webhook over plain http:// to another machine gets a warning in the log.
  • Limits. The panel, the game, and the API keep at most 50 webhooks, plus those in config.yml. Sending a test needs every permission the webhook holds, and works once every 10 seconds per webhook, so nobody can make the server spam an address.
  • A slow chat holds up only itself. Each webhook has its own queue of 256 events and drops the oldest when it falls behind. A failed delivery tries again after a second and after five more, except for errors a retry won't fix. A webhook that keeps failing gets one try per event, with one line in the log, until it answers again.

What a JSON webhook receives

A json webhook gets each event as a POST with the event as the WebSocket sends it:

{"type":"player.death","at":"2026-10-28T19:41:07.512Z","atMillis":1793216467512,"player":{"uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","name":"Steve"},"world":"world","data":{"message":"Steve was slain by Zombie"}}

The headers:

Header Holds
X-WorldAPI-Topic The topic, such as player.death; webhook.test for a test
X-WorldAPI-Timestamp When it was sent, in epoch milliseconds
X-WorldAPI-Delivery A UUID per event, the same on every retry, so you can drop duplicates
X-WorldAPI-Signature sha256= and the hex HMAC-SHA256 of the timestamp, a dot, the delivery, a dot, and the body, keyed with the webhook's secret
User-Agent WorldAPI/<version>

Check the signature on the raw body, before you parse it, and refuse old timestamps so nobody replays a captured request. In Python:

import hashlib, hmac, time

def from_worldapi(secret: str, headers, body: bytes) -> bool:
    timestamp = headers["X-WorldAPI-Timestamp"]
    signed = f"{timestamp}.{headers['X-WorldAPI-Delivery']}.".encode() + body
    expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    fresh = abs(time.time() * 1000 - int(timestamp)) < 5 * 60 * 1000
    return fresh and hmac.compare_digest(expected, headers.get("X-WorldAPI-Signature", ""))

Answer with any 2xx status. A 4xx other than 429 counts as final, and WorldAPI doesn't retry it.

Updating from 4.2 or earlier

If your config.yml lists its own auth.groups, add "webhooks:manage" where you want it, to admin and owner as the default does. Until then, only a token or an account that lists webhooks:manage by itself can make webhooks.

Next: Configuration.

Clone this wiki locally