Integrate Bolt.new with ClickUp using your personal API token from Settings → Apps and the ClickUp REST API v2. Build custom task boards, fetch workspace hierarchies, create and update tasks, and track time — all through a Next.js API route that keeps your token server-side. ClickUp's API token auth is simpler than OAuth, making it one of the more approachable productivity tool integrations.
Building Custom ClickUp Dashboards and Automations with Bolt.new
ClickUp's REST API v2 gives you programmatic access to the full workspace hierarchy: teams (workspaces), spaces, folders, lists, and tasks. With this API you can build views and automations that ClickUp's own interface does not offer — custom dashboards combining data from multiple spaces, automated task creation triggered by events in other systems, time tracking reports formatted exactly for your billing workflow, or read-only task boards for stakeholders who do not need a ClickUp account.
Unlike many productivity tool integrations, ClickUp uses a simple bearer token for API authentication rather than OAuth 2.0. Your personal API token — available in Settings → Apps — grants access to all workspaces connected to your account. This means you can build and test the full integration in Bolt's WebContainer without needing to deploy first, since there is no OAuth callback URL requirement. For team-level access in a production app where different users authenticate with their own ClickUp accounts, the OAuth 2.0 flow is available but not required for personal-use tools.
ClickUp enforces a rate limit of 100 requests per minute per token. For most dashboard use cases this is ample. If you are building a sync tool that processes large task lists, implement simple throttling with a small delay between API calls to avoid 429 rate limit errors.
Integration method
Bolt generates the ClickUp integration through conversation — you describe the task board or automation you want and Bolt writes the API route and React component code. ClickUp uses a simple API token for authentication (no OAuth required for personal use), so the full integration can be built and tested in Bolt's development environment without needing to deploy first. API calls flow through server-side Next.js routes to keep the token out of the browser.
Prerequisites
- A ClickUp account with at least one workspace containing spaces, lists, and tasks
- Your ClickUp personal API token (found at app.clickup.com/settings/apps — scroll to API Token)
- A Next.js project in Bolt.new (prompt 'Create a Next.js app' to get started)
- Your workspace ID and at least one list ID (visible in the URL when you open a list in ClickUp: app.clickup.com/{workspaceId}/v/li/{listId})
Step-by-step guide
Get your ClickUp API token and find your workspace IDs
Get your ClickUp API token and find your workspace IDs
ClickUp provides a personal API token that gives access to all your workspaces. To find it, log into ClickUp and go to Settings (bottom-left corner, click your avatar) → Apps → scroll down to the API Token section. Click Generate if you haven't yet, then copy the token. Store it immediately in a safe place — ClickUp only shows it once per generation, though you can regenerate it at any time. Next, find your workspace and list IDs. The easiest way is to look at the URL while browsing ClickUp. When you open a Space or List, the URL pattern is: app.clickup.com/{teamId}/v/li/{listId}. The teamId is your workspace ID (also called team ID in the ClickUp API). You can also fetch these programmatically: the ClickUp API endpoint GET /team returns all teams your token has access to, and from there you can traverse the hierarchy down to spaces and lists. Create a .env.local file in your Bolt project root with the values below. The VITE_* prefix is for client-side access in Vite projects — but for security, never expose your ClickUp API token client-side. Use process.env (no VITE_ prefix) only in server-side API routes. The CLICKUP_TEAM_ID and CLICKUP_LIST_ID values are safe to expose as they are just identifiers, not credentials.
Create a Next.js app with a ClickUp integration setup. Add a .env.local file with CLICKUP_API_TOKEN, CLICKUP_TEAM_ID, and CLICKUP_LIST_ID as placeholder variables. Create a /api/clickup/workspace route that fetches all teams and spaces for my workspace using the ClickUp REST API v2 and returns the hierarchy as JSON.
Paste this in Bolt.new chat
1// .env.local2CLICKUP_API_TOKEN=pk_your_personal_api_token_here3CLICKUP_TEAM_ID=your_workspace_id4CLICKUP_LIST_ID=your_default_list_idPro tip: Your ClickUp workspace ID appears in the URL as the number after app.clickup.com/ — for example, in app.clickup.com/12345678/home, your team ID is 12345678.
Expected result: Your .env.local file is created with placeholder values, and calling /api/clickup/workspace returns your workspace name, spaces, and lists as JSON.
Fetch the workspace hierarchy and display lists
Fetch the workspace hierarchy and display lists
The ClickUp API hierarchy is: Team (Workspace) → Spaces → Folders → Lists. Tasks live inside lists. To build any task view, you first need to know which lists exist. The API provides endpoints for each level: GET /team returns all workspaces, GET /team/{teamId}/space returns spaces in a workspace, GET /space/{spaceId}/folder returns folders, and GET /space/{spaceId}/list returns folderless lists. For most use cases, you need the list IDs up front to query tasks. Rather than traversing the full hierarchy on every page load, fetch the hierarchy once during app setup and store the list IDs in your app state or database. For a personal tool, you can even hardcode the list IDs you care about after finding them through a one-time API call. The API route below fetches all spaces and their lists in one operation by chaining the calls. It returns a simplified structure that is easy to render in a React dropdown or sidebar navigation. All ClickUp API calls use the same Authorization header format: the raw token string — not 'Bearer {token}', just the token itself. This is different from most other APIs.
Create a ClickUp workspace explorer API route at /api/clickup/spaces that fetches all spaces and their lists for my team ID. Then build a React sidebar component that displays the spaces as expandable sections with their lists inside. Clicking a list should set it as the active list and trigger a task fetch.
Paste this in Bolt.new chat
1// app/api/clickup/spaces/route.ts2import { NextResponse } from 'next/server';34const CLICKUP_API = 'https://api.clickup.com/api/v2';5const API_TOKEN = process.env.CLICKUP_API_TOKEN;6const TEAM_ID = process.env.CLICKUP_TEAM_ID;78type ClickUpList = { id: string; name: string; task_count: number };9type ClickUpFolder = { id: string; name: string; lists: ClickUpList[] };10type ClickUpSpace = { id: string; name: string; folders: ClickUpFolder[]; lists: ClickUpList[] };1112async function clickupFetch(path: string) {13 const response = await fetch(`${CLICKUP_API}${path}`, {14 headers: { Authorization: API_TOKEN! },15 });16 if (!response.ok) throw new Error(`ClickUp API error: ${response.status}`);17 return response.json();18}1920export async function GET() {21 try {22 const { spaces } = await clickupFetch(`/team/${TEAM_ID}/space?archived=false`);2324 const spacesWithLists: ClickUpSpace[] = await Promise.all(25 spaces.map(async (space: { id: string; name: string }) => {26 const [{ folders }, { lists: folderlessLists }] = await Promise.all([27 clickupFetch(`/space/${space.id}/folder?archived=false`),28 clickupFetch(`/space/${space.id}/list?archived=false`),29 ]);30 return { id: space.id, name: space.name, folders, lists: folderlessLists };31 })32 );3334 return NextResponse.json({ spaces: spacesWithLists });35 } catch (error) {36 return NextResponse.json(37 { error: error instanceof Error ? error.message : 'Unknown error' },38 { status: 500 }39 );40 }41}Pro tip: Note that the ClickUp Authorization header uses just the raw token string — not 'Bearer token'. Using 'Bearer' will result in 401 errors. This is a common gotcha when coming from other APIs.
Expected result: Calling /api/clickup/spaces returns a JSON array of your ClickUp spaces, each containing their folders and lists with names and task counts.
Build a task list view with filtering and sorting
Build a task list view with filtering and sorting
With list IDs available, you can fetch tasks from any list using GET /list/{listId}/task. The ClickUp task endpoint supports extensive query parameters for filtering: assignees (comma-separated user IDs), statuses (comma-separated status names), due_date_gt and due_date_lt (Unix timestamps in milliseconds for date range filtering), order_by (created, updated, or due_date), and reverse (true/false for sort direction). The task response includes all task fields: id, name, description, status, priority, due_date, assignees, tags, custom fields, and parent (for subtasks). Priority is returned as an object with priority (integer 1-4) and color fields. Status is an object with status (the name string) and type fields. Map these to your UI components accordingly. Pagination is handled through the page query parameter (0-indexed) with 100 tasks per page. Always check if the response indicates more pages are available (last_page field in the response) and implement a Load More button or automatic pagination for lists with more than 100 tasks. Because ClickUp API calls work fine as outbound requests from Bolt's WebContainer, you can build and test this entire task board in the Bolt preview without deploying. Incoming webhooks from ClickUp (for real-time task updates) require a deployed URL, but polling-based task fetching works in development.
Create a ClickUp task board page that fetches tasks from a given list ID using /api/clickup/tasks?listId=. Display tasks as cards with: task name, status badge (color-coded), priority badge (urgent=red, high=orange, normal=blue, low=gray), assignee avatar, and due date. Add filter buttons for status and priority at the top. Use React state for filtering — no additional API calls needed after the initial fetch.
Paste this in Bolt.new chat
1// app/api/clickup/tasks/route.ts2import { NextRequest, NextResponse } from 'next/server';34const CLICKUP_API = 'https://api.clickup.com/api/v2';5const API_TOKEN = process.env.CLICKUP_API_TOKEN;67export async function GET(request: NextRequest) {8 const { searchParams } = new URL(request.url);9 const listId = searchParams.get('listId') ?? process.env.CLICKUP_LIST_ID;10 const page = searchParams.get('page') ?? '0';11 const orderBy = searchParams.get('order_by') ?? 'due_date';1213 const params = new URLSearchParams({14 page,15 order_by: orderBy,16 subtasks: 'true',17 include_closed: 'false',18 });1920 const response = await fetch(21 `${CLICKUP_API}/list/${listId}/task?${params.toString()}`,22 { headers: { Authorization: API_TOKEN! } }23 );2425 if (!response.ok) {26 return NextResponse.json(27 { error: `ClickUp returned ${response.status}` },28 { status: response.status }29 );30 }3132 const data = await response.json();33 return NextResponse.json({34 tasks: data.tasks,35 lastPage: data.last_page,36 });37}Pro tip: ClickUp due dates are stored as Unix timestamps in milliseconds. Convert them with new Date(parseInt(task.due_date)).toLocaleDateString() for display.
Expected result: The task board page displays ClickUp tasks as cards with color-coded status and priority badges, and the filter buttons update the visible tasks using client-side filtering.
Create and update tasks, and log time entries
Create and update tasks, and log time entries
Creating a task requires a POST to /list/{listId}/task with at minimum a name field. Optional fields include description (plain text or Markdown), status (must match an existing status name in the list), priority (1=urgent, 2=high, 3=normal, 4=low), assignees (array of user IDs), due_date (Unix timestamp in milliseconds), and tags (array of tag names). The task creation response includes the full task object with the newly assigned task ID. Updating an existing task uses PUT /task/{taskId} with the same field structure. Only include the fields you want to change — omitted fields are left unchanged. To update an assignee, use the special add or rem arrays inside the assignees object: { assignees: { add: [userId], rem: [] } }. Time tracking in ClickUp uses the Time Tracking API. To log time, POST to /task/{taskId}/time with a start (Unix ms), duration (in milliseconds), and optional description. To start a timer (live tracking), POST to /team/{teamId}/time_entries/start. Fetch time entries for a task with GET /task/{taskId}/time. These mutation endpoints all work from Bolt's WebContainer since they are outbound HTTP requests. No deployment is needed to test task creation and updates — only ClickUp webhook receipt (for real-time change notifications pushed FROM ClickUp) requires a deployed URL.
Add task creation and time logging to my ClickUp integration. Create a /api/clickup/tasks/create route that POSTs a new task to a list with name, description, priority, and due date. Create a /api/clickup/time/log route that logs a time entry for a task ID with start time, duration in hours, and description. Add a simple form UI for creating tasks and a time log form.
Paste this in Bolt.new chat
1// app/api/clickup/tasks/create/route.ts2import { NextRequest, NextResponse } from 'next/server';34const CLICKUP_API = 'https://api.clickup.com/api/v2';5const API_TOKEN = process.env.CLICKUP_API_TOKEN;6const DEFAULT_LIST_ID = process.env.CLICKUP_LIST_ID;78export async function POST(request: NextRequest) {9 const { listId, name, description, priority, dueDate, assignees } = await request.json();1011 const taskBody: Record<string, unknown> = {12 name,13 description: description ?? '',14 priority: priority ?? 3, // 1=urgent, 2=high, 3=normal, 4=low15 assignees: assignees ?? [],16 };1718 if (dueDate) {19 taskBody.due_date = new Date(dueDate).getTime(); // Convert to Unix ms20 taskBody.due_date_time = true;21 }2223 const response = await fetch(24 `${CLICKUP_API}/list/${listId ?? DEFAULT_LIST_ID}/task`,25 {26 method: 'POST',27 headers: {28 Authorization: API_TOKEN!,29 'Content-Type': 'application/json',30 },31 body: JSON.stringify(taskBody),32 }33 );3435 if (!response.ok) {36 const error = await response.json();37 return NextResponse.json({ error }, { status: response.status });38 }3940 const task = await response.json();41 return NextResponse.json({ id: task.id, name: task.name, url: task.url });42}4344// app/api/clickup/time/log/route.ts — log a time entry45export async function logTime(taskId: string, durationHours: number, description: string) {46 const durationMs = Math.round(durationHours * 3600 * 1000);47 const start = Date.now() - durationMs;4849 return fetch(`${CLICKUP_API}/task/${taskId}/time`, {50 method: 'POST',51 headers: {52 Authorization: API_TOKEN!,53 'Content-Type': 'application/json',54 },55 body: JSON.stringify({ start, duration: durationMs, description }),56 });57}Pro tip: Task priority in the ClickUp API uses integers, not strings: 1 = urgent, 2 = high, 3 = normal, 4 = low. The GET task response returns priority as an object with both the integer and a color hex code.
Expected result: Submitting the task creation form creates a new task in your ClickUp list, and the time log form successfully records a time entry against an existing task.
Common use cases
Cross-space task dashboard
Build a consolidated view showing tasks assigned to a specific team member across multiple ClickUp spaces, with priority coloring and due date countdown. Team leads can see workload at a glance without opening each space separately in ClickUp.
Create a ClickUp task dashboard in Next.js. Fetch all tasks assigned to a user ID across multiple list IDs from the ClickUp API. Display them in a table sorted by due date with priority color badges (urgent = red, high = orange, normal = blue, low = gray). Include a filter by space.
Copy this prompt to try it in Bolt.new
Automated task creation from form submissions
When a user submits a bug report or feature request form in your app, automatically create a ClickUp task in the appropriate list with the form data as the task description, assign it to the right team member, and set the priority based on the severity selected in the form.
Add a ClickUp integration to my feedback form. When a user submits the form with a bug title, description, and severity (low/medium/high/critical), create a task in ClickUp list ID [LIST_ID] with the form data. Map severity to ClickUp priorities: critical = urgent, high = high, medium = normal, low = low.
Copy this prompt to try it in Bolt.new
Time tracking report exporter
Pull time entries logged in ClickUp for a selected date range and format them into a billable hours report by client (space) and project (list). Export the report as a CSV that can be imported into an invoicing tool.
Build a time tracking report page that calls the ClickUp API to fetch all time entries for a given date range. Group entries by space name and list name, calculate total hours per group, and display a summary table. Add a Download CSV button that exports the data with columns: Date, Space, List, Task Name, Duration (hours).
Copy this prompt to try it in Bolt.new
Troubleshooting
All ClickUp API calls return 401 OAUTH_027: OAuth token not found
Cause: The Authorization header is incorrectly formatted. ClickUp's API uses the raw token string as the Authorization header value — not 'Bearer {token}'. Additionally, the API token may have been regenerated after you stored it, or the .env variable name is wrong.
Solution: Verify the Authorization header in your fetch call is set to exactly the token value: { Authorization: process.env.CLICKUP_API_TOKEN }. Do not add 'Bearer' or 'Token' prefix. Confirm the token in your .env matches the current token in ClickUp Settings → Apps.
1// Correct — no Bearer prefix:2headers: { Authorization: process.env.CLICKUP_API_TOKEN! }34// Wrong — do NOT use this:5headers: { Authorization: `Bearer ${process.env.CLICKUP_API_TOKEN}` }Task creation returns 400 with 'List not found' even though the list ID looks correct
Cause: The list ID in your request does not match any list accessible to your API token. This can happen if the list is in a workspace your token does not have access to, or if the list was deleted. ClickUp list IDs are numeric — a non-numeric ID or a space ID used instead of a list ID will also cause this error.
Solution: Use the /api/clickup/spaces route you built to fetch the current list of spaces and lists. Find the correct list ID in that response and update CLICKUP_LIST_ID in your .env. Remember that list IDs and space IDs look similar but are different — tasks live in lists, not spaces.
API calls work in development but return CORS errors in the browser console when called directly from a component
Cause: The ClickUp API call is being made directly from a client-side React component using fetch(), which triggers CORS. ClickUp's API does not allow cross-origin requests from browser environments.
Solution: Move all ClickUp API calls to server-side Next.js API routes (app/api/clickup/*/route.ts). Your React components should call your own API routes (e.g., fetch('/api/clickup/tasks')), which then call ClickUp server-to-server without CORS restrictions.
Best practices
- Never put your ClickUp API token in client-side code or in environment variables prefixed with NEXT_PUBLIC_ — always access it only in server-side API routes using process.env.CLICKUP_API_TOKEN.
- Cache the workspace hierarchy (spaces, folders, lists) on the server side and refresh it periodically rather than fetching on every page load — the hierarchy rarely changes and the calls count against your 100 req/min rate limit.
- Add rate limit handling to your API routes: if ClickUp returns a 429 status, read the X-RateLimit-Reset header and retry after that time, or implement exponential backoff.
- Use the include_closed=false query parameter when fetching tasks to exclude completed tasks by default, significantly reducing response size for active workspaces with long histories.
- Fetch user IDs from the GET /team/{teamId}/member endpoint once and store them if your app needs to assign tasks or filter by assignee — avoid hardcoding user IDs which will break if team membership changes.
- For webhook-based real-time updates (task created, status changed), you must deploy your app first since ClickUp webhooks push events to your server's public URL, which the Bolt WebContainer cannot receive.
- Use the ClickUp Custom Fields API to read and set custom field values if your workflow relies on them — custom fields have separate field IDs that you retrieve from the list's field configuration endpoint.
Alternatives
Jira is the enterprise standard for software development teams and offers deeper integration with development tools like GitHub and CI/CD pipelines, though its API is more complex than ClickUp's.
Trello's simpler board-and-card model and REST API are easier to integrate for lightweight task tracking without the complexity of ClickUp's multi-level hierarchy.
Asana offers a well-documented REST API with strong support for portfolio and cross-project views, making it a solid alternative for teams that prefer Asana's UX.
Monday.com uses a GraphQL API that can be more flexible for complex data queries across multiple boards compared to ClickUp's REST v2 approach.
Frequently asked questions
Do I need OAuth for the ClickUp integration or is the API token enough?
For personal tools and single-user dashboards, the personal API token from Settings → Apps is sufficient and much simpler to implement. OAuth 2.0 is only needed if you want other users to connect their own ClickUp accounts to your app — for example, if you are building a SaaS product where each customer logs in with their own ClickUp credentials.
Can I test the ClickUp integration in Bolt's preview before deploying?
Yes, almost entirely. Outbound API calls to ClickUp — fetching tasks, creating tasks, updating tasks, and logging time — all work in Bolt's WebContainer preview. The only limitation is incoming webhooks: if you want ClickUp to notify your app in real-time when tasks change, you need a deployed public URL. For polling-based approaches (refreshing every few seconds), no deployment is needed.
How do I find the user IDs needed to assign tasks?
Call GET https://api.clickup.com/api/v2/team/{teamId}/member with your API token. The response contains an array of member objects, each with an id (the user ID for task assignment), username, and email. Store these to build an assignee picker in your UI.
What is the ClickUp API rate limit and how do I handle it?
ClickUp limits API calls to 100 requests per minute per personal token. If you exceed this, you receive a 429 response with a X-RateLimit-Reset header indicating when the limit resets. For dashboard apps making a few dozen calls on page load this is rarely an issue. For bulk operations, add a 700ms delay between requests to stay safely under the limit.
Can I access ClickUp Docs and Whiteboards through the API?
ClickUp Docs have limited API support — you can create and fetch docs, but editing rich content has constraints. Whiteboards are not currently accessible via the public API. For task, time tracking, and workspace hierarchy operations (the most common use cases), the API coverage is comprehensive.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation