# How to Integrate Bolt.new with Teamwork

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

## TL;DR

Connect Bolt.new to Teamwork using its REST API and HTTP Basic authentication — the simplest auth pattern available. Generate an API key in Teamwork Settings → API & Integrations, then fetch projects, tasks, and time entries via Next.js API routes. Build a project overview dashboard, create tasks, and log time. All outbound API calls work in Bolt's WebContainer during development. Teamwork webhooks require a deployed URL.

## Build a Custom Teamwork Project Dashboard in Bolt.new

Teamwork is a well-established project management platform used by agencies, professional services firms, and client-services teams. Its REST API gives you full access to the platform's data — projects, task lists, tasks, subtasks, milestones, time entries, files, messages, and more. A custom Bolt-built dashboard on top of Teamwork lets you surface the specific views your team needs without navigating Teamwork's full interface — a simplified daily task view for developers, an executive summary of project health for managers, or a client-facing status page showing deliverables without internal details.

What makes Teamwork's API particularly approachable is its authentication model. Unlike APIs that require OAuth flows or complex token management, Teamwork uses HTTP Basic authentication with the API key as the username. You generate a key in Settings, encode it with a colon and any placeholder password in base64, and include it as the Authorization header on every request. There is no token expiry, no refresh flow, and no redirect URIs to configure. You can build and test the full integration in Bolt's preview without any deployment step.

Teamwork's API follows a RESTful pattern with consistent endpoint structure. The base URL includes your Teamwork subdomain: `https://yoursite.teamwork.com/`. Common resource endpoints include `/projects.json`, `/tasklists.json`, `/tasks.json`, `/time_entries.json`, and `/milestones.json`. Responses return JSON objects that nest the resource inside a key matching the resource name. This nesting means a project list response returns `{ projects: [...] }` — your API route should extract the relevant array before returning it to the client.

## Before you start

- A Bolt.new account with a Next.js project
- A Teamwork account with admin or API access enabled
- A Teamwork API key (generated in Settings → API & Integrations)
- Your Teamwork site subdomain (the part before .teamwork.com in your URL)

## Step-by-step guide

### 1. Generate Teamwork API Key and Configure Authentication

Teamwork uses HTTP Basic authentication with the API key as the username. This is one of the simplest authentication patterns possible — no OAuth, no tokens to refresh, no redirect URIs. You need two pieces of information: the API key and your Teamwork subdomain.

To get the API key, log into Teamwork and click your profile avatar in the top-right corner. Select 'Edit My Details.' In the Profile settings, scroll down to 'API & Integrations' or look for an 'API Keys' section (the exact location varies slightly between Teamwork plan versions). Click 'Show API Token' or 'Generate New Token.' Copy the API key — it looks like a long alphanumeric string.

Your Teamwork subdomain is the part before `.teamwork.com` in your account URL. If you access Teamwork at `myagency.teamwork.com`, your subdomain is `myagency`. You will use this to construct the base URL for all API calls: `https://myagency.teamwork.com/`.

For HTTP Basic authentication, the Authorization header requires base64-encoded `{apiKey}:{anyPassword}`. Teamwork's documentation uses `X` as the placeholder password. In Node.js, encode this as: `Buffer.from(`${apiKey}:X`).toString('base64')` and include it as `Basic {encodedValue}` in the Authorization header. In practice, creating a reusable `teamworkFetch` helper that handles this encoding once is cleaner than repeating it in every API route.

Add your API key and subdomain to the project's .env file. Keep the API key server-side only (no NEXT_PUBLIC_ prefix) since it provides full access to your Teamwork account data.

```
// lib/teamwork.ts
const getTeamworkBaseUrl = () => {
  const subdomain = process.env.TEAMWORK_SUBDOMAIN;
  if (!subdomain) throw new Error('TEAMWORK_SUBDOMAIN is not configured');
  return `https://${subdomain}.teamwork.com`;
};

const getAuthHeader = () => {
  const apiKey = process.env.TEAMWORK_API_KEY;
  if (!apiKey) throw new Error('TEAMWORK_API_KEY is not configured');
  // HTTP Basic auth: base64(apiKey:X)
  const encoded = Buffer.from(`${apiKey}:X`).toString('base64');
  return `Basic ${encoded}`;
};

interface TeamworkFetchOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  params?: Record<string, string | number | boolean>;
  body?: unknown;
}

export async function teamworkFetch<T = unknown>(
  endpoint: string,
  options: TeamworkFetchOptions = {}
): Promise<T> {
  const baseUrl = getTeamworkBaseUrl();
  const url = new URL(`${baseUrl}/${endpoint}`);

  if (options.params) {
    Object.entries(options.params).forEach(([key, value]) => {
      url.searchParams.set(key, String(value));
    });
  }

  const response = await fetch(url.toString(), {
    method: options.method || 'GET',
    headers: {
      Authorization: getAuthHeader(),
      'Content-Type': 'application/json',
    },
    body: options.body ? JSON.stringify(options.body) : undefined,
  });

  if (!response.ok) {
    const text = await response.text().catch(() => '');
    throw new Error(`Teamwork API ${response.status}: ${text || response.statusText}`);
  }

  return response.json() as Promise<T>;
}

// Convenience types for common Teamwork resources
export interface TeamworkProject {
  id: string;
  name: string;
  description: string;
  status: string;
  'start-date': string;
  'end-date': string;
  'completed-count': number;
  'uncompleted-count': number;
  'is-project-admin': boolean;
}

export interface TeamworkTask {
  id: string;
  content: string;
  status: string;
  priority: string;
  'due-date': string;
  completed: boolean;
  'project-id': string;
  'project-name': string;
  'responsible-party-names': string;
  'estimated-minutes': number;
}
```

**Expected result:** The Teamwork helper is configured. A quick test calling teamworkFetch('projects.json') should return your projects wrapped in a { projects: [...] } object.

### 2. Build the Projects Overview API Route

Teamwork's project list endpoint is `GET /projects.json`. It returns all projects accessible to the API key holder. Key query parameters: `status` (active, archived, template, deleted — default is active), `orderby` (for sorting by name, date, startdate, enddate, lastactivity), `page` and `pageSize` for pagination (default 50 per page, max 250).

Each project object in the response includes: `id`, `name`, `description`, `status`, `start-date`, `end-date`, `completed-count` (completed tasks), `uncompleted-count` (open tasks), `overdue-count` (tasks past due), and `logo`. Note the hyphenated field names — Teamwork's API uses kebab-case for most fields, which requires bracket notation in JavaScript: `project['completed-count']`.

For a health dashboard, calculate a completion percentage from completed and uncompleted counts: `(completed / (completed + uncompleted)) * 100`. Add a health status label based on the percentage and whether any tasks are overdue: if overdue count is greater than zero and completion is below 50%, mark as 'At Risk'; if overdue count is zero, mark as 'On Track'; otherwise 'Needs Attention'.

For getting task counts with more detail (like overdue tasks), you may need to combine the project list endpoint with individual project statistics. The endpoint `GET /projects/{projectId}/tasks.json?filter=overdue` returns overdue tasks for a specific project. For a dashboard showing many projects, make these calls concurrently with Promise.all, but be mindful of Teamwork's rate limits (typically 150 requests per minute on most plans).

Teamwork also exposes `GET /stats.json` which returns account-level statistics and `GET /projects/{id}/stats.json` for project-level metrics including completion percentage — use this endpoint directly rather than calculating it manually for accuracy.

```
// app/api/teamwork/projects/route.ts
import { NextResponse } from 'next/server';
import { teamworkFetch, TeamworkProject } from '@/lib/teamwork';

interface TeamworkProjectsResponse {
  projects: TeamworkProject[];
}

export async function GET() {
  try {
    const data = await teamworkFetch<TeamworkProjectsResponse>('projects.json', {
      params: { status: 'active', orderby: 'name', pageSize: 250 },
    });

    const projects = (data.projects || []).map((p) => {
      const completed = p['completed-count'] || 0;
      const open = p['uncompleted-count'] || 0;
      const total = completed + open;
      const completionPct = total > 0 ? Math.round((completed / total) * 100) : 0;

      // Simple health heuristic
      let healthStatus = 'On Track';
      if (completionPct < 25 && total > 5) healthStatus = 'Early Stage';
      if (p['end-date']) {
        const daysLeft = Math.ceil(
          (new Date(p['end-date']).getTime() - Date.now()) / (1000 * 60 * 60 * 24)
        );
        if (daysLeft < 0 && open > 0) healthStatus = 'Overdue';
        else if (daysLeft < 7 && open > 0) healthStatus = 'At Risk';
      }

      return {
        id: p.id,
        name: p.name,
        description: p.description || '',
        status: p.status,
        completedTasks: completed,
        openTasks: open,
        completionPct,
        startDate: p['start-date'] || null,
        endDate: p['end-date'] || null,
        healthStatus,
      };
    });

    return NextResponse.json({ projects, total: projects.length });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to fetch projects';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** The projects API route returns all active projects with health status calculations. You should see your real Teamwork projects in the JSON response with completion percentages and health labels.

### 3. Fetch Tasks and Build the Task View

Tasks in Teamwork are officially called 'todo-items' in the API. The endpoint for listing all tasks in an account is `GET /tasks.json`, but this is rarely what you want — typically you fetch tasks for a specific project (`GET /projects/{id}/tasks.json`) or tasks assigned to a specific person (`GET /tasks.json?responsible-party-ids={personId}`).

The task object contains: `id`, `content` (the task name), `status` ('new' or 'completed'), `priority` (none, low, medium, high), `due-date` (YYYY-MM-DD format), `completed` (boolean), `project-id`, `project-name`, `responsible-party-names` (comma-separated list of assignees), `estimated-minutes`, and `description` (HTML or plain text).

For filtering tasks, the `filter` query parameter accepts: `all` (default), `active`, `completed`, `upcoming`, `overdue`, and `late`. The `overdue` filter returns tasks past their due date that are not completed. This is immediately useful for building an overdue task view without any client-side date calculations.

For date range filtering, use `dueDate-start` and `dueDate-end` query parameters with YYYY-MM-DD format dates. To get today's tasks, set both start and end to today's date. To get this week's tasks, set start to Monday and end to Sunday of the current week.

When fetching tasks for a dashboard, request only the fields you need using the `fields` parameter — this reduces response size. For person IDs (needed for filtering by assignee), first call `GET /people.json` to get team members with their IDs.

Task completion toggle is done with a `POST /tasks/{id}/complete.json` request — no body required, just the POST. Task incompletion uses `POST /tasks/{id}/uncomplete.json`. These simple endpoints make building interactive task lists with checkboxes straightforward.

```
// app/api/teamwork/tasks/route.ts
import { NextResponse } from 'next/server';
import { teamworkFetch } from '@/lib/teamwork';

interface TeamworkTasksResponse {
  'todo-items': Array<{
    id: string;
    content: string;
    status: string;
    priority: string;
    'due-date': string;
    completed: boolean;
    'project-id': string;
    'project-name': string;
    'responsible-party-names': string;
    'estimated-minutes': number;
    description: string;
  }>;
}

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const projectId = searchParams.get('projectId');
  const filter = searchParams.get('filter') || 'active';
  const responsiblePartyIds = searchParams.get('responsiblePartyIds');
  const page = searchParams.get('page') || '1';

  const endpoint = projectId
    ? `projects/${projectId}/tasks.json`
    : 'tasks.json';

  const params: Record<string, string> = {
    filter,
    page,
    pageSize: '100',
    'sort': 'duedate',
    'sort-order': 'asc',
  };

  if (responsiblePartyIds) params['responsible-party-ids'] = responsiblePartyIds;

  try {
    const data = await teamworkFetch<TeamworkTasksResponse>(endpoint, { params });
    const tasks = (data['todo-items'] || []).map((t) => ({
      id: t.id,
      content: t.content,
      priority: t.priority || 'none',
      dueDate: t['due-date'] || null,
      completed: t.completed,
      projectId: t['project-id'],
      projectName: t['project-name'],
      assignees: t['responsible-party-names'] || '',
      estimatedMinutes: t['estimated-minutes'] || 0,
    }));

    return NextResponse.json({ tasks, total: tasks.length });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to fetch tasks';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** The tasks API route returns tasks with priority, due date, and assignee information. The overdue filter returns only tasks past their due date. The completion toggle endpoint toggles the task status in Teamwork.

### 4. Add Time Logging and Task Creation

Creating tasks and logging time are the two most common write operations in a Teamwork integration. Both use POST requests and follow the same pattern as other Teamwork API operations: authenticate with HTTP Basic, post to the appropriate endpoint, check for a 201 Created response.

To create a task, POST to `POST /tasklists/{tasklistId}/tasks.json` with a JSON body nested inside a `todo-item` key: `{ 'todo-item': { content: 'Task name', 'due-date': 'YYYY-MM-DD', priority: 'high', 'responsible-party-id': '12345' } }`. Alternatively, use `POST /projects/{projectId}/tasks.json` to create in the default task list of a project. The task list ID is required for precise placement — first fetch task lists with `GET /projects/{projectId}/tasklists.json` to get available lists.

Time entries attach to a task via `POST /tasks/{taskId}/time_entries.json` with body: `{ 'time-entry': { description: 'Work description', hours: 2, minutes: 30, date: 'YYYY-MM-DD', 'person-id': '12345', billable: true } }`. The time entry date is required and must be in YYYY-MM-DD format. The `billable` flag affects reporting for client billing. If person-id is omitted, the time is logged for the API key owner.

To get available person IDs for a project (needed for assigning tasks and logging time for specific people), call `GET /projects/{projectId}/people.json`. This returns team members on the project with their IDs. For account-wide people, use `GET /people.json`.

Both create operations return a `STATUS` field in the response with value `OK` on success. Unlike REST conventions, Teamwork sometimes returns 200 instead of 201 for creation — check the STATUS field rather than the HTTP status code for confirmation.

```
// app/api/teamwork/time-entries/route.ts
import { NextResponse } from 'next/server';
import { teamworkFetch } from '@/lib/teamwork';

interface TimeEntry {
  id: string;
  description: string;
  hours: number;
  minutes: number;
  date: string;
  'person-first-name': string;
  'person-last-name': string;
  'project-name': string;
  'todo-item-name': string;
  billable: boolean;
}

interface TimeEntriesResponse {
  'time-entries': TimeEntry[];
}

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const personId = searchParams.get('personId');
  const date = searchParams.get('date') || new Date().toISOString().split('T')[0];

  try {
    const data = await teamworkFetch<TimeEntriesResponse>('time_entries.json', {
      params: {
        'fromdate': date,
        'todate': date,
        ...(personId ? { 'userId': personId } : {}),
      },
    });

    const entries = data['time-entries'] || [];
    const totalMinutes = entries.reduce(
      (sum, e) => sum + (e.hours || 0) * 60 + (e.minutes || 0),
      0
    );

    return NextResponse.json({
      entries,
      totalHours: Math.floor(totalMinutes / 60),
      totalMinutes: totalMinutes % 60,
    });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to fetch time entries';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}

export async function POST(request: Request) {
  const { taskId, hours, minutes, date, description, billable = true } = await request.json();

  if (!taskId || (!hours && !minutes)) {
    return NextResponse.json({ error: 'taskId and hours or minutes are required' }, { status: 400 });
  }

  try {
    const result = await teamworkFetch(`tasks/${taskId}/time_entries.json`, {
      method: 'POST',
      body: {
        'time-entry': {
          description: description || '',
          hours: hours || 0,
          minutes: minutes || 0,
          date: date || new Date().toISOString().split('T')[0],
          billable,
        },
      },
    }) as { STATUS?: string; timeLogId?: string };

    if (result.STATUS !== 'OK') {
      throw new Error('Time entry creation failed');
    }

    return NextResponse.json({ success: true, timeLogId: result.timeLogId });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to log time';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** Time entries are successfully created in Teamwork and appear in the project's time log. The GET route returns today's time entries with a calculated total hours summary.

## Best practices

- Store TEAMWORK_API_KEY as a server-side only environment variable without the NEXT_PUBLIC_ prefix — the API key grants full access to your Teamwork account and must never be exposed in client bundles.
- Cache project and task list data for 60-120 seconds to reduce API calls — Teamwork rate limits vary by plan (typically 150 requests per minute) and dashboard refreshes can quickly accumulate calls.
- Handle Teamwork's kebab-case field names consistently by defining TypeScript interfaces with quoted property names ('due-date': string) and converting to camelCase in your API route's map function before returning data to React components.
- Always extract the resource array from the correct response key — projects live in data.projects, tasks in data['todo-items'], time entries in data['time-entries']. Check the key name in Teamwork's API docs before building new endpoints.
- Use the filter parameter on task endpoints rather than fetching all tasks and filtering client-side — 'overdue' and 'upcoming' filters are computed by Teamwork's servers and reduce data transfer significantly.
- Implement task creation with task list selection — always present users with a dropdown of task lists for the selected project (GET /projects/{id}/tasklists.json) so tasks land in the correct list rather than a default.
- For time logging features, always require date selection with a clear default (today) — Teamwork rejects time entries without a date, and users often forget to set it when using a rapid-entry interface.
- Before building complex multi-endpoint dashboards, check your Teamwork plan's API rate limits in Settings → API & Integrations to understand the ceiling and design your caching strategy accordingly.

## Use cases

### Project Overview Dashboard

Build a dashboard that shows all active projects with their health status, task completion percentages, upcoming milestones, and overdue tasks. Give project managers a quick view of portfolio health without drilling into each project individually in Teamwork. Include color-coded status indicators (green for on-track, yellow for at-risk, red for overdue) based on task completion rates and upcoming deadlines.

Prompt example:

```
Build a Teamwork project overview dashboard. Create a Next.js API route at /api/teamwork/projects that fetches all active projects from Teamwork using HTTP Basic auth with TEAMWORK_API_KEY from process.env and TEAMWORK_SUBDOMAIN for the base URL. Return project name, description, status, task count, completed task count, overdue tasks count, and next milestone. Build a React dashboard with a project card grid. Each card shows the project name, a circular progress indicator for task completion, overdue count in red, and next milestone with date. Color the card border green/yellow/red based on health.
```

### Personal Task View for Team Members

Create a focused task view showing only the tasks assigned to a specific person. Filter by due date to show today's tasks, this week's tasks, and overdue tasks separately. Allow team members to update task completion status directly from the Bolt interface. A simplified personal view removes the cognitive overhead of navigating full project trees.

Prompt example:

```
Build a personal task view using Teamwork API. Create a Next.js API route at /api/teamwork/my-tasks that fetches tasks assigned to a specific person using the /tasks.json endpoint with responsible-party-ids filter param. Separate tasks into: Overdue (past due date), Due Today, Due This Week, and Upcoming. Build a React task list with checkboxes that call /api/teamwork/tasks/[id]/complete to toggle completion. Show task name, project name, priority badge, and due date. Accept a userId query param. Store TEAMWORK_API_KEY and TEAMWORK_SUBDOMAIN in process.env.
```

### Time Tracking Logger

Build a streamlined time logging interface that lets team members log hours to tasks without navigating to the correct task in Teamwork's tree structure. Show a recent tasks list, a quick search, and a time entry form. Submit time logs directly to Teamwork via API. Ideal for consultancies that bill by the hour and need fast, friction-free time capture.

Prompt example:

```
Create a time tracking logger using Teamwork API. Build a /api/teamwork/time-entries route that accepts GET (fetch today's time entries for a user) and POST (create a new time entry with { taskId, hours, minutes, date, description }). The POST should call Teamwork's POST /tasks/{taskId}/time_entries.json endpoint. Build a React time logger with: a recent projects dropdown that fetches tasks from the selected project, a description input, hours and minutes number inputs, a date picker (defaulting to today), and a Log Time button. Show today's total hours logged at the top.
```

## Troubleshooting

### API returns 401 Unauthorized despite entering the correct API key

Cause: The HTTP Basic auth encoding is incorrect, or the API key was copied with extra whitespace. Teamwork requires the key encoded as base64(apiKey + ':X') — the colon and placeholder password are required.

Solution: Double-check the API key in Teamwork Settings → API & Integrations and confirm there are no extra spaces. Verify the auth header is constructed as 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'). Some older Teamwork accounts use a different API endpoint structure — check the API docs for your plan.

```
// Verify auth header construction:
const apiKey = process.env.TEAMWORK_API_KEY || '';
const encoded = Buffer.from(`${apiKey}:X`).toString('base64');
console.log('Auth header:', `Basic ${encoded}`);
// Test manually: decode the base64 and verify it matches your API key
```

### Tasks array is empty even though projects have tasks in the Teamwork dashboard

Cause: The Teamwork API returns tasks under the key 'todo-items', not 'tasks'. If your code reads response.tasks, it gets undefined, which maps to an empty array.

Solution: Update the code to read response['todo-items'] instead of response.tasks. This is Teamwork's legacy naming convention from before REST API standardization and affects all task-related endpoints.

```
// Wrong:
const tasks = data.tasks || [];

// Correct:
const tasks = data['todo-items'] || [];
```

### Webhook events are not arriving at the API route

Cause: Teamwork webhooks require a publicly accessible URL to send POST requests to. Bolt's WebContainer preview URL cannot receive incoming connections from Teamwork's servers.

Solution: Deploy to Netlify or Bolt Cloud first to get a stable HTTPS URL. In Teamwork, go to Settings → Integrations → Webhooks and register your deployed URL. Teamwork supports webhooks for task creation, task completion, time logging, and project events.

### Time entry creation returns STATUS 'ERROR' with no clear message

Cause: The time entry body is missing a required field or has incorrect format. Common causes: date in wrong format (must be YYYYMMDD for some API versions, not YYYY-MM-DD), hours and minutes both zero, or invalid task ID.

Solution: Verify the date format required by your Teamwork API version. Also ensure hours + minutes is greater than zero (0 hours 0 minutes is invalid). Confirm the task ID is for an existing, non-deleted task using GET /tasks/{id}.json first.

```
// Teamwork API v1 date format for time entries is YYYYMMDD:
const formattedDate = date.replace(/-/g, ''); // '2025-01-15' -> '20250115'
// Use formattedDate in the time-entry body
```

## Frequently asked questions

### Does Bolt.new have a native Teamwork integration?

No — Bolt.new does not have a native Teamwork connector. The integration uses Next.js API routes with HTTP Basic authentication using your Teamwork API key. Teamwork's API requires no OAuth setup, making it one of the simpler integrations to build from scratch with Bolt's AI assistance.

### Can I test the Teamwork integration in Bolt's WebContainer preview?

Yes — all outbound API calls to Teamwork work in the WebContainer preview since they are standard HTTPS requests. You can fetch projects, tasks, and time entries, and test creating records, all without deploying. The only feature requiring deployment is Teamwork webhooks, which send incoming POST requests that the WebContainer cannot receive.

### Where do I find the Teamwork API key?

Log into Teamwork, click your profile avatar in the top right, select 'Edit My Details,' then scroll to the API section. Click 'Show API Token' or 'Generate New Token.' The API key is account-specific — each team member who needs API access generates their own key in their own profile settings.

### Why does Teamwork use 'todo-items' instead of 'tasks' in the API?

This is a legacy naming convention from Teamwork's early API design. The platform called tasks 'to-do items' in its original UI, and the API key name was never updated when the UI adopted the 'tasks' terminology. This is documented in Teamwork's API reference but catches nearly every developer building a Teamwork integration for the first time.

### Does the Teamwork API support real-time updates?

Teamwork does not have a WebSocket or Server-Sent Events API for real-time updates. Real-time notifications use webhooks — Teamwork sends POST requests to your server when events occur (task created, task completed, time logged). These webhooks require a deployed URL. For near-real-time dashboards without webhooks, implement polling from your API route every 30-60 seconds.

---

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