Integrating Teamwork with Lovable requires Edge Functions to proxy the Teamwork REST API. Store your Teamwork API token in Cloud Secrets, create an Edge Function that reads projects, tasks, milestones, and time entries, and build custom client-facing dashboards in Lovable's React frontend. Teamwork's API is designed around agency-client workflows — projects link to companies, time entries attach to budgets, and milestones provide billing-checkpoint visibility that sets it apart from general-purpose task managers.
Why integrate Teamwork with Lovable?
Teamwork is built specifically for client-service businesses — agencies, consultancies, marketing firms, and professional services companies. Unlike general-purpose project management tools, Teamwork embeds client billing concepts directly into its data model: projects link to client companies, time entries carry billing codes and rates, milestones can serve as billing checkpoints, and the platform generates profitability reports comparing estimated vs actual hours across client engagements.
Building a custom Lovable app on Teamwork data enables compelling client-facing scenarios: branded client portals that show project progress, milestone completion, and time logged without exposing your internal Teamwork workspace, automated billing summaries that aggregate time entries by billing code and present them as invoice-ready reports, resource utilization dashboards that show team capacity across multiple active client projects simultaneously, and client intake forms that create new projects in Teamwork with the correct company, category, and billing settings pre-configured.
Teamwork's API v3 is a REST API hosted at your Teamwork site URL (yourcompany.teamwork.com/projects/api/v3/) and uses Basic Authentication where the API token serves as the username and the letter 'X' is the password. This unusual auth pattern is important to implement correctly in your Edge Function. The API returns JSON with a specific envelope structure — most responses wrap data in a key matching the resource type (e.g., 'projects' for a projects list, 'task' for a single task). This tutorial covers all of these patterns with working TypeScript code.
Integration method
Teamwork has no native Lovable connector. All Teamwork API calls use the REST API v3 at your-site.teamwork.com/projects/api/v3, proxied through Supabase Edge Functions. API tokens are stored in Cloud Secrets and sent as Basic Auth headers (token as username, X as password). Edge Functions fetch projects, tasks, milestones, time entries, and companies, returning client-billing-focused data to your Lovable frontend.
Prerequisites
- A Lovable project with Cloud enabled
- A Teamwork account with at least one project and API access enabled
- A Teamwork API token — generate at your-site.teamwork.com → your profile icon → Edit Profile → API Keys → Generate token
- Your Teamwork site URL (e.g., yourcompany.teamwork.com) — this is the base for all API requests
- Company IDs and project IDs for the client projects you want to display — found in Teamwork URLs or via the people/companies API
Step-by-step guide
Generate a Teamwork API token and store credentials in Cloud Secrets
Generate a Teamwork API token and store credentials in Cloud Secrets
Teamwork API tokens are generated from your user profile settings within your Teamwork site. Log into your Teamwork instance at yourcompany.teamwork.com and click your avatar in the top-right corner. Select 'Edit Profile', then navigate to the 'API Keys' section. Click 'Generate API Key' (or 'Create API Key' depending on your plan), optionally name the key 'Lovable Integration', and copy the token immediately. Teamwork's API uses an unusual Basic Authentication scheme: the token acts as the username and the literal letter 'X' is the password. When encoded to Base64 for the Authorization header, this becomes: Base64(token:X). Your Edge Function will construct this header automatically — you just need to store the raw token. In your Lovable project, open the Cloud tab via the '+' icon, navigate to Secrets, and add two secrets: TEAMWORK_API_TOKEN with your token value, and TEAMWORK_SITE_URL with your full site URL including protocol (e.g., https://yourcompany.teamwork.com). The site URL is needed because the API base URL is unique to your Teamwork instance — unlike SaaS APIs with a single base URL, every Teamwork customer has their own subdomain.
Pro tip: Teamwork also supports API v1 at your-site.teamwork.com/projects/api/v1/ with slightly different endpoints. This tutorial uses v3 which is the current recommended version. Check your API documentation at your-site.teamwork.com/launchpad/v3/reference for the exact v3 endpoint paths.
Expected result: TEAMWORK_API_TOKEN and TEAMWORK_SITE_URL are stored in Cloud Secrets. Both appear with masked values in the Secrets list.
Create the Teamwork proxy Edge Function
Create the Teamwork proxy Edge Function
Teamwork's API uses Basic Authentication, which requires constructing an Authorization header with a Base64-encoded string of 'token:X'. In Deno, you can use the btoa() function available globally to perform the Base64 encoding. The Edge Function reads both secrets, constructs the auth header, and uses an action-based routing pattern to handle the most common Teamwork operations. Teamwork's API v3 returns responses with a specific structure: collection responses wrap data in arrays under the resource name key (e.g., { projects: [...] }), while single-resource responses use singular keys (e.g., { project: {...} }). The function handles this consistently and forwards the full response to your frontend for flexible data access. Note that Teamwork's API is hosted at your site URL, not a central API URL — the TEAMWORK_SITE_URL secret is used to construct all endpoint URLs dynamically.
Create a Supabase Edge Function at supabase/functions/teamwork-proxy/index.ts. Read TEAMWORK_API_TOKEN and TEAMWORK_SITE_URL from Deno.env.get(). Create a Basic Auth header using btoa(token + ':X'). Handle CORS. Accept POST with action and params. Implement: 'list_projects' (GET /projects/api/v3/projects.json with optional params.companyId filter), 'get_project' (GET /projects/api/v3/projects/{params.projectId}.json), 'list_tasks' (GET /projects/api/v3/tasks.json with params.projectId), 'get_milestones' (GET /projects/api/v3/milestones.json with params.projectId), 'list_time_entries' (GET /projects/api/v3/time.json with params.projectId and optional date range params), 'create_time_entry' (POST /projects/api/v3/tasks/{params.taskId}/time.json with hours, date, description, isBillable from params), 'list_companies' (GET /projects/api/v3/companies.json). All GET requests append .json suffix. Authorization header: 'Basic ' + base64 encoded credentials.
Paste this in Lovable chat
1// supabase/functions/teamwork-proxy/index.ts2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";34const corsHeaders = {5 "Access-Control-Allow-Origin": "*",6 "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",7};89serve(async (req) => {10 if (req.method === "OPTIONS") {11 return new Response("ok", { headers: corsHeaders });12 }1314 const apiToken = Deno.env.get("TEAMWORK_API_TOKEN");15 const siteUrl = Deno.env.get("TEAMWORK_SITE_URL");16 if (!apiToken || !siteUrl) {17 return new Response(JSON.stringify({ error: "Teamwork credentials not configured" }), {18 status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" },19 });20 }2122 const base64Auth = btoa(`${apiToken}:X`);23 const authHeaders = {24 Authorization: `Basic ${base64Auth}`,25 "Content-Type": "application/json",26 };27 const base = `${siteUrl}/projects/api/v3`;2829 const { action, params = {} } = await req.json();30 let resp;3132 switch (action) {33 case "list_projects": {34 const q = new URLSearchParams();35 if (params.companyId) q.set("companyId", params.companyId);36 if (params.status) q.set("status", params.status);37 resp = await fetch(`${base}/projects.json?${q}`, { headers: authHeaders });38 break;39 }40 case "get_project":41 resp = await fetch(`${base}/projects/${params.projectId}.json`, { headers: authHeaders });42 break;43 case "list_tasks": {44 const q = new URLSearchParams({ projectId: params.projectId });45 if (params.assignedUserId) q.set("assignedUserId", params.assignedUserId);46 resp = await fetch(`${base}/tasks.json?${q}`, { headers: authHeaders });47 break;48 }49 case "get_milestones": {50 const q = new URLSearchParams({ projectId: params.projectId });51 resp = await fetch(`${base}/milestones.json?${q}`, { headers: authHeaders });52 break;53 }54 case "list_time_entries": {55 const q = new URLSearchParams({ projectId: params.projectId });56 if (params.fromDate) q.set("fromDate", params.fromDate);57 if (params.toDate) q.set("toDate", params.toDate);58 resp = await fetch(`${base}/time.json?${q}`, { headers: authHeaders });59 break;60 }61 case "create_time_entry":62 resp = await fetch(`${base}/tasks/${params.taskId}/time.json`, {63 method: "POST",64 headers: authHeaders,65 body: JSON.stringify({66 timelog: {67 hours: String(Math.floor(params.hours)),68 minutes: String(Math.round((params.hours % 1) * 60)),69 date: params.date,70 description: params.description || "",71 isBillable: params.isBillable ?? true,72 }73 }),74 });75 break;76 case "list_companies":77 resp = await fetch(`${base}/companies.json`, { headers: authHeaders });78 break;79 default:80 return new Response(JSON.stringify({ error: `Unknown action: ${action}` }), {81 status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" },82 });83 }8485 const data = await resp.json();86 return new Response(JSON.stringify(data), {87 status: resp.status, headers: { ...corsHeaders, "Content-Type": "application/json" },88 });89});Pro tip: Teamwork time entries store hours and minutes as separate integer fields, not as a decimal. When accepting decimal hours from users (e.g., 1.5 hours), split them: hours = Math.floor(1.5) = 1, minutes = Math.round(0.5 * 60) = 30.
Expected result: The Edge Function deploys. Calling action 'list_projects' returns your Teamwork projects. Calling 'list_companies' returns client companies with their IDs.
Build a client-facing project dashboard
Build a client-facing project dashboard
The most common use case for a Teamwork-Lovable integration is a client portal: a branded page showing the client their project's status without requiring Teamwork credentials or exposing your internal workspace. The key is filtering Teamwork data by company ID — every Teamwork project links to a company (the client), so you can fetch all projects for a given company and display only their data. The following prompt generates a project overview component that shows project health, milestone timeline, and time tracking summary — the three most valuable pieces of information for client billing conversations. It uses the Teamwork project's budgetHours field alongside logged time to calculate budget utilization, and renders milestones in chronological order with visual indicators for completed, upcoming, and overdue states.
Create a TeamworkClientPortal component that accepts a companyId prop. On mount, call my teamwork-proxy Edge Function with action 'list_projects' and the companyId to get all client projects. For each project, also fetch milestones (action 'get_milestones') and time entries for the current month (action 'list_time_entries' with fromDate first of month, toDate today). Render a project card for each project with: project name, health status badge (using the project's status field), a progress bar showing hours logged vs budget hours, a milestone list showing next 3 upcoming milestones sorted by due date, and total hours billed this month. Add a toggle to show all milestones vs upcoming only.
Paste this in Lovable chat
Pro tip: Teamwork project health can be read from the response's status field. Map the values to display labels: 'active' → 'On Track', 'late' → 'At Risk', 'archived' → 'Completed'. The project's budgetHours field holds the total estimated hours budget.
Expected result: The portal renders project cards for the specified company with milestone timelines, time summaries, and health indicators. Milestones show dates and completion checkmarks pulled live from Teamwork.
Add time logging and invoice summary generation
Add time logging and invoice summary generation
The real power of a Teamwork integration for agency teams is closing the loop between tracked time and client billing. After building the read-only portal, you can add a time-logging interface for internal use and a billing summary feature that formats logged time entries as an invoice-ready report. Teamwork stores time entries with billable flags, rates, and descriptions — enough information to generate a formatted summary of billable hours by task category. For teams that want to go further, RapidDev's team can help build a complete invoice generation flow that takes Teamwork time entries and uses the Stripe or QuickBooks integration to generate actual invoices. The Edge Function handles fetching and aggregating time data, and a second Lovable page generates a formatted billing report that can be exported as a PDF or emailed to clients.
Add a 'Billing Summary' tab to the TeamworkClientPortal component. When selected, fetch all time entries for the selected project for the current billing period (this month). Group entries by task list name (the parent tasklist of each task). Display a table: Task Category, Total Billable Hours, Total Non-Billable Hours, Description summaries. At the bottom show grand total billable hours and estimated invoice amount using a rate field (add a rate input defaulting to $150/hr). Add an 'Export as CSV' button that downloads the billing summary. Also add a 'Log Time' button that opens a modal calling create_time_entry for team members to log new entries.
Paste this in Lovable chat
Pro tip: When fetching time entries for billing summaries, add include=tasks to your request parameters to get task names with each time entry — this avoids needing separate requests to look up task names for each entry.
Expected result: The Billing Summary tab shows time grouped by task category with billable/non-billable breakdowns and a total amount. The Export CSV button downloads a properly formatted file. The Log Time modal creates entries in Teamwork.
Common use cases
Client-facing project portal with milestone tracking
An agency runs multiple concurrent client projects in Teamwork and wants to give each client a branded portal showing their project milestones, overall progress, and time logged this billing period. The Edge Function fetches the company's projects using the company ID filter, then for each project retrieves milestones and time entry totals. The frontend shows a milestone timeline with completion status and a time summary comparing hours used vs budget.
Build a client portal at /client-portal that accepts a companyId URL parameter. Fetch all projects linked to that company from my Teamwork Edge Function. For each project, show: project name, health indicator (on track / at risk / off track), milestone list with due dates and completion status (green checkmark for complete, clock for upcoming), total hours logged this month vs budget hours, and percent complete. Add a project selector if there are multiple active projects. Make it look professional and branded with no Teamwork branding visible.
Copy this prompt to try it in Lovable
Time logging interface for team members
A consulting firm wants a simplified time logging screen inside their Lovable operations tool where team members can log time against Teamwork tasks without switching apps. The Edge Function fetches the user's active tasks, and the frontend provides a quick-log interface showing task names, project names, and a time entry form with hours, date, description, and billable flag. Submitted entries are created directly in Teamwork via the time entries endpoint.
Create a time logging page at /log-time. Fetch my active Teamwork tasks assigned to user ID 12345 sorted by project name. Display them as a list with task name and project name. Next to each task show an 'Add Time' button that expands an inline form: Hours (number input with 0.5 increments), Date (default today), Description, and Billable toggle. On submit, call my Teamwork Edge Function to create a time entry on that task. Show a running total of hours logged today at the top of the page. After logging, update the total without a full page refresh.
Copy this prompt to try it in Lovable
Profitability dashboard across client projects
A project manager needs a weekly view of project profitability — budgeted hours vs actual hours logged, broken down by project and team member. The Edge Function fetches time entries with billing rates for each active project, computes utilization percentage, and returns a summary. The Lovable frontend renders a sortable table with profit/loss indicators, budget burn rate, and estimated completion based on current velocity.
Build a profitability dashboard at /profitability that fetches all active projects from Teamwork with their budget hours. For each project, fetch time entries for the current month grouped by task list. Calculate: budgeted hours, actual hours logged, hours remaining, budget burn percentage, and estimated overage if current pace continues. Display projects in a table sortable by budget burn percentage. Color-code rows: green under 80% burn, yellow 80-100%, red over budget. Show totals row at the bottom. Add a date range filter defaulting to current month.
Copy this prompt to try it in Lovable
Troubleshooting
API returns 401 Unauthorized even though the API token looks correct
Cause: Teamwork requires Basic Auth where the token is the username and 'X' is the password. If the Authorization header is formatted differently (e.g., using Bearer instead of Basic, or omitting the ':X' suffix in the Base64 encode), all requests will be rejected.
Solution: Verify the auth header construction in your Edge Function. The Base64 value must encode 'YOUR_TOKEN:X' (token, colon, capital X). Use btoa(apiToken + ':X') in Deno. The header value must be 'Basic ' followed by the Base64 string — not 'Bearer'. Check Cloud → Logs to see the exact error response body from Teamwork for more detail.
1const base64Auth = btoa(`${apiToken}:X`);2const authHeaders = { Authorization: `Basic ${base64Auth}` };Time entry creation returns success but hours show as 0 in Teamwork
Cause: Teamwork's time entry API requires hours and minutes as separate string fields — passing a decimal number (like 1.5) to the hours field results in the value being truncated to its integer part.
Solution: Split decimal hours into integer hours and minutes before submitting. Convert 1.5 hours to hours: '1' and minutes: '30'. Both must be strings, not numbers. Validate that minutes are between 0 and 59 — values of 60 or above cause unpredictable behavior.
1const totalHours = 1.5;2const hours = String(Math.floor(totalHours));3const minutes = String(Math.round((totalHours % 1) * 60));4// Result: hours='1', minutes='30'The API URL returns 404 for all requests even with correct credentials
Cause: The TEAMWORK_SITE_URL secret is missing the correct path structure. Teamwork API v3 lives at /projects/api/v3 — if the site URL includes a trailing slash or the path structure is wrong, all requests will 404.
Solution: Verify TEAMWORK_SITE_URL is set to your base site URL without a trailing slash (e.g., https://yourcompany.teamwork.com). The Edge Function should append /projects/api/v3 to construct the base API path. Double check by testing the URL format directly: https://yourcompany.teamwork.com/projects/api/v3/projects.json should return project data when called with valid auth.
1// Correct URL construction2const siteUrl = Deno.env.get("TEAMWORK_SITE_URL"); // https://yourcompany.teamwork.com3const base = `${siteUrl}/projects/api/v3`;4// Result: https://yourcompany.teamwork.com/projects/api/v3Time entries appear in Teamwork but with no project association
Cause: Time entries can be created on tasks or directly on projects. When creating on a task, the project association comes from the task's project membership. If the task ID is wrong or belongs to a different project, the time entry may not appear where expected.
Solution: Verify the task ID used in the create_time_entry action belongs to the correct project. Fetch the task first and confirm its projectId matches your expected project. Alternatively, use the project-level time entry endpoint POST /projects/{projectId}/time.json which ensures the entry is associated with the correct project regardless of task assignment.
Best practices
- Store your Teamwork site URL as a Cloud Secret (TEAMWORK_SITE_URL) rather than hardcoding it, making it easy to switch between a staging and production Teamwork instance.
- Always request only the projects filtered by companyId for client portal pages — never return all projects to a client-facing view, as this could expose internal or other clients' project names.
- Cache company and project metadata in Supabase with a 15-minute TTL to reduce API calls — company and project structures change infrequently but time entries change constantly.
- Use Teamwork's date range parameters (fromDate, toDate) when fetching time entries to avoid loading all historical time data on every request — filter to the current billing period.
- Validate billable hours before submitting time entries: enforce a maximum of 24 hours per day per person, and require a description for billable entries to maintain audit-quality records.
- For milestone-based billing, use Teamwork milestone completion webhooks to trigger invoice creation automatically rather than requiring manual billing actions from project managers.
- Separate the API token permission level — use a read-only token for client portal pages and a full-access token for internal team time-logging features, stored as different Cloud Secrets.
Alternatives
Asana offers broader use cases beyond client-service work, with stronger custom workflow support, but lacks Teamwork's built-in billing, expense, and profitability features.
Basecamp is better for teams wanting simplicity and opinionated project structure, but lacks Teamwork's time tracking, billing codes, and profitability analytics.
Harvest specializes in time tracking and invoicing as standalone tools rather than bundled with project management, making it a good complement to simpler PM tools.
Frequently asked questions
Does Teamwork have a native connector in Lovable?
No, Teamwork is not one of Lovable's 17 shared connectors. Integration uses Edge Functions that proxy the Teamwork REST API v3. The API token is stored in Cloud Secrets and all requests use Basic Auth — your token is never exposed to the frontend.
Can I give clients access to the portal without them seeing my other client projects?
Yes — filter all API requests by the client's company ID from Teamwork. Store a mapping of client user identities (from Supabase Auth) to their Teamwork company ID in your database. When a client logs into the portal, look up their company ID and pass it as a parameter to the Edge Function, which filters all project and time entry queries to that company only.
What Teamwork plan do I need for API access?
The Teamwork REST API is available on Deliver plan and above ($13.99/user/month as of 2026). The Free plan and Starter plan do not include API access. Check your current plan at your-site.teamwork.com → Account Settings → Subscription if you're unsure whether your account has API access enabled.
Can I track time in Lovable without installing the Teamwork desktop app?
Yes — the time entry creation endpoint (POST /tasks/{taskId}/time.json) lets you log time directly from any interface without the desktop app or browser extension. Your Lovable app becomes an alternative time-logging interface. The entries sync to Teamwork instantly and appear in all Teamwork reports, timesheets, and billing summaries alongside entries made through the native app.
How do I handle team members who need to log time without accessing all project data?
Create separate Edge Functions with different permission scopes: a time-logging function that only creates entries (write access) and a reporting function that reads project and billing data. For the logging function, only expose the task list assigned to the logged-in user by filtering with assignedUserId. Store the Teamwork user ID mapping in your Supabase user profiles table to match Lovable auth users to Teamwork person IDs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation