Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Teamwork

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.

What you'll learn

  • How to generate a Teamwork API key and authenticate using HTTP Basic auth
  • How to fetch projects, task lists, and tasks via Next.js API routes
  • How to build a project overview dashboard with task counts and progress
  • How to create tasks and log time entries programmatically
  • How to filter active tasks by assignee, due date, and priority
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate18 min read20 minutesProductivityApril 2026RapidDev Engineering Team
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.

Integration method

Bolt Chat + API Route

Bolt generates Next.js API routes that call Teamwork's REST API using HTTP Basic authentication — the API key is used as the username with any string as the password. This is the simplest API authentication pattern available: no OAuth flow, no token refresh, just encode the key in the Authorization header. All outbound calls work in Bolt's WebContainer during development. Webhooks for project events require a deployed URL.

Prerequisites

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

Bolt.new Prompt

Add TEAMWORK_API_KEY and TEAMWORK_SUBDOMAIN to the .env file. Create a lib/teamwork.ts helper that exports a teamworkFetch function. It takes an endpoint string and optional options, constructs the URL as https://{TEAMWORK_SUBDOMAIN}.teamwork.com/{endpoint}, adds an Authorization header using HTTP Basic auth with the API key as username and 'X' as password (base64 encoded), and returns the parsed JSON response. Export constants for common endpoints.

Paste this in Bolt.new chat

lib/teamwork.ts
1// lib/teamwork.ts
2const getTeamworkBaseUrl = () => {
3 const subdomain = process.env.TEAMWORK_SUBDOMAIN;
4 if (!subdomain) throw new Error('TEAMWORK_SUBDOMAIN is not configured');
5 return `https://${subdomain}.teamwork.com`;
6};
7
8const getAuthHeader = () => {
9 const apiKey = process.env.TEAMWORK_API_KEY;
10 if (!apiKey) throw new Error('TEAMWORK_API_KEY is not configured');
11 // HTTP Basic auth: base64(apiKey:X)
12 const encoded = Buffer.from(`${apiKey}:X`).toString('base64');
13 return `Basic ${encoded}`;
14};
15
16interface TeamworkFetchOptions {
17 method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
18 params?: Record<string, string | number | boolean>;
19 body?: unknown;
20}
21
22export async function teamworkFetch<T = unknown>(
23 endpoint: string,
24 options: TeamworkFetchOptions = {}
25): Promise<T> {
26 const baseUrl = getTeamworkBaseUrl();
27 const url = new URL(`${baseUrl}/${endpoint}`);
28
29 if (options.params) {
30 Object.entries(options.params).forEach(([key, value]) => {
31 url.searchParams.set(key, String(value));
32 });
33 }
34
35 const response = await fetch(url.toString(), {
36 method: options.method || 'GET',
37 headers: {
38 Authorization: getAuthHeader(),
39 'Content-Type': 'application/json',
40 },
41 body: options.body ? JSON.stringify(options.body) : undefined,
42 });
43
44 if (!response.ok) {
45 const text = await response.text().catch(() => '');
46 throw new Error(`Teamwork API ${response.status}: ${text || response.statusText}`);
47 }
48
49 return response.json() as Promise<T>;
50}
51
52// Convenience types for common Teamwork resources
53export interface TeamworkProject {
54 id: string;
55 name: string;
56 description: string;
57 status: string;
58 'start-date': string;
59 'end-date': string;
60 'completed-count': number;
61 'uncompleted-count': number;
62 'is-project-admin': boolean;
63}
64
65export interface TeamworkTask {
66 id: string;
67 content: string;
68 status: string;
69 priority: string;
70 'due-date': string;
71 completed: boolean;
72 'project-id': string;
73 'project-name': string;
74 'responsible-party-names': string;
75 'estimated-minutes': number;
76}

Pro tip: Teamwork API responses nest resources inside a key matching the resource name. A project list returns { projects: [...] }, a task list returns { todo-items: [...] }. Always extract the array from the correct key before processing — the key name is sometimes hyphenated (todo-items, not tasks).

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.

Bolt.new Prompt

Create a Next.js API route at app/api/teamwork/projects/route.ts that fetches active projects from Teamwork using teamworkFetch. Extract the projects array from the response and map each project to { id, name, description, status, completedTasks, openTasks, startDate, endDate, healthStatus }. Calculate healthStatus as 'On Track' (openTasks > 0 but no overdue pattern), 'At Risk' or 'On Time' based on completion ratio. Return { projects, total } sorted by name.

Paste this in Bolt.new chat

app/api/teamwork/projects/route.ts
1// app/api/teamwork/projects/route.ts
2import { NextResponse } from 'next/server';
3import { teamworkFetch, TeamworkProject } from '@/lib/teamwork';
4
5interface TeamworkProjectsResponse {
6 projects: TeamworkProject[];
7}
8
9export async function GET() {
10 try {
11 const data = await teamworkFetch<TeamworkProjectsResponse>('projects.json', {
12 params: { status: 'active', orderby: 'name', pageSize: 250 },
13 });
14
15 const projects = (data.projects || []).map((p) => {
16 const completed = p['completed-count'] || 0;
17 const open = p['uncompleted-count'] || 0;
18 const total = completed + open;
19 const completionPct = total > 0 ? Math.round((completed / total) * 100) : 0;
20
21 // Simple health heuristic
22 let healthStatus = 'On Track';
23 if (completionPct < 25 && total > 5) healthStatus = 'Early Stage';
24 if (p['end-date']) {
25 const daysLeft = Math.ceil(
26 (new Date(p['end-date']).getTime() - Date.now()) / (1000 * 60 * 60 * 24)
27 );
28 if (daysLeft < 0 && open > 0) healthStatus = 'Overdue';
29 else if (daysLeft < 7 && open > 0) healthStatus = 'At Risk';
30 }
31
32 return {
33 id: p.id,
34 name: p.name,
35 description: p.description || '',
36 status: p.status,
37 completedTasks: completed,
38 openTasks: open,
39 completionPct,
40 startDate: p['start-date'] || null,
41 endDate: p['end-date'] || null,
42 healthStatus,
43 };
44 });
45
46 return NextResponse.json({ projects, total: projects.length });
47 } catch (err) {
48 const message = err instanceof Error ? err.message : 'Failed to fetch projects';
49 return NextResponse.json({ error: message }, { status: 500 });
50 }
51}

Pro tip: Teamwork's kebab-case field names (completed-count, start-date) require bracket notation in JavaScript. TypeScript will also flag these as invalid identifier syntax in dot notation — define interface properties with quoted key names: { 'completed-count': number } or rename them in your map function.

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.

Bolt.new Prompt

Create a Next.js API route at app/api/teamwork/tasks/route.ts that fetches tasks. Accept query params: projectId (optional), filter (all/active/overdue/upcoming), responsiblePartyIds (optional), page (default 1). Call Teamwork's tasks endpoint and return { tasks: [...], total }. Each task should have id, content, status, priority, dueDate, completed, projectName, assignees. Also create app/api/teamwork/tasks/[id]/complete/route.ts that handles POST to toggle task completion via Teamwork API.

Paste this in Bolt.new chat

app/api/teamwork/tasks/route.ts
1// app/api/teamwork/tasks/route.ts
2import { NextResponse } from 'next/server';
3import { teamworkFetch } from '@/lib/teamwork';
4
5interface TeamworkTasksResponse {
6 'todo-items': Array<{
7 id: string;
8 content: string;
9 status: string;
10 priority: string;
11 'due-date': string;
12 completed: boolean;
13 'project-id': string;
14 'project-name': string;
15 'responsible-party-names': string;
16 'estimated-minutes': number;
17 description: string;
18 }>;
19}
20
21export async function GET(request: Request) {
22 const { searchParams } = new URL(request.url);
23 const projectId = searchParams.get('projectId');
24 const filter = searchParams.get('filter') || 'active';
25 const responsiblePartyIds = searchParams.get('responsiblePartyIds');
26 const page = searchParams.get('page') || '1';
27
28 const endpoint = projectId
29 ? `projects/${projectId}/tasks.json`
30 : 'tasks.json';
31
32 const params: Record<string, string> = {
33 filter,
34 page,
35 pageSize: '100',
36 'sort': 'duedate',
37 'sort-order': 'asc',
38 };
39
40 if (responsiblePartyIds) params['responsible-party-ids'] = responsiblePartyIds;
41
42 try {
43 const data = await teamworkFetch<TeamworkTasksResponse>(endpoint, { params });
44 const tasks = (data['todo-items'] || []).map((t) => ({
45 id: t.id,
46 content: t.content,
47 priority: t.priority || 'none',
48 dueDate: t['due-date'] || null,
49 completed: t.completed,
50 projectId: t['project-id'],
51 projectName: t['project-name'],
52 assignees: t['responsible-party-names'] || '',
53 estimatedMinutes: t['estimated-minutes'] || 0,
54 }));
55
56 return NextResponse.json({ tasks, total: tasks.length });
57 } catch (err) {
58 const message = err instanceof Error ? err.message : 'Failed to fetch tasks';
59 return NextResponse.json({ error: message }, { status: 500 });
60 }
61}

Pro tip: Teamwork's task response key is 'todo-items', not 'tasks'. This is one of the most common sources of confusion when first integrating with the API — if your tasks array is empty, check that you are reading data['todo-items'] and not data['tasks'].

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.

Bolt.new Prompt

Create two API routes. First: app/api/teamwork/tasks/create/route.ts that accepts POST with { projectId, tasklistId, content, dueDate, priority, responsiblePartyId }. Call Teamwork to create the task and return the new task ID. Second: app/api/teamwork/time-entries/route.ts with GET to fetch today's time entries for a user (personId query param) and POST to create a time entry with { taskId, hours, minutes, date, description, billable }. Use teamworkFetch from lib/teamwork.ts for both.

Paste this in Bolt.new chat

app/api/teamwork/time-entries/route.ts
1// app/api/teamwork/time-entries/route.ts
2import { NextResponse } from 'next/server';
3import { teamworkFetch } from '@/lib/teamwork';
4
5interface TimeEntry {
6 id: string;
7 description: string;
8 hours: number;
9 minutes: number;
10 date: string;
11 'person-first-name': string;
12 'person-last-name': string;
13 'project-name': string;
14 'todo-item-name': string;
15 billable: boolean;
16}
17
18interface TimeEntriesResponse {
19 'time-entries': TimeEntry[];
20}
21
22export async function GET(request: Request) {
23 const { searchParams } = new URL(request.url);
24 const personId = searchParams.get('personId');
25 const date = searchParams.get('date') || new Date().toISOString().split('T')[0];
26
27 try {
28 const data = await teamworkFetch<TimeEntriesResponse>('time_entries.json', {
29 params: {
30 'fromdate': date,
31 'todate': date,
32 ...(personId ? { 'userId': personId } : {}),
33 },
34 });
35
36 const entries = data['time-entries'] || [];
37 const totalMinutes = entries.reduce(
38 (sum, e) => sum + (e.hours || 0) * 60 + (e.minutes || 0),
39 0
40 );
41
42 return NextResponse.json({
43 entries,
44 totalHours: Math.floor(totalMinutes / 60),
45 totalMinutes: totalMinutes % 60,
46 });
47 } catch (err) {
48 const message = err instanceof Error ? err.message : 'Failed to fetch time entries';
49 return NextResponse.json({ error: message }, { status: 500 });
50 }
51}
52
53export async function POST(request: Request) {
54 const { taskId, hours, minutes, date, description, billable = true } = await request.json();
55
56 if (!taskId || (!hours && !minutes)) {
57 return NextResponse.json({ error: 'taskId and hours or minutes are required' }, { status: 400 });
58 }
59
60 try {
61 const result = await teamworkFetch(`tasks/${taskId}/time_entries.json`, {
62 method: 'POST',
63 body: {
64 'time-entry': {
65 description: description || '',
66 hours: hours || 0,
67 minutes: minutes || 0,
68 date: date || new Date().toISOString().split('T')[0],
69 billable,
70 },
71 },
72 }) as { STATUS?: string; timeLogId?: string };
73
74 if (result.STATUS !== 'OK') {
75 throw new Error('Time entry creation failed');
76 }
77
78 return NextResponse.json({ success: true, timeLogId: result.timeLogId });
79 } catch (err) {
80 const message = err instanceof Error ? err.message : 'Failed to log time';
81 return NextResponse.json({ error: message }, { status: 500 });
82 }
83}

Pro tip: Teamwork time entries use integer hours and minutes separately — there is no decimal hours field. Log 1.5 hours as { hours: 1, minutes: 30 }, not { hours: 1.5 }. The minimum billable entry is 0 hours and 1 minute.

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.

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

Bolt.new Prompt

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.

Copy this prompt to try it in Bolt.new

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.

Bolt.new Prompt

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.

Copy this prompt to try it in Bolt.new

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.

Bolt.new Prompt

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.

Copy this prompt to try it in Bolt.new

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.

typescript
1// Verify auth header construction:
2const apiKey = process.env.TEAMWORK_API_KEY || '';
3const encoded = Buffer.from(`${apiKey}:X`).toString('base64');
4console.log('Auth header:', `Basic ${encoded}`);
5// 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.

typescript
1// Wrong:
2const tasks = data.tasks || [];
3
4// Correct:
5const 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.

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

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.

Alternatives

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.