Files
upstream-zulip/docs/webhooks/incoming-webhooks-reference.md
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

294 lines
12 KiB
Markdown

# Incoming webhooks reference
This guide provides a detailed reference for developing and configuring
Zulip incoming webhook integrations. For step-by-step guidance, read the
[incoming webhooks walkthrough](incoming-webhooks-walkthrough.md), before using
this reference guide.
## Custom HTTP headers
Some third-party outgoing webhook APIs, such as GitHub's, don't encode
all of the information about an event in the HTTP request body. Instead,
they put key details like the **event type** that generates the particular
payload in a separate HTTP header.
Generally, this is clear in the third-party's API documentation that
you will be referencing when creating fixtures.
### Extracting event-type HTTP headers from payloads
To get the HTTP header value from the payload, in your `view.py`, you can
use the `get_event_header` function in `zerver/lib/webhooks/common.py`,
like so:
```python
event = get_event_header(request, header, integration_name)
```
`request` is the `HttpRequest` object passed to your main webhook
function. `header` is the name of the custom header you'd like to extract,
such as `X-Event-Key`. And `integration_name` is the name of the
third-party service in question, such as `GitHub`.
Because such headers are how some integrations indicate the event types
of their outgoing webhook payloads, the absence of such a header usually
indicates a configuration issue, where one either entered the URL for a
different integration, or happens to be running an older version of the
integration that doesn't set that header.
If the requisite header is missing, this function sends a direct message
to the owner of the webhook bot, notifying them of the missing header.
### Recording event-type HTTP headers in fixtures
In order to test Zulip's handling of this data, you will need to record
which HTTP headers are used with each fixture you capture. Since this is
integration-dependent, Zulip supports a simple format.
Encode the value of the HTTP header in the first part of the fixture's
filename, for example:
```
pull_request__opened.json
```
`pull_request` is the value of the header `X-Github-Event`, and `opened` is
the subtype of the event type. They are separated by a double underscore, to
allow using single underscores within each segment.
### Extracting event-type HTTP headers from fixtures
To get the HTTP header value from the fixture's filename in your
tests, you can define a `fixture_to_headers` function in your
`view.py`, using the `default_fixture_to_headers` function in
`zerver/lib/webhooks/common.py`, like so:
```python
fixture_to_headers = default_fixture_to_headers("HTTP_X_GITHUB_EVENT")
```
`HTTP_X_GITHUB_EVENT` is the name of the custom header you'd like to extract.
The default implementation `default_fixture_to_headers` uses the first part
of the fixture's filename as the header value, separated by a double
underscore (`__`). If you need to use a different method for encoding the
header value(s), you can define your own `fixture_to_headers` function with
the custom parsing logic in your `view.py`, instead of using
`default_fixture_to_headers`.
## Custom URL query parameters
### Registering webhooks requiring custom configuration
In cases where an incoming webhook integration supports optional URL
parameters, one can use the `url_options` feature. It's a field in the
`IncomingWebhookIntegration` class that is used when [generating an
integration URL](https://zulip.com/help/generate-integration-url) (for a
bot) in the web and desktop apps, which encodes the user input for each
parameter in the integration URL.
These URL options can be declared as follows:
```python
IncomingWebhookIntegration(
'helloworld',
...
url_options=[
WebhookUrlOption(
name='ignore_private_repositories',
label='Exclude notifications from private repositories',
input_type='checkbox',
),
],
)
```
`url_options` is a list describing the parameters the web app UI should
offer when generating the integration URL:
- `name`: The parameter name that is used to encode the user input in the
integration's webhook URL.
- `label`: A short descriptive label for this URL parameter in the web
app UI.
- `input_type`: The type of input field this option maps to in the UI.
The web app UI currently supports the following input types:
- `checkbox`: A checkbox input for presence-only values (true or absent),
disabled by default.
- `checkbox_enabled`: A checkbox input for boolean parameters, enabled by
default.
- `text`: A text input for string values.
To add support for other input types, you can update
`web/src/integration_url_modal.ts`.
In rare cases, it may be necessary for an incoming webhook to require
additional user configuration beyond what is specified in the POST URL.
A typical use case for this would be APIs that require clients to do a
callback to get details beyond an opaque object ID that one would want to
include in a Zulip notification message. The `config_options` field in
the `IncomingWebhookIntegration` class is reserved for this use case.
### WebhookUrlOption presets
The `build_preset_config` method creates `WebhookUrlOption` objects with
pre-configured fields. These preset URL options primarily serve two
purposes:
- To construct common `WebhookUrlOption` objects that are used in various
incoming webhook integrations.
- To construct `WebhookUrlOption` objects with special UI in the web app
for [generating incoming webhook URLs](https://zulip.com/help/generate-integration-url).
For other purposes, you can use the `WebhookUrlOption` class directly.
Using a preset URL option with the `build_preset_config` method:
```python
# zerver/lib/integrations.py
from zerver.lib.webhooks.common import PresetUrlOption, WebhookUrlOption
# -- snip --
IncomingWebhookIntegration(
"github",
# -- snip --
url_options=[
WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES),
],
),
```
The currently configured preset URL options are:
- **`BRANCHES`**: This preset is intended to be used for [version control
integrations](https://zulip.com/integrations/category/version-control),
and adds UI for the user to configure which branches of a project's
repository will trigger Zulip notification messages. When the user
specifies which branches to receive notifications from, the `branches`
parameter will be added to the [generated integration
URL](https://zulip.com/help/generate-integration-url). For example, if
the user input `main` and `dev` for the branches of their repository,
then `&branches=main%2Cdev` would be appended to the generated URL.
- **`IGNORE_PRIVATE_REPOSITORIES`**: This preset is intended to be used for
[version control integrations](https://zulip.com/integrations/category/version-control),
and adds UI for the user to exclude private repositories from triggering
Zulip notification messages. When the user selects this option, the
`ignore_private_repositories` boolean parameter will be added to the
[generated integration URL](https://zulip.com/help/generate-integration-url).
- **`CHANNEL_MAPPING`**: This preset is intended to be used for [chat-app
integrations](https://zulip.com/integrations/category/communication)
(like Slack), and adds a special option, **Matching Zulip channel**, to
the web app UI for where to send Zulip notification messages. This
special option maps the notification messages to Zulip channels that
match the messages' original channel name in the third-party service.
When selected, this requires setting a single topic for notification
messages, and adds `&mapping=channels` to the [generated integration
URL](https://zulip.com/help/generate-integration-url).
### Writing tests for custom URL query parameters
Custom arguments passed in URL query parameters work as expected in the
webhook code, but require special handling in tests.
For example, here is the definition of a webhook function that gets both
`stream` and `topic` from the query parameters:
```python
@webhook_view("Querytest")
@typed_endpoint
def api_querytest_webhook(
request: HttpRequest,
user_profile: UserProfile,
*,
payload: Annotated[str, ApiParamConfig(argument_type_is_body=True)],
stream: str = "test",
topic: str = "Default Alert",
) -> HttpResponse:
```
In actual use, you might configure the third-party service to call your
Zulip incoming webhook integration with a URL like this:
```
http://myhost/api/v1/external/querytest?api_key=abcdefgh&stream=alerts&topic=queries
```
It provides values for `stream` and `topic`, and the integration can get
those using `@typed_endpoint` without any special handling. How does this
work in a test?
The new attribute `TOPIC` exists only in our class so far. In order to
construct a URL with a query parameter for `topic`, you can pass the
attribute `TOPIC` as a keyword argument to `build_webhook_url`, like so:
```python
class QuerytestHookTests(WebhookTestCase):
TOPIC = "Default topic"
def test_querytest_test_one(self) -> None:
# construct the URL used for this test
self.TOPIC = "Query test"
self.url = self.build_webhook_url(topic=self.TOPIC)
# define the expected message contents
expected_topic = "Query test"
expected_message = "This is a test of custom query parameters."
self.check_webhook('test_one', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
```
You can also override `get_body` or `get_payload` if your test data
needs to be constructed in an unusual way.
For more, see the definition for the base class, `WebhookTestCase`
in `zerver/lib/test_classes.py`, or just grep for examples.
## Negative tests
A negative test is one that should result in an error, such as incorrect
data from the third-party's payload or headers. To correctly test these
cases, you must explicitly code your test's execution (using other test
helpers, as needed) rather than calling the usual `check_webhook` test
helper function.
Here is an example from the WordPress integration:
```python
def test_unknown_action_no_data(self) -> None:
# Mimic check_webhook() to manually execute a negative test.
# Otherwise its call to send_webhook_payload() would assert on the non-success
# we are testing. The value of result is the error message the webhook should
# return if no params are sent. The fixture for this test is an empty file.
# subscribe to the target channel
self.subscribe(self.test_user, self.channel_name)
# post to the webhook url
post_params = {'stream_name': self.channel_name,
'content_type': 'application/x-www-form-urlencoded'}
result = self.client_post(self.url, 'unknown_action', **post_params)
# check that we got the expected error message
self.assert_json_error(result, "Unknown WordPress webhook action: WordPress action")
```
In a normal test, `check_webhook` would handle all the setup and then
check that the incoming webhook's response matches the expected success
result. If the webhook returns an error, the test fails. Instead, you can
explicitly do the test setup it would have done, and check the error
result yourself.
Here, `subscribe` is a test helper that uses `test_user` and `channel_name`
(attributes from the base class) to register the user to receive messages
in the given channel. If the channel doesn't exist, it creates it.
`client_post`, another helper function, performs the HTTP POST that calls
the incoming webhook. As long as `self.url` is correct, you don't need to
construct the webhook URL yourself. (In most cases, it is.)
`assert_json_error` then checks if the result matches the expected error.
If you had used `check_webhook`, it would have called
`send_webhook_payload`, which checks the result with `assert_json_success`.