diff --git a/docs/setup/webhooks.md b/docs/setup/webhooks.md
index afcbcfb5f..e2f2d04db 100644
--- a/docs/setup/webhooks.md
+++ b/docs/setup/webhooks.md
@@ -1,16 +1,15 @@
# Webhooks
-Hookshot supports two kinds of webhooks, inbound (previously known as Generic Webhooks) and outbound.
-
+Matrix Hookshot supports two types of webhooks: **Inbound** (previously known as Generic Webhooks) and **Outbound**.
## Configuration
-You will need to add the following configuration to the config file.
+To enable webhooks in Hookshot, update the configuration file as follows:
```yaml
generic:
enabled: true
- outbound: true # For outbound webhook support
+ outbound: true # Enable outbound webhook support
urlPrefix: https://example.com/mywebhookspath/
allowJsTransformationFunctions: false
waitForComplete: false
@@ -22,228 +21,132 @@ generic:
## Inbound Webhooks
-Hookshot supports generic webhook support so that services can send messages into Matrix rooms without being aware of the Matrix protocol. This works
-by having services hit a unique URL that then transforms a HTTP payload into a Matrix message.
-
-
-Previous versions of the bridge listened for requests on `/` rather than `/webhook`. While this behaviour will continue to work,
-administators are advised to use `/webhook`.
-
-
-The webhooks listener listens on the path `/webhook`.
+Inbound webhooks allow external services to send messages to Matrix rooms without understanding the Matrix protocol. Services send HTTP requests to a unique webhook URL, and Hookshot transforms the payload into a Matrix message.
-The bridge listens for incoming webhooks requests on the host and port provided in the [`listeners` config](../setup.md#listeners-configuration).
+> **Note:** Older versions of the bridge listened on `/` instead of `/webhook`. While `/` still works, administrators should update configurations to use `/webhook`.
-`urlPrefix` describes the public facing URL of your webhook handler. For instance, if your load balancer redirected
-webhook requests from `https://example.com/mywebhookspath` to the bridge (on `/webhook`), an example webhook URL would look like:
-`https://example.com/mywebhookspath/abcdef`.
+### Webhook Listener Configuration
-`waitForComplete` causes the bridge to wait until the webhook is processed before sending a response. Some services prefer you always
-respond with a 200 as soon as the webhook has entered processing (`false`) while others prefer to know if the resulting Matrix message
-has been sent (`true`). By default this is `false`.
+- The bridge listens for incoming webhook requests on the host and port specified in the [`listeners` configuration](../setup.md#listeners-configuration).
+- `urlPrefix` specifies the externally accessible URL of your webhook handler. For example, if your load balancer forwards webhook requests from `https://example.com/mywebhookspath` to the bridge (`/webhook`), an example webhook URL would be:
+
+ ```
+ https://example.com/mywebhookspath/abcdef
+ ```
-`enableHttpGet` means that webhooks can be triggered by `GET` requests, in addition to `POST` and `PUT`. This was previously on by default,
-but is now disabled due to concerns mentioned below.
+- `waitForComplete` determines when Hookshot responds to webhook requests:
+ - `false` (default): Responds immediately with `200 OK` after receiving the request.
+ - `true`: Waits until the Matrix message is sent before responding.
+
+- `enableHttpGet` allows webhooks to be triggered using `GET` requests. This was previously enabled by default but is now disabled due to security concerns.
+- `maxExpiryTime` (default: unlimited) sets how long a webhook remains valid before expiration (e.g., `30d` for 30 days).
+ - `sendExpiryNotice`: Sends a warning message before expiry.
+ - `requireExpiryTime`: Prevents webhooks from being created without an expiry time.
-`maxExpiryTime` sets an upper limit on how long a webhook can be valid for before the bridge expires it. By default this is unlimited. This
-takes a duration represented by a string. E.g. "30d" is 30 days. See [this page](https://github.com/jkroso/parse-duration?tab=readme-ov-file#available-unit-types-are)
-for available units. Additionally:
+#### Webhook Authentication and User Configuration
- - `sendExpiryNotice` configures whether a message is sent into a room when the connection is close to expiring.
- - `requireExpiryTime` forbids creating a webhook without a expiry time. This does not apply to existing webhooks.
+You can assign a unique user ID prefix for webhook connections in Matrix rooms using `userIdPrefix`. For example, if `webhook_` is the prefix and the connection name is `example`, the generated user ID would be `@webhook_example:example.com`.
-You may set a `userIdPrefix` to create a specific user for each new webhook connection in a room. For example, a connection with a name
-like `example` for a prefix of `webhook_` will create a user called `@webhook_example:example.com`. If you enable this option,
-you need to configure the user to be part of your registration file e.g.:
+To register these users, update `registration.yaml`:
```yaml
# registration.yaml
-...
namespaces:
users:
- - regex: "@webhook_.+:example.com" # Where example.com is your domain name.
+ - regex: "@webhook_.+:example.com"
exclusive: true
```
-### Adding a webhook
-
-To add a webhook to your room:
- - Invite the bot user to the room.
- - Make sure the bot able to send state events (usually the Moderator power level in clients)
- - Say `!hookshot webhook example` where `example` is a name for your hook.
- - The bot will respond with the webhook URL to be sent to services.
-
-### Webhook Handling
-
-Hookshot handles `POST` and `PUT` HTTP requests by default.
-
-Hookshot handles HTTP requests with a method of `GET`, `POST` or `PUT`.
-
-If the request is a `GET` request, the query parameters are assumed to be the body. Otherwise, the body of the request should be a supported payload.
-
-If the body contains a `text` key, then that key will be used as a message body in Matrix (aka `body`). This text will be automatically converted from Markdown to HTML (unless
-a `html` key is provided.).
-
-If the body contains a `html` key, then that key will be used as the HTML message body in Matrix (aka `formatted_body`). A `text` key fallback MUST still be provided.
-
-If the body *also* contains a `username` key, then the message will be prepended by the given username. This will be prepended to both `text` and `html`.
+### Adding a Webhook to a Room
-If the body does NOT contain a `text` field, the full payload will be sent to the room. This can be adapted into a message by creating a **JavaScript transformation function**.
+1. Invite the bot user to the Matrix room.
+2. Ensure the bot has permission to send state events (usually requires Moderator access).
+3. Send the following command in the room:
+
+ ```
+ !hookshot webhook example
+ ```
+
+ Replace `example` with a meaningful name for your webhook.
+4. The bot will generate a webhook URL to use with external services.
+### Webhook Payload Handling
-#### Payload formats
+Hookshot supports `POST`, `PUT`, and optionally `GET` (if enabled) for incoming webhooks. The body of the request must be in one of the following formats:
-If the request is a `POST`/`PUT`, the body of the request will be decoded and stored inside the event. Currently, Hookshot supports:
+| Content-Type | Format |
+|-------------|--------|
+| `application/json` | JSON |
+| `application/x-www-form-urlencoded` | Web form data |
+| `text/*` | Plain text |
+| `/xml` or `+xml` | XML |
-- XML, when the `Content-Type` header ends in `/xml` or `+xml`.
-- Web form data, when the `Content-Type` header is `application/x-www-form-urlencoded`.
-- JSON, when the `Content-Type` header is `application/json`.
-- Text, when the `Content-Type` header begins with `text/`.
+If a request contains:
-Decoding is done in the order given above. E.g. `text/xml` would be parsed as XML. Any formats not described above are not
-decoded.
+- A `text` key: This becomes the message body in Matrix.
+- A `html` key: This becomes the formatted message body in Matrix.
+- A `username` key: The message is prefixed with the username.
+- No `text` field: The entire payload is sent to the room.
-#### GET requests
-
-In previous versions of hookshot, it would also handle the `GET` HTTP method. This was disabled due to concerns that it was too easy for the webhook to be
-inadvertently triggered by URL preview features in clients and servers. If you still need this functionality, you can enable it in the config.
-
-Hookshot will insert the full content of the body into a key under the Matrix event called `uk.half-shot.hookshot.webhook_data`, which may be useful if you have
-other integrations that would like to make use of the raw request body.
-
-
-Matrix does NOT support floating point values in JSON, so the uk.half-shot.hookshot.webhook_data field will automatically convert any float values
-to a string representation of that value. This change is not applied to the JavaScript transformation data
-variable, so it will contain proper float values.
-
-
-#### Wait for complete
-
-It is possible to choose whether a webhook response should be instant, or after hookshot has handled the message. The reason
-for this is that some services expect a quick response time (like Slack) whereas others will wait for the request to complete. You
-can specify this either globally in your config, or on the widget with `waitForComplete`.
-
-If you make use of the `webhookResponse` feature, you will need to enable `waitForComplete` as otherwise hookshot will
-immeditately respond with it's default response values.
-
-
-#### Expiring webhooks
-
-Webhooks can be configured to expire, such that beyond a certain date they will fail any incoming requests. Currently this expiry time
-is mutable, so anybody able to configure connections will be able to change the expiry date. Hookshot will send a notice to the room
-at large when the webhook has less than 3 days until it's due to expire (if `sendExpiryNotice` is set).
+For more advanced processing, you can use JavaScript transformation functions.
### JavaScript Transformations
-
-Although every effort has been made to securely sandbox scripts, running untrusted code from users is always risky. Ensure safe permissions
-in your room to prevent users from tampering with the script.
-
-
-This bridge supports creating small JavaScript snippets to translate an incoming webhook payload into a message for the room, giving
-you a very powerful ability to generate messages based on whatever input is coming in.
+> **Warning:** Even though scripts run in a sandboxed environment, avoid running untrusted code.
-The input is parsed and executed within a separate JavaScript Virtual Machine context, and is limited to an execution time of 2 seconds.
-With that said, the feature is disabled by default and `allowJsTransformationFunctions` must be enabled in the config.
+Transformation scripts allow you to modify webhook payloads before they are sent to a Matrix room. To enable this feature, set:
-The code snippets can be edited by editing the Matrix state event corresponding to this connection (with a state type of `uk.half-shot.matrix-hookshot.generic.hook`).
-Because this is a fairly advanced feature, this documentation won't go into how to edit state events from your client.
-Please seek out documentation from your client on how to achieve this.
-
-The script string should be set within the state event under the `transformationFunction` key.
-
-#### Script API
-
-Transformation scripts have a versioned API. You can check the version of the API that the hookshot instance supports
-at runtime by checking the `HookshotApiVersion` variable. If the variable is undefined, it should be considered `v1`.
-
-The execution environment will contain a `data` variable, which will be the body of the incoming request (see [Payload formats](#payload-formats)).
-Scripts are executed synchronously and expect the `result` variable to be set.
-
-If the script contains errors or is otherwise unable to work, the bridge will send an error to the room. You can check the logs of the bridge
-for a more precise error.
+```yaml
+allowJsTransformationFunctions: true
+```
-#### V2 API
+#### Example Script (V2 API)
-The `v2` api expects an object to be returned from the `result` variable.
+If `data` is:
-```json5
-{
- "version": "v2" // The version of the schema being returned from the function. This is always "v2".
- "empty": true|false, // Should the webhook be ignored and no output returned. The default is false (plain must be provided).
- "plain": "Some text", // The plaintext value to be used for the Matrix message.
- "html": "Some text", // The HTML value to be used for the Matrix message. If not provided, plain will be interpreted as markdown.
- "msgtype": "some.type", // The message type, such as m.notice or m.text, to be used for the Matrix message. If not provided, m.notice will be used.
- "webhookResponse": { // Optional response to send to the webhook requestor. All fields are optional. Defaults listed.
- "body": "{ \"ok\": true }",
- "contentType": "application/json",
- "statusCode": 200
- }
-}
+```json
+{"counter": 5, "maxValue": 4}
```
-#### Example script
-
-Where `data` = `{"counter": 5, "maxValue": 4}`
+Then, the script:
```js
-if (data.counter === undefined) {
- // The API didn't give us a counter, send no message.
- result = {empty: true, version: "v2"};
-} else if (data.counter > data.maxValue) {
- result = {plain: `**Oh no!** The counter has gone over by ${data.counter - data.maxValue}`, version: "v2"};
+if (data.counter > data.maxValue) {
+ result = {plain: `**Alert!** Counter exceeded by ${data.counter - data.maxValue}`, version: "v2"};
} else {
- result = {plain: `*Everything is fine*, the counter is under by ${data.maxValue - data.counter}`, version: "v2"};
+ result = {plain: `Counter is within safe limits.`, version: "v2"};
}
```
+### Webhook Testing Tools
-#### V1 API
+To test your webhooks, consider using:
-The v1 API expects `result` to be a string. The string will be automatically interpreted as Markdown and transformed into HTML. All webhook messages
-will be prefixed with `Received webhook:`. If `result` is falsey (undefined, false or null) then the message will be `No content`.
+- **[Beeceptor](https://beeceptor.com/)** – Set up a mock endpoint for capturing webhook requests.
+- **[PostBin](https://www.postb.in/)** – Inspect and debug webhook requests.
-#### Example script
+## Outbound Webhooks
-Where `data` = `{"counter": 5, "maxValue": 4}`
-
-```js
-if (data.counter > data.maxValue) {
- result = `**Oh no!** The counter has gone over by ${data.counter - data.maxValue}`
-} else {
- result = `*Everything is fine*, the counter is under by ${data.maxValue - data.counter}`
-}
-```
-
-## Outbound webhooks
-
-You can also configure Hookshot to send outgoing requests to other services when a message appears
-on Matrix. To do so, you need to configure hookshot to enable outgoing messages with:
+Hookshot can also send outbound webhooks when messages appear in Matrix rooms. Enable this with:
```yaml
generic:
outbound: true
```
-### Request format
-
-Requests can be sent to any service that accepts HTTP requests. You may configure Hookshot to either use the HTTP `PUT` (default)
-or `POST` methods.
-
-Each request will contain 3 headers which you may use to authenticate and direct traffic:
+### Request Format
- - 'X-Matrix-Hookshot-EventId' contains the event's ID.
- - 'X-Matrix-Hookshot-RoomId' contains the room ID where the message was sent.
- - 'X-Matrix-Hookshot-Token' is the unique authentication token provided when you created the webhook. Use this
- to verify that the message came from Hookshot.
+Hookshot sends `PUT` requests to an external service. The request contains:
-The payloads are formatted as `multipart/form-data`.
+| Header | Description |
+|--------|-------------|
+| `X-Matrix-Hookshot-EventId` | The Matrix event ID. |
+| `X-Matrix-Hookshot-RoomId` | The room where the message was sent. |
+| `X-Matrix-Hookshot-Token` | Authentication token for verification. |
-The first file contains the event JSON data, proviced as the `event` file. This is a raw representation of the Matrix event data. If the
-event was encrypted, this will be the **decrypted** body.
+The payload uses `multipart/form-data` and includes:
-If any media is linked to in the event, then a second file will be present named `media` which will contain the media referenced in
-the event.
+- `event`: The raw Matrix event data (decrypted if the message was encrypted).
+- `media`: If the event contains media, this file holds the referenced media.
-All events that occur in the room will be sent to the outbound URL, so be careful to ensure your remote service can filter the
-traffic appropriately (e.g. check the `type` in the event JSON)
+> **Note:** Ensure your external service can handle and filter events based on the `type` field in the event JSON.