---
name: clickup-oauth-app
description: Signs in to ClickUp and registers an OAuth app inside a Workspace's Settings → Apps to obtain an OAuth2 client ID and client secret — with redirect URLs, the fact that ClickUp has no OAuth scopes at all, the Workspace-selection consent model, non-expiring tokens with no refresh token, per-plan rate limits and a safe credential handoff. Use when asked to get ClickUp OAuth credentials, create a ClickUp app, set up ClickUp API access for many customers, rotate a ClickUp client secret, or debug a ClickUp authorization error like `OAUTH_007`, `OAUTH_010`, `OAUTH_017`, `OAUTH_023` "Team not authorized", or a ClickUp webhook that silently stopped firing. For any other vendor's developer portal, use that vendor's skill instead.
---

# ClickUp OAuth2 App Registration

Get a working ClickUp OAuth2 client — an app, redirect URLs, a client ID and a secret — for a platform that connects
many customers' ClickUp Workspaces.

ClickUp's registration form is the smallest in this whole skill collection: a name and a redirect URL. Everything
expensive here is downstream of that form, and four things are not obvious:

1. **ClickUp has no OAuth scopes.** None. There is no `scope` parameter, no consent checkboxes, no scope catalogue.
   The token inherits the authorizing user's permissions **wholesale**. Least privilege is therefore a *which human
   authorizes* problem, not a scope problem (§5).
2. **The user picks Workspaces at consent, and can pick fewer than you expect.** A token that authenticates perfectly
   can be blind to the exact Workspace your customer cares about, and the error says "Team not authorized" — a phrase
   that appears nowhere in your code (§6).
3. **Tokens do not expire and there is no refresh token.** The token response is a single field. That removes an
   entire class of bugs and creates a different one: a long-lived bearer credential with no documented server-side
   revocation endpoint (§7).
4. **The app lives inside one customer-facing Workspace's settings**, created by an owner or admin of that Workspace.
   There is no separate developer portal, no app review and no listing gate — and no obvious owner if that employee
   leaves (§2, §3).

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **App name** | Shown to users at the consent screen; this is the whole branding surface |
| **ClickUp account** that will own the app, and **which Workspace** it is created in | Must be that Workspace's owner or admin (§2) |
| **Redirect URLs** | Every callback host your platform serves (§4) |
| **Which ClickUp plan** the app's home Workspace and your customers are on | Decides rate limits and which endpoints exist at all (§8) |
| **Whether customers will authorize as an admin or as a limited member** | The only lever you have on access breadth (§5) |
| **Whether webhooks are in scope** | Changes the verification you must do (§8, §9) |
| **New app, or an edit to an existing one?** | A new client ID re-authorizes every customer (§1) |

## Quick Start

1. Confirm a **new** app is needed — existing customer connections are bound to the current client ID (§1).
2. Sign in as an **owner or admin** of the Workspace that will host the app (§2).
3. **Settings → Apps → Create new app**; name it and add a redirect URL (§3).
4. Add **every** redirect URL, exactly, https only (§4).
5. Skip the scope step — there isn't one. Read §5 anyway; it is the security answer you will be asked for.
6. Understand Workspace selection before you write a connect flow (§6).
7. Capture the client ID and secret; note that the token never expires and there is no refresh token (§7).
8. Check the rate-limit tier and the Enterprise-only endpoint list against what the connector calls (§8).
9. Verify from a **second account in an unrelated Workspace**, on a non-Enterprise plan (§9).
10. Hand the credentials over — never commit them (§10).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **The docs live at `developer.clickup.com`.** The old `clickup.com/api` documentation host now redirects there.
  Anything that still describes a "team settings → Integrations → ClickUp API" click path is stale; the current path
  is **avatar → Settings → Apps**, and ClickUp's own deep link is `https://app.clickup.com/settings/apps`.
- **Only Workspace owners or admins can create OAuth apps.** ClickUp states this plainly on the authentication page.
- **There are no OAuth scopes.** The published v2 OpenAPI specification contains no `scope`, no `code_challenge` and
  no `refresh_token` anywhere. The only occurrences of the word "scope" in it are about Custom Fields.
- **Authorization URL is `https://app.clickup.com/api`** — a consent page, despite looking like an API route. The
  token endpoint is the differently-hosted `https://api.clickup.com/api/v2/oauth/token`. Mixing the two hosts up is
  the single most common first-day mistake.
- **`state` is documented as an optional extra parameter** on the authorize URL.
- **The token response is documented as `{ "access_token": "..." }`** — `access_token` is the only required
  property. No `refresh_token`, no `expires_in`, no `token_type`, no `scope`.
- **"The access token currently does not expire. This is subject to change."** ClickUp says this twice, on the
  authentication page and in the FAQ. The hedge is theirs; treat expiry as something that could appear.
- **Rate limits are per token, per minute, and changed shape from the older published numbers.** Current table:
  Free Forever / Unlimited / Business **100**, Business Plus **1,000**, Enterprise and Enterprise Plus **10,000**.
  `429` responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.
- **Both v2 and v3 are live and authenticate identically** — same host, same `Authorization` header, one credential.
  v3 is not a migration you must do; it is where the newer surfaces (Docs, Chat, audit logs, attachments, ACLs) are.
- **There is no app review, no directory submission gate and no install cap** documented for using the API. The App
  Center and the partner program are marketing surfaces, not a prerequisite for serving customers.
- **No OAuth revocation endpoint is documented.** There is no `/revoke`, no `/introspect`, no refresh route — the
  only OAuth route in the v2 specification is the token exchange.

If Settings → Apps does not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing app, or register a new one

A new app means a **new client ID, and every existing customer token is bound to the old one** — every customer
re-authorizes and re-picks their Workspaces. Reuse the existing app for: adding a redirect URL, replacing a leaked
secret, or diagnosing an authorization failure.

Register a **new** app only when the user explicitly wants one: a replacement for a compromised app, a separate app
for a different product, a deliberate environment split, or — the ClickUp-specific case — because the existing app
was created inside a Workspace the company no longer controls (§2). Say which path you are taking before you touch
anything.

One consolation specific to ClickUp: because there are no scopes and no consent-time capability list, **you never
have to re-authorize customers just to widen what the connector reads**. Adding a new object type to the integration
changes nothing about the grant. That is unusual, and it is worth saying out loud when someone assumes otherwise.

## 2. Account, and the ownership problem nobody plans for

- Sign in at `https://app.clickup.com`. Settings live under your avatar in the upper-right corner → **Settings** →
  **Apps** in the sidebar, or go straight to `https://app.clickup.com/settings/apps`.
- **You must be an owner or admin of the Workspace** you create the app in. A plain Member cannot.
- **There is no separate developer account and no developer portal.** The app is an object inside an ordinary
  customer-facing ClickUp Workspace's settings, alongside that Workspace's personal API token generator.

That last point is the trap. The credential your whole multi-tenant integration depends on is administered from one
company Workspace, by whoever has owner or admin there. Before you create anything, settle:

- **Which Workspace hosts the app.** Prefer a company-controlled Workspace whose admin list is managed, not a
  personal or trial Workspace someone spun up.
- **Who can see and reset the secret.** Anyone who is an owner/admin of that Workspace and can reach Settings → Apps.
- **What happens when the creating employee leaves.** ClickUp does not document a transfer-ownership flow for an
  OAuth app. Make sure at least two people who will still be there hold owner/admin on the host Workspace, and record
  the app name, client ID and host Workspace somewhere outside ClickUp.

Hand control back to the user for anything only they can do: signup, email verification, 2FA, accepting terms, being
promoted to admin. Do not retry a blocked step in a loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Give
the user the exact ordered click path and the literal redirect URLs from §4 to paste, and continue once they report
back with the client ID.

## 3. Create the app

**Settings → Apps → Create new app.** ClickUp asks for two things:

1. **App name.** This is what the user sees when they land on the consent page. There is no logo, no description, no
   privacy-policy field and no support URL in the OAuth app form — so the name is your entire trust surface. Use the
   product name a customer will recognise, not an internal codename.
2. **Redirect URL** — see §4.

On save you are shown a **`client_id`** and a **`secret`**. That is the whole registration. There is no app type to
pick (public vs confidential), no distribution setting, no environment toggle, no capability list and no submission
step. Anything a runbook tells you to configure beyond these two fields does not exist in this portal.

ClickUp does not publish how many apps a Workspace may hold, whether the secret can be re-read after creation, or
what editing and deleting an app does to issued tokens. **Treat the secret as shown once**: capture it into the
secret store at creation rather than assuming you can come back for it, and if the portal does let you re-read it,
note that as a finding rather than relying on it.

## 4. Redirect URLs

Register **every** callback host your platform serves. For Unified.to these are one per data center; confirm the
current list with the platform owner rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

What ClickUp documents about them:

- **Multiple are supported.** The create-app step says "a redirect URL" (singular), but the error for a mismatch is
  *"Redirect URI does not match the redirect **uris** of this application"* (`OAUTH_007`), and the app holds a set. If
  the form only ever accepts one value, stop and report that rather than registering four separate apps.
- **The `redirect_uri` you send must match a registered value.** A mismatch is `OAUTH_007`; omitting it entirely is
  `OAUTH_017` *"Redirect URI not passed"*. Treat matching as exact — do not rely on trailing-slash or subpath
  forgiveness.
- **Use https.** ClickUp's note is *"Non-SSL redirect URIs may not be supported in the future."* Do not register a
  plaintext callback for a production platform.
- **`redirect_uri` is not part of the token exchange.** ClickUp's token request schema requires exactly `client_id`,
  `client_secret` and `code` — no `redirect_uri`, no `grant_type`. If your HTTP client insists on sending
  `grant_type=authorization_code`, that is your client's habit, not ClickUp's requirement.

## 5. Scopes — there are none, and that is the thing to explain

**ClickUp's OAuth flow has no `scope` parameter and no scope catalogue.** The documented authorize URL is:

```
https://app.clickup.com/api?client_id={client_id}&redirect_uri={redirect_uri}&state={state}
```

and that is the complete parameter list. No `scope`, no `response_type`, no PKCE (`code_challenge` appears nowhere in
ClickUp's published specification). The consent screen offers the user a Workspace picker, not a permission list.

**What the token can therefore do:** everything the authorizing user can do, in the Workspaces they granted. Read and
write. Tasks, Lists, Folders, Spaces, Docs, comments, attachments, time tracking, users, groups — the connector's own
code is the only thing narrowing it. Two consequences worth stating in a security review before someone else does:

- **Least privilege is a choice about *who* authorizes, not *what* you request.** If a customer wants the integration
  confined to one Space, the answer is a ClickUp user account whose own access is confined to that Space, doing the
  authorizing — commonly a dedicated service user invited as a Member or a Guest with access to just that hierarchy.
  There is no API-side configuration that achieves it. Say this plainly; "we request minimal scopes" is not available
  as an answer here.
- **Role is enforced server-side, per endpoint, at call time.** ClickUp's role model (1 owner, 2 admin, 3 member,
  4 guest) is what the token carries. The clearest illustration lives in the v3 audit-log endpoint, which ClickUp
  documents as *"Only the Workspace owner can create audit logs"* **and** Enterprise-only. A token from an admin,
  through a correctly registered app, simply cannot call it. When a customer reports "permission denied" on one
  endpoint while everything else works, check the authorizing user's role before you check anything else.

> **As of 2026-09-20, this platform's ClickUp connector sends no scopes** (correct — ClickUp has none) and its
> authorize call carries exactly `client_id`, `redirect_uri` and a signed `state`. It does **not** use PKCE, which
> matches ClickUp's documented flow. Because there is no scope to widen, adding object types to the connector never
> re-authorizes existing customers. Confirm with the connector's owner before reporting this as settled.

## 6. Workspace selection — where "it authenticated but sees nothing" comes from

This is ClickUp's equivalent of a permission model, and it is invisible from your side.

When the user lands on `https://app.clickup.com/api?...`, they are asked **which of their Workspaces to grant your
app**. They may select one, several, or — in a hurry, on a shared screen, on mobile — not the one your customer
actually meant. The resulting token is silently narrower than everyone assumed.

What to build around it:

- **`GET /api/v2/team` (Get Authorized Workspaces) is the source of truth.** It returns the Workspaces *this token*
  may reach. Call it immediately after the exchange and store the result; never infer Workspace coverage from
  anything the customer told you. (In v2, `team` means Workspace — see §8.)
- **Calling into a Workspace that was not granted returns "Team not authorized"** with error codes `OAUTH_023`,
  `OAUTH_026`, `OAUTH_027`, and `OAUTH_029` through `OAUTH_045`. That wide, unmemorable code range is the single
  highest-value thing to map to a human-readable message in your connector: *"This ClickUp user did not grant access
  to that Workspace — send them back through the connect flow and ask them to tick it."*
- **Re-authorization is the fix, and it is non-destructive.** ClickUp's own instruction is to redirect the user to
  the same authorization URL again to modify Workspace permissions. There is no separate management endpoint.
- **Authorization is per user, not per company.** Each colleague who connects gets their own token, their own
  Workspace selection and their own effective permissions. Two users in the same Workspace can legitimately produce
  two connections that see different data.
- **A user can revoke from their side at any time**, with no notification to you. It surfaces as *"Token not found"* —
  `OAUTH_019`, `OAUTH_021`, `OAUTH_025`, `OAUTH_077`.

> **As of 2026-09-20, this platform's ClickUp connector resolves a single Workspace at connect time and pins the
> connection to it** — it reads the authorizing user's identity, looks through the Workspaces the token can reach,
> picks the one that user is a member of, and stores that one id. Every downstream call that needs a Workspace,
> including webhook registration and the people/group objects, uses that stored id. **A customer who authorizes
> several Workspaces therefore gets one connection covering one of them**, and which one is decided by ordering
> rather than by the customer. If a customer needs two Workspaces, they need two connections. Confirm the current
> behaviour with the connector's owner; it is worth an explicit product decision rather than an implicit one.

## 7. Capture the credentials, and the token model

From **Settings → Apps → your app**: the **`client_id`** and the **`secret`**.

Record:

- **Client ID** and **client secret**, plus the **Workspace that hosts the app** and who administers it (§2)
- Authorize (a consent page, not an API): `https://app.clickup.com/api`
- Token exchange: `POST https://api.clickup.com/api/v2/oauth/token`
- API base: `https://api.clickup.com/api` — one host for everyone, no per-customer domain to discover
- The registered redirect URLs, verbatim

**The exchange.** Body carries exactly `client_id`, `client_secret` and `code`. ClickUp's specification lists both
`application/json` and `application/x-www-form-urlencoded` as accepted content types. The response is documented as a
single required field:

```json
{ "access_token": "..." }
```

**Token behaviour, and why it is unusual:**

- **No expiry.** ClickUp: *"The access token currently does not expire. This is subject to change."* There is nothing
  to refresh and no clock to watch.
- **No refresh token.** `refresh_token` does not appear anywhere in ClickUp's published v2 specification. A connector
  that assumes OAuth always returns one will store `undefined` and be fine — until someone writes a refresh path that
  can never succeed. Do not build one.
- **No documented revoke or introspect endpoint.** The only OAuth route ClickUp publishes is the token exchange. You
  cannot programmatically kill a token you issued; the user revokes from their side, and you find out through
  `OAUTH_019` / `OAUTH_021` / `OAUTH_025` / `OAUTH_077`.
- **So a leaked ClickUp access token is a permanent, unscoped, unrevocable-by-you bearer credential** for everything
  the authorizing user can reach. That is the sentence to put in the security section. Encrypt at rest, never log it,
  and treat a suspected leak as a customer-side revocation request, not something you can clean up yourself.
- **Header format.** ClickUp documents `Authorization: Bearer {access_token}` for OAuth tokens and
  `Authorization: {personal_token}` (bare, `pk_`-prefixed) for personal tokens. Two different shapes on the same
  header; if you support both credential types, do not share one formatter between them without checking.

**The personal-token alternative, and why it is not the answer here.** The same Settings → Apps page generates a
personal API token: `pk_`-prefixed, never expires, no OAuth flow. ClickUp's own guidance is that it is for
"individual or testing purposes," and that for apps others use you implement OAuth. It is genuinely useful for
getting a connector's read path working on day one, and for the docs' own Try-It feature (which explicitly does not
accept OAuth tokens). It is not a multi-tenant answer: it is one person's credential, carries their full access,
every action is attributed to them, it dies when they leave, and regenerating it breaks every app using the old one.
If someone proposes personal tokens to skip registration, say this.

Resetting the client secret: ClickUp does not document what happens to already-issued access tokens. Assume the
worst, never reset without explicit go-ahead and a cutover plan, and verify the effect on one test connection before
telling customers anything.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the transcript
and can be regenerated, then move on.

> **As of 2026-09-20, this platform's ClickUp connector** exchanges the code by POSTing `client_id`, `client_secret`
> and `code` as **query parameters** rather than in the request body — ClickUp's specification documents a JSON or
> form-encoded body, and this has historically worked anyway, but it is a divergence to re-check whenever the
> exchange starts failing. It stores no refresh token and has no refresh path (correct for ClickUp). It sends the
> token in the `Authorization` header **bare, with no `Bearer` prefix**, for both OAuth and personal tokens —
> ClickUp's current documentation specifies `Bearer` for OAuth tokens and bare only for personal ones, so this is a
> second divergence worth confirming against a live call rather than assuming. It also accepts a **personal API
> token** as an alternative credential for single-user setups. Confirm all of this with the connector's owner.

## 8. Plans, versions and rate limits — what actually bites at scale

**Rate limits are per token, per minute, set by the plan of the Workspace hosting the token:**

| Plan | Requests/minute/token |
| --- | --- |
| Free Forever | **100** |
| Unlimited | **100** |
| Business | **100** |
| Business Plus | **1,000** |
| Enterprise / Enterprise Plus | **10,000** |

This is the number that decides whether your product works, and it is brutal at the bottom: **three of the five tiers
share the same 100/minute budget**, and that budget covers everything the connector does for that customer — initial
sync, incremental polls, per-task attachment lookups, webhook-triggered re-fetches. A full-hierarchy crawl of a
mid-sized Workspace (Spaces → Folders → Lists → tasks → subtasks → attachments) exceeds 100 requests in seconds.

Design for it deliberately:

- **Read the plan, don't guess it.** `GET /api/v2/team/{team_id}/plan` (Get Workspace Plan) tells you which tier this
  connection is on. Tier your sync cadence and concurrency off that, not off a global constant.
- **The budget is per token, so one noisy customer cannot throttle another** — but a single connector fanning out
  across one customer's whole Workspace throttles *itself*.
- **Obey the headers.** `429` responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and a Unix-timestamp
  `X-RateLimit-Reset`. Back off to the reset rather than retrying blind.
- **Webhooks are the cheap path.** ClickUp's webhooks exist precisely so you do not poll a 100/minute budget.

**Endpoints that only exist on Enterprise.** The API itself is on every plan, but a documented set of endpoints is
not: Invite User To Workspace, **Get User**, Edit User On Workspace, Remove User From Workspace, and the whole guest
management family. Plus `admin_can_manage` on Update Space, some time-tracking parameters on Business Plus and above,
and the v3 audit-log endpoint (Enterprise **and** Workspace-owner-only).

This matters at connect time. **Get User** is the natural way to enrich the authorizing identity — and it is
Enterprise-only, so on the plan most customers are on it simply fails. **Get Authorized User** (`GET /api/v2/user`)
is not restricted and returns the authorizing user's `id`, `username` and `email` directly. Prefer it for identity.

> **As of 2026-09-20, this platform's ClickUp connector enriches the authorizing user's identity through the
> Enterprise-only per-Workspace user endpoint, inside a swallowed try/catch** — so on Free, Unlimited, Business and
> Business Plus the enrichment silently returns nothing and the connection is created without a captured name or
> email, with no error surfaced. It already calls the unrestricted authorized-user endpoint moments earlier in the
> same flow, and that response carries the email. Worth raising with the connector's owner as a concrete fix rather
> than a documentation note.

**v2 vs v3.** Both are current, and **there is no authentication difference** — same host, same `Authorization`
header, one credential, one rate-limit budget. The difference is vocabulary and coverage:

| | API v2 | API v3 |
| --- | --- | --- |
| Path | `https://api.clickup.com/api/v2/...` | `https://api.clickup.com/api/v3/...` |
| Top-level container | **`team`** — legacy word for Workspace | **`workspaces`** |
| `group` | A user group inside a Workspace | Same meaning |
| Covers | Tasks, Lists, Folders, Spaces, comments, custom fields, views, time tracking, users, **webhooks**, the OAuth token exchange | Docs and pages, Chat, audit logs, attachments, ACLs, a few task operations |

The `team` = Workspace collision is the documentation trap: in v2, `team_id` is a **Workspace** id, while `group_id`
is a user group — and ClickUp's product UI calls user groups "Teams". Three meanings, one word. Name your variables
after the concept, not after the path segment.

> **As of 2026-09-20, this platform's ClickUp connector mixes both versions against a single credential** — v2 for
> tasks, projects, people, groups, comments, attachments and webhooks, and v3 for the Docs/pages objects. That is
> supported and correct; there is no second app, second secret or second header to configure.

## 9. Webhooks — and the two ways they die quietly

Webhooks are registered through the API, not the app settings, with `POST /api/v2/team/{team_id}/webhook` carrying an
`endpoint`, an `events` array (or `"*"` for everything) and optionally one location filter. Read these before
shipping:

- **A webhook belongs to the user whose token created it**, not to your app. ClickUp: *"If the user who created a
  webhook is disabled, the webhook remains but stops triggering"* — it checks the creating user is still in the
  relevant hierarchy before each delivery. A customer offboarding one employee can therefore stop your sync for their
  whole company, with no error anywhere on your side. **Get Webhooks** (`GET /api/v2/team/{team_id}/webhook`)
  likewise returns only the webhooks created by the authenticated user.
- **Signing.** Creating a webhook returns a `secret` unique to that webhook, **in the create response and nowhere
  else**. Every delivery carries `X-Signature`: a hex HMAC-SHA256 of the raw request body under that secret. If you
  discard the secret at creation you can never verify a delivery again without deleting and recreating the webhook.
  Verify against the **raw** body — re-serializing a parsed JSON object changes the bytes and the digest.
- **Health, and automatic disabling.** A non-2xx response, or any response slower than **7 seconds**, marks the
  webhook *failing*. ClickUp retries an event up to five times, then increments `fail_count` and drops that event
  permanently — **failed events are never resent**. Recovery resets `fail_count` automatically. But at
  `fail_count` **100** the webhook is **suspended** and deliveries stop. Two responses suspend it *immediately*:
  **`410`** and — the one that catches platforms — **`401`**. An endpoint that answers `401` to an unrecognised or
  expired webhook id kills its own subscription on the first bad delivery.
- **No notification is sent when the health status changes.** Poll the webhook object's `health.status` and
  `fail_count` yourself; reactivate with `PUT /api/v2/webhook/{webhook_id}` setting status back to active.
- **One location per hierarchy level per webhook**, and the most specific wins. Registering the same configuration
  and location twice returns `OAUTH_171` *"Webhook configuration already exists"*.
- **Use `{{webhook_id}}:{{history_item_id}}` as the idempotency key.** ClickUp says so explicitly; deliveries repeat.
- **No fixed source IPs.** ClickUp states it has no dedicated webhook IP range and uses dynamic addressing, so an
  IP allow-list is not an available control. Signature verification is the control.
- **Deleting is `DELETE /api/v2/webhook/{webhook_id}`.** Singular `webhook`, with the `/v2` segment. Worth
  double-checking in any client: a delete that quietly 404s leaves a live webhook hammering a dead endpoint until it
  accrues 100 failures.

> **As of 2026-09-20, this platform's ClickUp connector registers native webhooks for task and project
> create/update/delete, scoped to the Workspace it pinned at connect time (§6) and optionally to a Space, Folder or
> List.** Two gaps to raise with its owner: it **keeps only the webhook id from the create response and discards the
> per-webhook signing secret**, so incoming deliveries cannot be signature-verified and are not; and its
> **unsubscribe call targets a path that does not match ClickUp's documented delete route** (it omits the `/v2`
> segment and pluralises the resource), which would make webhook teardown a silent no-op and leave orphaned
> subscriptions failing against a retired endpoint. Both are verifiable against a live connection in minutes.

## 10. Verify end-to-end

Authorizing in the Workspace that hosts the app, as its owner, on whatever plan your company is on, proves almost
nothing. Test the path a customer takes:

1. Authorize as a **different user in an unrelated Workspace**, on a **non-Enterprise plan**, through your platform's
   real connect flow.
2. At the consent screen, deliberately grant **one** Workspace while the account has more than one. Confirm your
   connector notices, and that a call into an ungranted Workspace produces a message a support agent can act on —
   not a raw `OAUTH_031`.
3. Immediately after the exchange, call `GET /api/v2/team` and store the authorized Workspace list.
4. Confirm the token response has **no** `refresh_token` and **no** `expires_in`, and that nothing in your code path
   waits for either.
5. Confirm the identity enrichment works on that non-Enterprise plan — this is where an Enterprise-only endpoint
   fails silently.
6. Authorize again as a **limited Member or Guest** and confirm the connector degrades sensibly rather than throwing:
   with no scopes, role is the only access control, and a guest's token is a legitimate customer configuration.
7. If webhooks are in play: create one, capture and store the `secret`, verify one real `X-Signature` against the raw
   body, then **delete it and confirm it is actually gone** via Get Webhooks.
8. Run a realistic initial sync against a Workspace on a 100/minute plan and watch for `429`.

| Symptom | Cause |
| --- | --- |
| `OAUTH_010` "Client Not Found" | Wrong `client_id`, or the app was not created/saved properly (§3) |
| `OAUTH_007` "Redirect URI does not match…" | The `redirect_uri` sent is not one of the app's registered values (§4) |
| `OAUTH_017` "Redirect URI not passed" / "Authorization Header Required" | `redirect_uri` missing from the authorize call, or no `Authorization` header on an API call (§4, §7) |
| `OAUTH_023` / `OAUTH_026` / `OAUTH_027` / `OAUTH_029`–`OAUTH_045` "Team not authorized" | The user did not grant that Workspace. Re-send them to the authorize URL (§6) |
| `OAUTH_019` / `OAUTH_021` / `OAUTH_025` / `OAUTH_077` "Token not found" | The user revoked, or a personal token was regenerated (§7) |
| Authorizes fine, then reaches the wrong Workspace | Multi-Workspace grant collapsed to one connection (§6) |
| Auth works, one endpoint 403s at one customer | Authorizing user's role, or an Enterprise-only endpoint (§5, §8) |
| Identity/email is blank on connect, no error logged | Enterprise-only user endpoint failing silently on a lower plan (§8) |
| `429` at modest volume | 100/minute tier. Read the plan and the `X-RateLimit-*` headers (§8) |
| Webhook stopped firing, nothing in your logs | Creating user disabled or removed from the hierarchy (§9) |
| Webhook suspended after one bad deploy | Your endpoint answered `401` or `410` — both suspend immediately (§9) |
| Webhooks degrade over days, then stop | `fail_count` climbing to 100 from slow (>7s) or non-2xx responses (§9) |
| `OAUTH_171` | A webhook with that configuration and location already exists (§9) |
| Signature never matches | Digest taken over a re-serialized body instead of the raw bytes, or the wrong secret (§9) |
| `401` on every call with a token you know is good | Header shape: OAuth wants `Bearer <token>`, personal tokens are sent bare (§7) |

## 11. Hand off — never commit the secret

- **Do not** write the client secret or any access/personal token into source control, a test, a fixture, a committed
  `.env`, a ticket, a PR body, or a chat channel. Values go to the user, for the secret store or console. ClickUp
  tokens do not expire, so a leaked one stays valid until a human revokes it (§7) — the blast radius is larger than
  for vendors with short-lived tokens.
- If a code change is needed (a redirect host, a rate-limit tier, a webhook path), keep it secret-free and say what
  the human must set out of band.
- Close with: app name; **the Workspace that hosts the app and who administers it**; the client ID; where the secret
  was delivered; the authorize page and token endpoint; the registered redirect URLs; **explicitly, that ClickUp has
  no scopes and the token carries the authorizing user's full permissions**; the Workspace-selection behaviour you
  observed at consent; the rate-limit tier of the Workspaces you tested; whether webhooks were registered and whether
  their secrets are stored; and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the Workspace that would host the app is personal, a trial, or
administered by one person (§2); a customer's security review asks for scope restriction and the honest answer is
"use a restricted ClickUp user" (§5); a customer needs more than one Workspace from a single connection (§6);
regenerating the client secret is proposed and nobody knows what it does to live tokens (§7); a customer reports a
permanent `429` and the fix is a plan upgrade they must pay for (§8); a suspected token leak needs customer-side
revocation because you have no revoke endpoint (§7); or Settings → Apps does not match the **Platform state** section
above.

## References

Official ClickUp pages only; every URL below returned HTTP 200 on 2026-09-20. The app settings themselves live at
`https://app.clickup.com/settings/apps` and require sign-in. ClickUp help-centre articles are served behind a
challenge that refuses non-browser clients, so none are cited here — read them in a browser if you need the
product-side view.

- Authentication: personal token, OAuth flow, app creation, Workspace selection — https://developer.clickup.com/docs/authentication
- Get Access Token (the token endpoint's full schema) — https://developer.clickup.com/reference/getaccesstoken
- Get Authorized Workspaces (`GET /v2/team`) — https://developer.clickup.com/reference/getauthorizedteams
- Get Authorized User (`GET /v2/user`) — https://developer.clickup.com/reference/getauthorizeduser
- Get User (Enterprise-only) — https://developer.clickup.com/reference/getuser
- Get Workspace Plan — https://developer.clickup.com/reference/getworkspaceplan
- Rate limits by plan, and the `X-RateLimit-*` headers — https://developer.clickup.com/docs/rate-limits
- API availability by plan (the Enterprise-only endpoint list) — https://developer.clickup.com/docs/apis-available-by-plan
- ClickUp API v2 and v3 terminology (team vs Workspace vs group) — https://developer.clickup.com/docs/general-v2-v3-api
- Common errors and the `OAUTH_*` codes — https://developer.clickup.com/docs/common_errors
- FAQ (token expiry, `Content-Type`, subtasks, user roles) — https://developer.clickup.com/docs/faq
- Webhooks: scope, locations, events, delivery contract — https://developer.clickup.com/docs/webhooks
- Webhook signature (`X-Signature`, HMAC-SHA256) — https://developer.clickup.com/docs/webhooksignature
- Webhook health status (failing, suspended, `fail_count`) — https://developer.clickup.com/docs/webhookhealth
- Task webhook payload examples — https://developer.clickup.com/docs/webhooktaskpayloads
- Create Webhook — https://developer.clickup.com/reference/createwebhook
- Get Webhooks — https://developer.clickup.com/reference/getwebhooks
- Update Webhook (reactivate a suspended one) — https://developer.clickup.com/reference/updatewebhook
- Delete Webhook — https://developer.clickup.com/reference/deletewebhook
- OpenAPI specifications page — https://developer.clickup.com/docs/open-api-spec
- v2 OpenAPI specification (raw) — https://developer.clickup.com/openapi/clickup-api-v2-reference.json
- v3 OpenAPI specification (raw) — https://developer.clickup.com/openapi/ClickUp_PUBLIC_API_V3.yaml
- Try the API in your web browser (personal tokens only) — https://developer.clickup.com/docs/trytheapi
- Documentation index for machine readers — https://developer.clickup.com/llms.txt
- Developer docs home (the old `clickup.com/api` redirects here) — https://developer.clickup.com/
- ClickUp plans and pricing — https://clickup.com/pricing
- ClickUp integrations / App Center — https://clickup.com/integrations
