> For the complete documentation index, see [llms.txt](https://docs.zenml.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zenml.io/pro/core-concepts/webhooks.md).

# Webhooks

Receive authenticated events from external systems in ZenML.

Webhooks connect external systems to ZenML so that your ZenML deployment can react to events that happen outside the platform. For example, you can receive an event when a pull request is merged in GitHub, when a ClickUp task changes status, when someone requests a deployment in Slack, or when an internal service publishes a new model version.

When you create a webhook, ZenML exposes a project-scoped intake URL. Configure the external system to send signed HTTP requests to that URL.

For every delivery, the webhook endpoint:

1. verifies the request signature using the webhook's signing secret;
2. validates the headers and JSON payload required by that webhook type;
3. records intake statistics for accepted and rejected deliveries; and
4. sends the provider-selected successful response; and
5. makes any accepted event available to configured consumers in a background task after the response is sent.

Consumers define how ZenML reacts to accepted events. For example, a [webhook trigger](/pro/core-concepts/triggers.md#webhook-triggers) can filter GitHub, ClickUp or Slack events and execute attached pipeline snapshots when a pull request is merged or a task status changes, a deployment message is posted, or an approval reaction is added.

When at least one webhook trigger matches, ZenML retains the original JSON request body so directly triggered pipeline steps can retrieve fields omitted from the normalized semantic event. Request headers are not retained. See [Access the raw webhook body](/pro/core-concepts/triggers.md#access-the-raw-webhook-body).

The endpoint returns an HTTP response selected by the provider. GitHub and custom deliveries normally return `202 Accepted`; Slack returns `200 OK`. An invalid signature, invalid payload, missing webhook, or inactive webhook returns an appropriate `4xx` response. The response reports the result of webhook intake. Any work performed by consumers happens in an in-process background task after the response is sent.

## Available providers

| Provider                                                 | Type      | Authentication                        | Event model                                                |
| -------------------------------------------------------- | --------- | ------------------------------------- | ---------------------------------------------------------- |
| [Slack](/pro/core-concepts/webhooks/slack.md)            | `slack`   | Slack request signature and timestamp | Curated semantic Slack automation events and typed filters |
| [GitHub](/pro/core-concepts/webhooks/github.md)          | `github`  | GitHub HMAC-SHA256 signature          | Curated semantic GitHub events and string filters          |
| [ClickUp](/pro/core-concepts/webhooks/clickup.md)        | `clickup` | ClickUp HMAC-SHA256 signature         | Curated ClickUp task and list events and string filters    |
| [Custom webhooks](/pro/core-concepts/webhooks/custom.md) | `custom`  | ZenML HMAC-SHA256 signature           | User-supplied event name and JSON object                   |

Provider pages describe the headers, event model, external setup, and manual testing procedure for each provider.

## Manage webhooks

Webhooks are project-scoped resources. Their basic lifecycle is create, inspect, update, and delete.

### Create a webhook

Choose a name and provider type to create a webhook in the active project:

```bash
zenml webhook create my-github-webhook --type github
```

The equivalent SDK flow is:

```python
from zenml.client import Client

client = Client()
result = client.create_webhook(
    name="my-github-webhook",
    webhook_type="github",
)

webhook = result
print(webhook.endpoint_url)
```

Provider types are string identifiers. ZenML includes `github`, `clickup`, `slack`, and `custom`; servers may register additional provider implementations, and reject provider types that are not registered.

The provider type determines how deliveries are authenticated and interpreted and cannot be changed after creation. By default, ZenML also generates a signing secret. Capture it as described in [Manage signing credentials](#manage-signing-credentials).

### Describe and list webhooks

Describe one webhook by name or ID, or list the webhooks in the active project:

```bash
zenml webhook describe my-github-webhook
zenml webhook list
```

`describe` includes the complete endpoint URL. Use that value when configuring the external provider instead of constructing the URL manually.

With the SDK, `get_webhook` accepts a name, ID, or ID prefix. `list_webhooks` supports filters such as provider type and active state:

```python
from zenml.client import Client

client = Client()

webhook = client.get_webhook("my-github-webhook")
github_webhooks = client.list_webhooks(
    webhook_type="github",
    active=True,
)
```

Normal describe, get, and list responses never include the signing secret.

### Update a webhook

You can rename a webhook or change its active state. These are the only mutable webhook properties:

```bash
zenml webhook update my-github-webhook --name production-github
zenml webhook update production-github --inactive
zenml webhook update production-github --active
```

Via the SDK:

```python
webhook = client.update_webhook(
    name_id_or_prefix="my-github-webhook",
    name="production-github",
    active=False,
)
```

The webhook ID, project association, and provider type cannot be updated. The endpoint path is derived from the provider type and webhook ID. An inactive webhook rejects otherwise valid deliveries with `409 Conflict`. Changing the webhook's active state does not change the active state or configuration of its consumers.

### Delete a webhook

Delete a webhook by name or ID:

```bash
zenml webhook delete production-github
```

Via the SDK:

```python
client.delete_webhook("production-github")
```

ZenML rejects deletion while a non-archived webhook trigger references the webhook. Archive or permanently delete those triggers before deleting the webhook. Archived trigger history and its serialized configuration are retained.

## Manage signing credentials

Every webhook has one active signing secret. The sender uses it to sign the exact request body, and ZenML uses it to authenticate the delivery. The secret is write-only after it is created or rotated: normal webhook responses never expose it.

Secret visibility depends on the operation:

| Operation                          | CLI behavior                  | SDK behavior                       |
| ---------------------------------- | ----------------------------- | ---------------------------------- |
| Create with a generated secret     | Prints the secret once        | Returns it once as `result.secret` |
| Create with a user-supplied secret | Does not print the secret     | `result.secret` is `None`          |
| Rotate to a generated secret       | Prints the replacement once   | Returns it once as `result.secret` |
| Rotate to a user-supplied secret   | Prints the active replacement | Returns it once as `result.secret` |
| Describe, get, or list             | Never exposes the secret      | Never exposes the secret           |

### Use a ZenML-generated secret

When you omit `--secret`, ZenML generates a secret and the CLI prints it once as part of the create command:

```bash
zenml webhook create my-github-webhook --type github
```

Store the printed value securely before leaving the command output. With the SDK, the generated secret is available only on the create result:

```python
result = client.create_webhook(
    name="my-github-webhook",
    webhook_type="github",
)
signing_secret = result.secret.get_secret_value()
```

Subsequent calls to `get_webhook` or `list_webhooks` do not return it.

### Provide your own secret

If an existing credential-management workflow needs to choose the value, pass it during creation:

```bash
zenml webhook create my-github-webhook \
  --type github \
  --secret "$WEBHOOK_SECRET"
```

Via the SDK:

```python
import os

webhook_secret = os.environ["WEBHOOK_SECRET"]
result = client.create_webhook(
    name="my-github-webhook",
    webhook_type="github",
    secret=webhook_secret,
)
```

ZenML does not echo a user-supplied secret. The CLI does not print it, and `result.secret` is `None` in the SDK.

### Rotate a signing secret

Generate a replacement signing secret with:

```bash
zenml webhook rotate-secret my-github-webhook
```

Or provide the replacement yourself:

```bash
zenml webhook rotate-secret my-github-webhook \
  --secret "$NEW_WEBHOOK_SECRET"
```

Via the SDK:

```python
import os

result = client.rotate_webhook_secret("my-github-webhook")
new_secret = result.secret.get_secret_value()

replacement_secret = os.environ["NEW_WEBHOOK_SECRET"]
result = client.rotate_webhook_secret(
    "my-github-webhook",
    secret=replacement_secret,
)
```

The replacement secret is returned once. The previous secret stops authenticating deliveries as soon as rotation completes, so update the external provider with the replacement to resume delivery.

{% hint style="warning" %}
Treat signing secrets as credentials. Do not commit them to source control, include literal values in command history, or log them in production.
{% endhint %}

## Understand delivery responses

Webhook endpoints return intake-level responses:

| Status             | Meaning                                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `200 OK`           | The provider accepted the delivery and selected an immediate success response. Slack uses this for event and control deliveries. |
| `202 Accepted`     | The delivery was accepted for processing, or the provider intentionally ignored an unsupported event before webhook lookup.      |
| `400 Bad Request`  | Required provider metadata is missing, or the body is not a valid top-level JSON object.                                         |
| `401 Unauthorized` | The signature does not match the exact request body.                                                                             |
| `404 Not Found`    | The webhook does not exist, or the provider in the URL does not match its stored type.                                           |
| `409 Conflict`     | The authenticated webhook is inactive.                                                                                           |

For signed requests, the sender and ZenML must calculate the signature over the same raw bytes. Reformatting JSON or adding a trailing newline after calculating the signature changes those bytes and causes authentication to fail.

Provider-specific early handling can refine this behavior. For example, GitHub deliveries with a non-empty but unsupported `X-GitHub-Event` value return `202` without resolving a webhook. See the [GitHub](/pro/core-concepts/webhooks/github.md) and [ClickUp](/pro/core-concepts/webhooks/clickup.md) providers for details. Slack authenticates and acknowledges URL-verification and rate-limit control deliveries with `200`, but does not create events for consumers. See the [Slack provider](/pro/core-concepts/webhooks/slack.md#authentication-and-accepted-envelopes) for its complete envelope behavior.

Successful intake schedules event handlers in an in-process background task. It does not guarantee that a handler starts or completes, and the handoff is not durable if the server process stops after sending the response.

## Inspect intake statistics

Hydrated webhook details include intake statistics that help answer whether a provider is reaching and authenticating with ZenML:

* received, accepted, authentication-failed, and invalid-payload counts;
* the last received and accepted timestamps;
* the last error timestamp and a bounded error summary.

The CLI `describe` command and the SDK `get_webhook` method return hydrated details by default:

```bash
zenml webhook describe my-github-webhook
```

```python
webhook = client.get_webhook("my-github-webhook")
print(webhook.stats.accepted_count)
print(webhook.stats.auth_failed_count)
print(webhook.stats.last_error_summary)
```

These statistics cover intake only. They do not report consumer matches, queue publication, pipeline run creation, or run outcomes.

## Next steps

* [Configure a Slack webhook](/pro/core-concepts/webhooks/slack.md)
* [Configure a GitHub webhook](/pro/core-concepts/webhooks/github.md)
* [Configure a ClickUp webhook](/pro/core-concepts/webhooks/clickup.md)
* [Send custom webhook events](/pro/core-concepts/webhooks/custom.md)
* [Execute snapshots with webhook triggers](/pro/core-concepts/triggers.md#webhook-triggers)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.zenml.io/pro/core-concepts/webhooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
