# How to Integrate Bolt.new with ClickUp

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
// .env.local
CLICKUP_API_TOKEN=pk_your_personal_api_token_here
CLICKUP_TEAM_ID=your_workspace_id
CLICKUP_LIST_ID=your_default_list_id
```

**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.

### 2. 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.

```
// app/api/clickup/spaces/route.ts
import { NextResponse } from 'next/server';

const CLICKUP_API = 'https://api.clickup.com/api/v2';
const API_TOKEN = process.env.CLICKUP_API_TOKEN;
const TEAM_ID = process.env.CLICKUP_TEAM_ID;

type ClickUpList = { id: string; name: string; task_count: number };
type ClickUpFolder = { id: string; name: string; lists: ClickUpList[] };
type ClickUpSpace = { id: string; name: string; folders: ClickUpFolder[]; lists: ClickUpList[] };

async function clickupFetch(path: string) {
  const response = await fetch(`${CLICKUP_API}${path}`, {
    headers: { Authorization: API_TOKEN! },
  });
  if (!response.ok) throw new Error(`ClickUp API error: ${response.status}`);
  return response.json();
}

export async function GET() {
  try {
    const { spaces } = await clickupFetch(`/team/${TEAM_ID}/space?archived=false`);

    const spacesWithLists: ClickUpSpace[] = await Promise.all(
      spaces.map(async (space: { id: string; name: string }) => {
        const [{ folders }, { lists: folderlessLists }] = await Promise.all([
          clickupFetch(`/space/${space.id}/folder?archived=false`),
          clickupFetch(`/space/${space.id}/list?archived=false`),
        ]);
        return { id: space.id, name: space.name, folders, lists: folderlessLists };
      })
    );

    return NextResponse.json({ spaces: spacesWithLists });
  } catch (error) {
    return NextResponse.json(
      { error: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    );
  }
}
```

**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.

### 3. 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.

```
// app/api/clickup/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server';

const CLICKUP_API = 'https://api.clickup.com/api/v2';
const API_TOKEN = process.env.CLICKUP_API_TOKEN;

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const listId = searchParams.get('listId') ?? process.env.CLICKUP_LIST_ID;
  const page = searchParams.get('page') ?? '0';
  const orderBy = searchParams.get('order_by') ?? 'due_date';

  const params = new URLSearchParams({
    page,
    order_by: orderBy,
    subtasks: 'true',
    include_closed: 'false',
  });

  const response = await fetch(
    `${CLICKUP_API}/list/${listId}/task?${params.toString()}`,
    { headers: { Authorization: API_TOKEN! } }
  );

  if (!response.ok) {
    return NextResponse.json(
      { error: `ClickUp returned ${response.status}` },
      { status: response.status }
    );
  }

  const data = await response.json();
  return NextResponse.json({
    tasks: data.tasks,
    lastPage: data.last_page,
  });
}
```

**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.

### 4. 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.

```
// app/api/clickup/tasks/create/route.ts
import { NextRequest, NextResponse } from 'next/server';

const CLICKUP_API = 'https://api.clickup.com/api/v2';
const API_TOKEN = process.env.CLICKUP_API_TOKEN;
const DEFAULT_LIST_ID = process.env.CLICKUP_LIST_ID;

export async function POST(request: NextRequest) {
  const { listId, name, description, priority, dueDate, assignees } = await request.json();

  const taskBody: Record<string, unknown> = {
    name,
    description: description ?? '',
    priority: priority ?? 3, // 1=urgent, 2=high, 3=normal, 4=low
    assignees: assignees ?? [],
  };

  if (dueDate) {
    taskBody.due_date = new Date(dueDate).getTime(); // Convert to Unix ms
    taskBody.due_date_time = true;
  }

  const response = await fetch(
    `${CLICKUP_API}/list/${listId ?? DEFAULT_LIST_ID}/task`,
    {
      method: 'POST',
      headers: {
        Authorization: API_TOKEN!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(taskBody),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    return NextResponse.json({ error }, { status: response.status });
  }

  const task = await response.json();
  return NextResponse.json({ id: task.id, name: task.name, url: task.url });
}

// app/api/clickup/time/log/route.ts — log a time entry
export async function logTime(taskId: string, durationHours: number, description: string) {
  const durationMs = Math.round(durationHours * 3600 * 1000);
  const start = Date.now() - durationMs;

  return fetch(`${CLICKUP_API}/task/${taskId}/time`, {
    method: 'POST',
    headers: {
      Authorization: API_TOKEN!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ start, duration: durationMs, description }),
  });
}
```

**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.

## 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.

## 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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).
```

## 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.

```
// Correct — no Bearer prefix:
headers: { Authorization: process.env.CLICKUP_API_TOKEN! }

// Wrong — do NOT use this:
headers: { 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.

## 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.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/clickup
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/clickup
