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 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
Generate Teamwork API Key and Configure Authentication
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.
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
1// lib/teamwork.ts2const 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};78const 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};1516interface TeamworkFetchOptions {17 method?: 'GET' | 'POST' | 'PUT' | 'DELETE';18 params?: Record<string, string | number | boolean>;19 body?: unknown;20}2122export 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}`);2829 if (options.params) {30 Object.entries(options.params).forEach(([key, value]) => {31 url.searchParams.set(key, String(value));32 });33 }3435 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 });4344 if (!response.ok) {45 const text = await response.text().catch(() => '');46 throw new Error(`Teamwork API ${response.status}: ${text || response.statusText}`);47 }4849 return response.json() as Promise<T>;50}5152// Convenience types for common Teamwork resources53export 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}6465export 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.
Build the Projects Overview API Route
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.
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
1// app/api/teamwork/projects/route.ts2import { NextResponse } from 'next/server';3import { teamworkFetch, TeamworkProject } from '@/lib/teamwork';45interface TeamworkProjectsResponse {6 projects: TeamworkProject[];7}89export async function GET() {10 try {11 const data = await teamworkFetch<TeamworkProjectsResponse>('projects.json', {12 params: { status: 'active', orderby: 'name', pageSize: 250 },13 });1415 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;2021 // Simple health heuristic22 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 }3132 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 });4546 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.
Fetch Tasks and Build the Task View
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.
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
1// app/api/teamwork/tasks/route.ts2import { NextResponse } from 'next/server';3import { teamworkFetch } from '@/lib/teamwork';45interface 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}2021export 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';2728 const endpoint = projectId29 ? `projects/${projectId}/tasks.json`30 : 'tasks.json';3132 const params: Record<string, string> = {33 filter,34 page,35 pageSize: '100',36 'sort': 'duedate',37 'sort-order': 'asc',38 };3940 if (responsiblePartyIds) params['responsible-party-ids'] = responsiblePartyIds;4142 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 }));5556 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.
Add Time Logging and Task Creation
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.
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
1// app/api/teamwork/time-entries/route.ts2import { NextResponse } from 'next/server';3import { teamworkFetch } from '@/lib/teamwork';45interface 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}1718interface TimeEntriesResponse {19 'time-entries': TimeEntry[];20}2122export 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];2627 try {28 const data = await teamworkFetch<TimeEntriesResponse>('time_entries.json', {29 params: {30 'fromdate': date,31 'todate': date,32 ...(personId ? { 'userId': personId } : {}),33 },34 });3536 const entries = data['time-entries'] || [];37 const totalMinutes = entries.reduce(38 (sum, e) => sum + (e.hours || 0) * 60 + (e.minutes || 0),39 040 );4142 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}5253export async function POST(request: Request) {54 const { taskId, hours, minutes, date, description, billable = true } = await request.json();5556 if (!taskId || (!hours && !minutes)) {57 return NextResponse.json({ error: 'taskId and hours or minutes are required' }, { status: 400 });58 }5960 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 };7374 if (result.STATUS !== 'OK') {75 throw new Error('Time entry creation failed');76 }7778 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.
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.
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.
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.
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 keyTasks 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.
1// Wrong:2const tasks = data.tasks || [];34// 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.
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 bodyBest 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
Asana offers a similar REST API with OAuth authentication and is popular with marketing and product teams, though its API setup is more complex than Teamwork's simple HTTP Basic auth.
ClickUp's API provides the same project and task management capabilities with API token authentication and a more modern REST design than Teamwork's legacy JSON suffix URLs.
Jira is better for software development workflows with issue tracking, sprint management, and developer tool integrations, while Teamwork focuses on client-services project management.
Basecamp offers a simpler project management structure with HTTP Basic auth API similar to Teamwork, and is preferred by teams that want minimal complexity over feature depth.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation