> 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/clickup.md).

# ClickUp

Configure and test ClickUp webhook delivery to ZenML.

The ClickUp webhook provider authenticates Workspace webhook deliveries and maps supported ClickUp payloads to a small catalog of semantic events. Consumers can use this provider-owned event model instead of depending on ClickUp's raw payload structure. [Webhook triggers](/pro/core-concepts/triggers.md#webhook-triggers) are the currently documented consumer that exposes this event configuration.

ClickUp registers webhooks through its API and generates the signing secret itself. Create the ZenML webhook first so you have an intake URL, then store ClickUp's secret on the ZenML webhook.

## Create the webhook in ZenML

Create a ClickUp webhook in the active project:

```bash
zenml webhook create clickup-ops --type clickup
```

Describe the webhook and copy its endpoint URL:

```bash
zenml webhook describe clickup-ops
```

Via the SDK:

```python
from zenml.client import Client
from zenml.webhooks.providers import BuiltinWebhookType

client = Client()
result = client.create_webhook(
    name="clickup-ops",
    webhook_type=BuiltinWebhookType.CLICKUP,
)

endpoint_url = result.endpoint_url
```

You can ignore the generated ZenML secret. ClickUp will issue a different shared secret when you register the endpoint, and that is the value ZenML must verify. See [Create a webhook](/pro/core-concepts/webhooks.md#create-a-webhook) for secret-management and rotation options.

## Register the webhook in ClickUp

ClickUp does not provide a repository-style webhook settings page. Create the subscription with the [Create Webhook](https://developer.clickup.com/reference/createwebhook) API using a personal API token and your ClickUp Workspace ID.

1. In ClickUp, open **Settings** and create a personal API token.
2. Copy the Workspace ID for the Workspace that should send events. You can also list authorized Workspaces with `GET https://api.clickup.com/api/v2/team` and use the `id` of the Workspace you want.
3. Send a `POST` request that points ClickUp at the ZenML endpoint URL.

```bash
export CLICKUP_TOKEN="<personal-api-token>"
export CLICKUP_WORKSPACE_ID="<workspace-id>"
export WEBHOOK_URL="<endpoint-url-from-zenml>"

curl -s -X POST "https://api.clickup.com/api/v2/team/${CLICKUP_WORKSPACE_ID}/webhook" \
  -H "Authorization: $CLICKUP_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"endpoint\": \"$WEBHOOK_URL\",
    \"events\": [
      \"taskCreated\",
      \"taskUpdated\",
      \"taskDeleted\",
      \"taskStatusUpdated\",
      \"taskMoved\",
      \"taskAssigneeUpdated\",
      \"taskCommentPosted\",
      \"listCreated\",
      \"listUpdated\",
      \"listDeleted\"
    ]
  }"
```

Copy `webhook.secret` from the response. You can optionally include `space_id`, `folder_id`, `list_id`, or `task_id` so ClickUp only sends events from one location. ZenML trigger filters still apply to whatever ClickUp delivers.

ClickUp sends an `X-Signature` header containing a raw hexadecimal HMAC-SHA256 digest of the exact request body. There is no `sha256=` prefix. ZenML verifies this signature before it parses or forwards the delivery.

## Store ClickUp's signing secret

Rotate the ZenML webhook secret to the value ClickUp returned. Until this step succeeds, real ClickUp deliveries fail authentication with `401 Unauthorized`. ClickUp [suspends the webhook](https://developer.clickup.com/docs/webhookhealth) after that response.

```bash
zenml webhook rotate-secret clickup-ops --secret "$CLICKUP_WEBHOOK_SECRET"
```

Via the SDK:

```python
import os

client.rotate_webhook_secret(
    "clickup-ops",
    secret=os.environ["CLICKUP_WEBHOOK_SECRET"],
)
```

## Supported events and filters

The provider reads the ClickUp event name from the JSON `event` field and maps qualifying payloads to ZenML semantic events with the same names:

| ClickUp event         | ZenML semantic event  | Available filter fields                                 |
| --------------------- | --------------------- | ------------------------------------------------------- |
| Task created          | `taskCreated`         | `task_id`, `list_id`, `space_id`, `folder_id`           |
| Task updated          | `taskUpdated`         | `task_id`, `list_id`, `space_id`, `folder_id`           |
| Task deleted          | `taskDeleted`         | `task_id`, `list_id`, `space_id`, `folder_id`           |
| Task status updated   | `taskStatusUpdated`   | `task_id`, `list_id`, `space_id`, `folder_id`, `status` |
| Task moved            | `taskMoved`           | `task_id`, `list_id`, `space_id`, `folder_id`           |
| Task assignee updated | `taskAssigneeUpdated` | `task_id`, `list_id`, `space_id`, `folder_id`           |
| Task comment posted   | `taskCommentPosted`   | `task_id`, `list_id`, `space_id`, `folder_id`           |
| List created          | `listCreated`         | `list_id`, `space_id`, `folder_id`                      |
| List updated          | `listUpdated`         | `list_id`, `space_id`, `folder_id`                      |
| List deleted          | `listDeleted`         | `list_id`, `space_id`, `folder_id`                      |

Event names match ClickUp's camelCase identifiers. Resource IDs are compared as strings, even when ClickUp sends a numeric `list_id`, `space_id`, or `folder_id`. Each event type has its own typed target class, such as `TaskStatusUpdated` or `ListCreated`, so filters that do not apply to that event are rejected at configuration time.

`status` is only available on `taskStatusUpdated`. It is the post-change value from the last `history_items` entry: a string `after` value, or `after.status` when `after` is an object. If you filter on `status` and the payload has no after-value, the event does not match. List events do not accept `task_id`, and task events other than `taskStatusUpdated` do not accept `status`.

A ClickUp event that is missing from this catalog can still authenticate and return `202 Accepted`. It is not mapped to a semantic event, so webhook triggers do not match it. A missing or empty `event` or `webhook_id` field returns `400 Bad Request`.

### String filters

Semantic event fields use ZenML string filters (`StringFilterOption`). You can use:

* a plain string for exact equality, such as `list_id: "162641285"`;
* `equals:`, `notequals:`, `contains:`, `notcontains:`, `startswith:`, and `endswith:` expressions;
* `oneof:` with a non-empty JSON list for exact alternatives, such as `status: 'oneof:["done","complete"]'`;
* `notoneof:` with a non-empty JSON list for exclusions; and
* a YAML list to OR multiple expressions for one field.

All filter fields in the table support these generic webhook string operators. Values configured for the same field use OR logic; different populated fields use AND logic. Omitted fields match any value. If a configured field is absent from a delivery, it does not match. See the complete [string filter operator reference](/pro/core-concepts/triggers.md#string-filter-operators).

For example, this target matches status updates to `done` on one list:

```yaml
target_events:
  - type: taskStatusUpdated
    list_id: "162641285"
    status: done
```

See [Webhook triggers](/pro/core-concepts/triggers.md#clickup-webhook-triggers) for a complete example that uses this configuration to execute a pipeline snapshot.

## Mock a signed ClickUp delivery

You can test the endpoint without asking ClickUp to send a delivery. This example uses a minimal ClickUp-like status payload; the important part is signing and sending exactly the same bytes.

Set the endpoint and the ClickUp signing secret stored on the ZenML webhook:

```bash
export WEBHOOK_URL="<endpoint-url-from-zenml>"
export WEBHOOK_SECRET="<clickup-webhook-secret>"
```

Write the request body without adding a trailing newline:

```bash
printf '%s' '{"event":"taskStatusUpdated","webhook_id":"wh-1","task_id":"abc","list_id":"162641285","history_items":[{"id":"hist-1","after":"done"}]}' > clickup-status.json
```

Calculate the raw hexadecimal signature over the file's exact bytes:

```bash
export SIGNATURE="$(python -c 'import hashlib,hmac,os; body=open("clickup-status.json","rb").read(); print(hmac.new(os.environ["WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest())')"
```

Send those same bytes with the ClickUp signature header:

```bash
curl -i -X POST "$WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -H "X-Signature: $SIGNATURE" \
  --data-binary @clickup-status.json
```

A valid delivery returns `202 Accepted`. This confirms webhook intake, not that a consumer matched or a pipeline run started.

To confirm that signature verification is active, send the same body with an invalid signature:

```bash
curl -i -X POST "$WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -H "X-Signature: invalid" \
  --data-binary @clickup-status.json
```

This request returns `401 Unauthorized`. If a calculated signature fails, verify that the bytes passed to `--data-binary` are the bytes used to calculate the digest; even a whitespace or newline change produces a different signature. Do not prefix the digest with `sha256=`.

ClickUp [immediately suspends](https://developer.clickup.com/docs/webhookhealth) a webhook after an unauthorized response, so align the signing secret before ClickUp starts delivering events.

## Related pages

* [Webhooks](/pro/core-concepts/webhooks.md)
* [GitHub webhooks](/pro/core-concepts/webhooks/github.md)
* [Custom webhooks](/pro/core-concepts/webhooks/custom.md)
* [Webhook triggers](/pro/core-concepts/triggers.md#clickup-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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
