Guide to ClickUp Webhooks: Features and Best Practices
ClickUp webhooks tell your systems what is happening in a Workspace: a task is created, changes status, gets a new assignee or a comment, moves to another List, or a Space, Folder, Goal or Target changes. If you sync ClickUp with another tool, report on work in progress, or trigger automation from task activity, webhooks are how those changes reach you without polling the API.
This guide covers how ClickUp webhooks are registered and signed, the events and their payloads, the health rules that can switch a webhook off, and the best practices for running a ClickUp receiver in production.
What are ClickUp webhooks?
A ClickUp webhook is a registration created through the API. It has an endpoint URL, the list of events it subscribes to, and optionally a location such as a Space or a List that narrows which events it receives. When a subscribed event happens, ClickUp sends a POST with a JSON body to the endpoint.
Webhooks belong to the user whose token created them. If that user is later disabled, the webhook remains but stops firing, because ClickUp checks before every delivery that the user is still part of the relevant part of the Workspace.
Every delivery is signed. Each registration has its own secret, returned when the webhook is created, and ClickUp sends a hex HMAC-SHA256 of the raw request body in the X-Signature header.
The detail that matters most in production is the health policy. ClickUp suspends a webhook immediately if your endpoint answers 401 or 410, and it sends no notification when it does.
ClickUp webhook features
| Feature | Details |
|---|---|
| Configuration | API only: POST /api/v2/team/{team_. There is no UI for API webhooks |
| Ownership | Tied to the user whose token created the webhook |
| Subscription model | A list of events, or * for all, per registration |
| Location filter | At most one of space_id, folder_id, list_id, task_id. The most specific applies |
| Signature header | X-Signature |
| Scheme | HMAC-SHA256, hex digest |
| Signed content | The raw request body, nothing else |
| Secret | One per registration, returned as webhook.secret |
| Replay window | None. No timestamp is sent or signed |
| Response window | 7 seconds. Any non-2xx is a failure |
| Retries | Up to five per event, then the event is dropped and fail_count goes up |
| Suspension | At fail_count 100, or immediately on a 401 or 410 |
| Notifications | None when a webhook's health changes |
| Source IPs | No dedicated IP addresses |
ClickUp says it uses "our domain name and dynamic addressing" for webhooks, so there is nothing to allowlist and the signature is the credential.
Common events
ClickUp documents 28 events. The event field in the body names the one that fired.
| Area | Events | Fires when |
|---|---|---|
| Tasks | taskCreated, taskUpdated, taskDeleted | A task is created, updated or deleted |
| Task fields | taskPriorityUpdated, taskStatusUpdated, taskAssigneeUpdated, taskDueDateUpdated, taskTagUpdated | A task's priority, status, assignees, due date or tags change |
| Task activity | taskMoved, taskCommentPosted, taskCommentUpdated | A task moves to another List, or a comment is added or edited |
| Time | taskTimeEstimateUpdated, taskTimeTrackedUpdated | A time estimate is set or changed, or tracked time is added, updated or deleted |
| Lists | listCreated, listUpdated, listDeleted | A List is created, updated or deleted |
| Folders | folderCreated, folderUpdated, folderDeleted | A Folder is created, updated or deleted |
| Spaces | spaceCreated, spaceUpdated, spaceDeleted | A Space is created, updated or deleted |
| Goals | goalCreated, goalUpdated, goalDeleted | A Goal is created, updated or deleted |
| Targets | keyResultCreated, keyResultUpdated, keyResultDeleted | A Target (key result) is created, updated or deleted |
Some of these come with companions. ClickUp's payload pages show, for example, a taskUpdated sent alongside taskStatusUpdated, and spaceUpdated alongside listCreated, so one action in the app can produce more than one delivery. ClickUp also notes that adding an attachment to a task does not fire taskUpdated, while uploading an attachment to a task comment does.
Here is ClickUp's example taskStatusUpdated payload:
{
"event": "taskStatusUpdated",
"history_items": [
{
"id": "2800787326392370170",
"type": 1,
"date": "1642736073330",
"field": "status",
"parent_id": "162641062",
"data": { "status_type": "custom" },
"source": null,
"user": {
"id": 183,
"username": "John",
"email": "john@company.com",
"color": "#7b68ee",
"initials": "J",
"profilePicture": null
},
"before": { "status": "to do", "color": "#f9d900", "orderindex": 0, "type": "open" },
"after": { "status": "in progress", "color": "#7C4DFF", "orderindex": 1, "type": "custom" }
}
],
"task_id": "1vj38vv",
"webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204"
}
Every body has event and webhook_id, plus the ID of the resource: task_id, list_id, folder_id, space_id, or goal_id with key_result_id for Targets. history_items appears only "if available". It is missing from Goal and Target events and from most create and delete events outside tasks, and when it is present, before and after change shape with field. date is milliseconds since the epoch as a string, and user.id is an integer. taskTimeTrackedUpdated also carries a top-level data object.
ClickUp adds a few typing notes: unset booleans, such as a Custom Field checkbox, may be null instead of false, and Custom Field values are not normalised.
Setting up ClickUp webhooks
Create the webhook with a ClickUp API token. The team_id is your Workspace ID.
curl -X POST "https://api.clickup.com/api/v2/team/$CLICKUP_TEAM_ID/webhook" \
-H "Authorization: $CLICKUP_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"endpoint": "https://example.com/webhooks/clickup",
"events": ["taskCreated", "taskUpdated", "taskStatusUpdated", "taskCommentPosted"],
"list_id": 9876
}'
The response contains the webhook object, including its id, its health (status and fail_count) and its secret. Store the secret server-side, for example as CLICKUP_WEBHOOK_SECRET. GET /api/v2/team/{team_ lists the webhooks created by the calling user, so read it with the same token.
ClickUp defaults to https if the endpoint has no protocol and warns that plain HTTP may stop being supported. There is no test-send button, so trigger a real change in a test List to check your endpoint.
Securing ClickUp webhooks
Read X-Signature, compute an HMAC-SHA256 of the raw request body with the webhook's secret, encode it as hex, and compare in constant time. If verification fails, answer with a 400, never a 401, because a 401 suspends the webhook.
const crypto = require('crypto');
const express = require('express');
function verifyClickUpSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
app.post('/webhooks/clickup', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyClickUpSignature(req.body, req.get('x-signature'), process.env.CLICKUP_WEBHOOK_SECRET)) {
// Not 401: ClickUp suspends a webhook immediately on a 401.
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
// Route on event.event. Persist the event, then process it asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
import os
def verify_clickup_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
# Compare as bytes: compare_digest raises TypeError on a str with non-ASCII characters.
received = signature_header.strip().lower().encode("utf-8")
return hmac.compare_digest(received, expected.encode("utf-8"))
# In FastAPI: raw_body = await request.body()
# if not verify_clickup_signature(raw_body, request.headers.get("x-signature"),
# os.environ["CLICKUP_WEBHOOK_SECRET"]):
# return Response(status_code=400) # never 401
The signature in ClickUp's docs, f7bc83f430538424b13298e6aa6, is shortened for illustration, so do not test against it. Use the secret clickup-test-webhook-secret and this exact body instead:
{"webhook_id":"7689a169-a000-4985-8676-6902b96d6627","event":"taskCreated","task_id":"c0j"}
The signature should be:
816cdc4127c908c60c729b11a0c2f2bad85f587001a517a06cb773e40660b9a7
Make ClickUp webhooks production-ready. Hookdeck Event Gateway verifies the
X-Signatureheader, answers ClickUp well inside the 7-second limit, and durably queues every task, List and Goal event before your service sees it.
ClickUp webhook limitations and pain points
A 401 switches the webhook off
The Problem: One rejected request and ClickUp stops sending anything, without telling you.
Why It Happens: ClickUp says "returning a 401 HTTP status code for a webhook will immediately mark it as suspended", and a 410 does the same. Returning 401 for a bad signature is the default reflex in most frameworks and auth middleware, so a single mis-signed request, or a secret mix-up after you create a second webhook, takes the integration down.
Workarounds:
- Return
400for a failed signature check, and make sure no auth middleware in front of the route answers401. - Never answer
410from a route that ClickUp calls unless you mean to stop the webhook. - Re-activate a suspended webhook with
PUT /api/v2/webhook/{webhook_andid} "status": "active".
How Hookdeck Can Help: ClickUp talks to Hookdeck rather than to your service, so status codes from your application, including a stray 401 from middleware, never reach ClickUp.
Failed events are gone for good
The Problem: Events sent during an outage never arrive, even after you recover.
Why It Happens: A delivery fails on any non-2xx response or after 7 seconds. ClickUp retries each event up to five times, then increments fail_count and drops it: "Failed events will not be resent." At a fail_count of 100 the webhook is suspended. The retry intervals are not published, and no notification is sent when health changes.
Workarounds:
- Acknowledge quickly and process asynchronously.
- Poll your webhooks'
healthfrom the API and alert on a risingfail_countor asuspendedstatus. - After an outage, reconcile from the API for the window you were down.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, then retries against your service on a schedule you control and lets you replay any event, so your downtime does not count towards fail_count and nothing is dropped.
No timestamp, and duplicates to handle
The Problem: A captured request can be replayed, and retries deliver the same event more than once.
Why It Happens: Only the body is signed and ClickUp sends no timestamp header. Retries after a timeout repeat the event, and some actions also produce companion events.
Workarounds:
- Deduplicate on
{webhook_, which is the key ClickUp suggests.id}:{history_ item_ id} - For events with no
history_items, such as Goal and Target events, build a key fromevent, the resource ID and the time you received it, and keep handlers idempotent.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries before they reach your service, based on the fields you choose.
One secret per registration
The Problem: Several webhooks pointing at one endpoint each sign with a different secret.
Why It Happens: Every registration gets its own secret, returned when it is created. There is no dashboard where you can look it up later.
Workarounds:
- Store each secret against its
webhook_idwhen you create the webhook. - Use a separate path per registration, or look up the secret by
webhook_idfrom the body before verifying.
How Hookdeck Can Help: Give each ClickUp registration its own Hookdeck source and route them to one service. Each source holds its own secret, and your code only has to trust Hookdeck.
Automation webhooks are a different thing
The Problem: Requests from a ClickUp Automation fail your signature check and do not match your event handler.
Why It Happens: The Automation "Call webhook" action posts a different body (auto_id, trigger_id, date and payload, with no event), and its documentation says nothing about X-Signature.
Workarounds:
- Point Automations at a separate route with its own authentication, such as a secret in the URL or a custom header.
- Keep API webhooks and Automation calls apart in your code.
How Hookdeck Can Help: Use a separate Hookdeck source for Automation calls, configured for whatever authentication you add, so it never mixes with your signed API webhooks.
Best practices
Verify against the raw body
The signature covers the exact bytes ClickUp sent. ClickUp's own note about re-stringifying a parsed body "without adding white spaces" is fragile. Read the raw body with express.raw(), Buffer.from(await request.arrayBuffer()) in a Next.js route handler, or await request.body() in FastAPI.
Never return 401 or 410
Make this a rule for the whole route, not just your verifier. Check what your framework, gateway and any auth middleware return for unauthenticated requests.
Watch webhook health
ClickUp stays silent when a webhook fails or is suspended. Poll GET /api/v2/team/{team_ with the token that created the webhooks and alert when health.status is not active.
Keep the creating user active
A webhook stops firing when the user who created it is disabled. Create webhooks with a service account that will not leave the Workspace.
Treat history_items as optional
Check for history_items before reading it, switch on field before reading before and after, and parse date as a string of milliseconds.
Acknowledge fast and process asynchronously
Verify, persist, return 200, and do the real work afterwards. See why to process webhooks asynchronously and our guide to webhook idempotency.
Conclusion
ClickUp signs every webhook with a hex HMAC-SHA256 of the raw body in X-Signature, keyed with that registration's secret. Verify the raw bytes, return 400 rather than 401 when verification fails, deduplicate on webhook_id and history_item_id, and watch fail_count, because ClickUp drops failed events and suspends webhooks without telling you.
Hookdeck Event Gateway sits in front of your receiver, checks every X-Signature, and holds each event until your service has processed it, so an outage on your side costs latency instead of dropped events and a suspended webhook.
Get started with Hookdeck for free and handle ClickUp webhooks reliably in minutes.