# How to Integrate Replit with Teamwork

- Tool: Replit
- Difficulty: Intermediate
- Time required: 20 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with Teamwork, store your Teamwork API key in Replit Secrets (lock icon 🔒) and call the Teamwork REST API from your server-side Node.js or Python code. Teamwork combines project management with built-in time tracking and client billing features. From Replit you can create projects, tasks, milestones, log time entries, and receive webhook events when work items are updated. Use Autoscale deployment for webhook event receivers.

## Project Management and Time Tracking Automation from Replit with Teamwork

Teamwork is purpose-built for agencies and service businesses that need to track both the work being done and the time spent doing it. Unlike general-purpose project management tools, Teamwork includes client-facing features: client portals, invoicing, budget tracking, and profitability reports. This makes it popular for digital agencies, consulting firms, and managed service providers who bill clients by the hour or project. The Teamwork API exposes all of this functionality programmatically — from Replit you can automate the parts of project management that are repetitive and error-prone when done manually.

The most valuable integration patterns are project scaffolding (automatically creating project structures, task lists, and initial tasks when a new client is onboarded), time logging (capturing time entries from external tools like code editors or support tickets and logging them to the right Teamwork project automatically), and reporting (pulling time data, task completion rates, and billing summaries for custom dashboards or automated reports to clients).

Teamwork's API uses HTTP Basic Auth — your API key is the username and the password can be any string (many developers use 'x' or 'password'). This makes it straightforward to integrate: no OAuth flow, no token expiry, no refresh logic. The API has two versions: v1 (older, more complete feature coverage) and v3 (newer, cleaner design). This guide uses v3 where available and falls back to v1 for features not yet in v3.

## Before you start

- A Replit account with a Node.js or Python Repl ready
- A Teamwork account — free trial available at teamwork.com
- Your Teamwork API key from your user profile in Teamwork
- Your Teamwork site URL (the subdomain you use to access Teamwork, e.g., yourcompany.teamwork.com)

## Step-by-step guide

### 1. Get Your Teamwork API Key and Store It in Replit Secrets

Teamwork API keys are user-specific — each user has their own API key that provides access to all projects visible to that user. For API integrations, best practice is to create a dedicated Teamwork user account (e.g., api@yourcompany.com) with the appropriate role and use that user's API key. This keeps API activity clearly attributed and means rotating the key does not affect a real user's sessions.

To get your API key: log into Teamwork → click your avatar in the top right → Edit Profile → scroll down to 'API Token' and click 'Show your token'. Copy the token.

You will also need your Teamwork site URL. If you access Teamwork at https://yourcompany.teamwork.com, then yourcompany.teamwork.com is your site URL.

Open your Replit project and click the lock icon (🔒) in the left sidebar. Add:

TEAMWORK_API_KEY: your Teamwork API token.

TEAMWORK_SITE: your Teamwork site URL without https:// (e.g., yourcompany.teamwork.com).

Teamwork API uses HTTP Basic Auth where the API key is the username and any string (conventionally 'x') is the password. Never include the API key in source code — Replit's Secret Scanner will flag it.

```
// check-teamwork-secrets.js
const required = ['TEAMWORK_API_KEY', 'TEAMWORK_SITE'];
for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing: ${key}. Add it in Replit Secrets (lock icon 🔒).`);
  }
}
const site = process.env.TEAMWORK_SITE;
if (site.startsWith('http')) {
  console.warn('Warning: TEAMWORK_SITE should be the subdomain only, e.g., yourcompany.teamwork.com — not https://...');
}
console.log('Teamwork secrets configured.');
console.log('Site:', process.env.TEAMWORK_SITE);
console.log('API Key prefix:', process.env.TEAMWORK_API_KEY.slice(0, 8) + '...');
```

**Expected result:** Both secrets appear in Replit Secrets. The verification script prints the site URL and API key prefix without errors.

### 2. Create Projects, Task Lists, and Tasks with Node.js

Teamwork's API base URL is https://{your-site}/projects/api/v3 for v3 endpoints and https://{your-site} for v1 endpoints. Authentication uses HTTP Basic Auth — encode the API key and password 'x' as Base64 in the Authorization header: 'Basic {base64(apiKey:x)}'.

Teamwork's v3 API uses a consistent JSON structure for creating resources: each POST body has a top-level key matching the resource type (e.g., 'project' for projects, 'task' for tasks). Responses follow the same structure with the created resource nested under its type key.

Project creation requires a name and optionally a description, start date, end date, and company ID (client). Task lists are created within projects, and tasks are created within task lists. This hierarchy (project → task list → task) mirrors Teamwork's interface.

For v1 endpoints (used for time logging and some advanced features), the base URL format is https://{your-site}/projects/{projectId}/time_entries.json and authentication is the same Basic Auth pattern.

The Node.js fetch API (Node 18+) or the axios library work well for Teamwork API calls. The examples below use fetch for zero dependencies.

```
// teamwork-client.js — Teamwork REST API client
const SITE = process.env.TEAMWORK_SITE;
const API_KEY = process.env.TEAMWORK_API_KEY;
const AUTH = 'Basic ' + Buffer.from(`${API_KEY}:x`).toString('base64');
const BASE_V3 = `https://${SITE}/projects/api/v3`;
const BASE_V1 = `https://${SITE}`;

async function twRequest(method, url, body = null) {
  const options = {
    method,
    headers: {
      'Authorization': AUTH,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    }
  };
  if (body) options.body = JSON.stringify(body);

  const response = await fetch(url, options);
  if (!response.ok) {
    const errText = await response.text();
    throw new Error(`Teamwork API ${response.status}: ${errText}`);
  }
  if (response.status === 204) return null;
  return response.json();
}

// Create a project
async function createProject(name, description = '', companyId = null) {
  const body = {
    project: { name, description }
  };
  if (companyId) body.project.company = { id: companyId };
  const result = await twRequest('POST', `${BASE_V3}/projects.json`, body);
  return result?.project;
}

// Get all projects
async function getProjects() {
  const result = await twRequest('GET', `${BASE_V3}/projects.json`);
  return result?.projects || [];
}

// Create a task list within a project
async function createTaskList(projectId, name) {
  const result = await twRequest('POST', `${BASE_V1}/projects/${projectId}/tasklists.json`, {
    'todo-list': { name }
  });
  return result?.['TASKLISTID'];
}

// Create a task within a task list
async function createTask(taskListId, title, description = '', assigneeId = null, dueDate = null) {
  const task = { 'content': title, 'description': description };
  if (assigneeId) task['responsible-party-id'] = assigneeId;
  if (dueDate) task['due-date'] = dueDate; // YYYYMMDD format
  const result = await twRequest('POST', `${BASE_V1}/tasklists/${taskListId}/tasks.json`, {
    'todo-item': task
  });
  return result?.id || result?.['TASKID'];
}

// Log a time entry (v1 API)
async function logTime(projectId, taskId, hours, minutes, date, description, personId) {
  const body = {
    'time-entry': {
      description,
      'person-id': personId,
      date, // YYYYMMDD
      hours: String(hours),
      minutes: String(minutes),
      isbillable: '1'
    }
  };
  const url = taskId
    ? `${BASE_V1}/tasks/${taskId}/time_entries.json`
    : `${BASE_V1}/projects/${projectId}/time_entries.json`;
  return twRequest('POST', url, body);
}

// Get time entries for a project
async function getTimeEntries(projectId) {
  const result = await twRequest('GET', `${BASE_V1}/projects/${projectId}/time_entries.json`);
  return result?.['time-entries'] || [];
}

// Example
(async () => {
  try {
    const projects = await getProjects();
    console.log(`Found ${projects.length} projects`);
    projects.slice(0, 3).forEach(p => console.log(` - ${p.id}: ${p.name}`));
  } catch (err) {
    console.error('Error:', err.message);
  }
})();

module.exports = { createProject, getProjects, createTaskList, createTask, logTime, getTimeEntries };
```

**Expected result:** Running the script retrieves and prints the first three Teamwork projects. If you have no projects yet, the output shows 'Found 0 projects' — you can then run createProject() to create one.

### 3. Manage Projects and Time Entries with Python

The Python client uses the requests library with HTTP Basic Auth. The requests library supports Basic Auth natively through the auth tuple parameter — pass (api_key, 'x') and requests handles the Base64 encoding and Authorization header automatically.

Teamwork's API returns slightly different JSON structure in v1 vs v3. The v1 API wraps objects in XML-like keys (e.g., 'todo-item', 'time-entry', 'todo-list') while v3 uses cleaner JSON with plural keys (e.g., 'tasks', 'projects'). Your Python client should handle both response formats for full compatibility.

For batch operations like creating multiple tasks at once, use Teamwork's v3 batch endpoint which accepts an array of task objects, or loop with individual creates and track errors per task.

Flask makes it easy to build a project management API proxy on top of Teamwork — expose simplified endpoints that your frontend or automation tools call, and your Replit server handles the Teamwork API complexity.

```
# teamwork_client.py — Teamwork REST API client for Replit (Python)
import os
import requests
from datetime import datetime

SITE = os.environ['TEAMWORK_SITE']
API_KEY = os.environ['TEAMWORK_API_KEY']
AUTH = (API_KEY, 'x')  # Teamwork Basic Auth: api_key as username, 'x' as password
BASE_V3 = f'https://{SITE}/projects/api/v3'
BASE_V1 = f'https://{SITE}'

HEADERS = {'Content-Type': 'application/json', 'Accept': 'application/json'}

def tw_request(method: str, url: str, body: dict = None) -> dict:
    """Make an authenticated Teamwork API request."""
    response = requests.request(method, url, auth=AUTH, headers=HEADERS, json=body)
    response.raise_for_status()
    if response.status_code == 204:
        return {}
    return response.json()

def get_projects() -> list:
    """Get all projects the API user has access to."""
    result = tw_request('GET', f'{BASE_V3}/projects.json')
    return result.get('projects', [])

def create_project(name: str, description: str = '') -> dict:
    """Create a new Teamwork project."""
    result = tw_request('POST', f'{BASE_V3}/projects.json', {
        'project': {'name': name, 'description': description}
    })
    return result.get('project', {})

def create_task_list(project_id: str, name: str) -> str:
    """Create a task list within a project. Returns the task list ID."""
    result = tw_request('POST', f'{BASE_V1}/projects/{project_id}/tasklists.json', {
        'todo-list': {'name': name}
    })
    return str(result.get('TASKLISTID', ''))

def create_task(task_list_id: str, title: str,
               description: str = '', due_date: str = None) -> str:
    """Create a task in a task list. due_date format: YYYYMMDD"""
    task = {'content': title, 'description': description}
    if due_date:
        task['due-date'] = due_date
    result = tw_request('POST', f'{BASE_V1}/tasklists/{task_list_id}/tasks.json', {
        'todo-item': task
    })
    return str(result.get('TASKID', result.get('id', '')))

def log_time(project_id: str, hours: int, minutes: int,
            description: str, date: str = None, task_id: str = None) -> dict:
    """Log a time entry. date format: YYYYMMDD (defaults to today)."""
    if not date:
        date = datetime.now().strftime('%Y%m%d')
    entry = {
        'time-entry': {
            'description': description,
            'date': date,
            'hours': str(hours),
            'minutes': str(minutes),
            'isbillable': '1'
        }
    }
    url = (f'{BASE_V1}/tasks/{task_id}/time_entries.json'
           if task_id else f'{BASE_V1}/projects/{project_id}/time_entries.json')
    return tw_request('POST', url, entry)

def get_time_entries(project_id: str) -> list:
    """Get all time entries for a project."""
    result = tw_request('GET', f'{BASE_V1}/projects/{project_id}/time_entries.json')
    return result.get('time-entries', [])

if __name__ == '__main__':
    projects = get_projects()
    print(f'Found {len(projects)} projects:')
    for p in projects[:5]:
        print(f'  {p["id"]}: {p["name"]}')
```

**Expected result:** Running python teamwork_client.py retrieves and prints the first five Teamwork projects with their IDs and names.

### 4. Set Up Webhooks and Build an Automation Server

Teamwork webhooks deliver real-time events to your Replit server when work items change — tasks are created, completed, or reassigned; time is logged; projects are updated; or milestones are reached. This lets you build responsive automations that react immediately to team activity.

To set up webhooks in Teamwork: go to Settings (gear icon) → Webhooks → Add Webhook. Enter your deployed Replit URL, select the event types you want to receive, and save. Teamwork delivers webhook payloads as JSON POST requests to your endpoint.

Teamwork webhook payloads include the event type (e.g., 'task.completed', 'time.created'), the entity that changed, and contextual data about the project and user involved. Use the event type to route different events to different handlers.

IMPORTANT: Configure the webhook URL to your deployed Replit URL, not a development URL. Development Repls go offline when you close your browser — Teamwork will mark deliveries as failed and retry with exponential backoff, but events fired during the offline period may be lost.

Use Autoscale deployment for Teamwork webhook receivers. Project management events (task completions, time logs) are infrequent and bursty — Autoscale handles this load pattern efficiently.

```
// teamwork-server.js — Express server for Teamwork API proxy and webhooks
const express = require('express');
const {
  createProject, createTaskList, createTask, logTime, getProjects
} = require('./teamwork-client');
const app = express();
app.use(express.json());

// POST /projects — create a project with default task lists
app.post('/projects', async (req, res) => {
  try {
    const { name, description, taskLists = [] } = req.body;
    if (!name) return res.status(400).json({ error: 'name required' });

    const project = await createProject(name, description);
    const projectId = project.id;
    const createdLists = [];

    // Create standard task lists
    for (const listName of taskLists) {
      const listId = await createTaskList(projectId, listName);
      createdLists.push({ name: listName, id: listId });
    }

    res.status(201).json({ projectId, taskLists: createdLists });
  } catch (err) {
    console.error('Create project error:', err.message);
    res.status(500).json({ error: err.message });
  }
});

// GET /projects — list projects
app.get('/projects', async (req, res) => {
  try {
    const projects = await getProjects();
    res.json(projects.map(p => ({ id: p.id, name: p.name, status: p.status })));
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /time — log a time entry
app.post('/time', async (req, res) => {
  try {
    const { projectId, taskId, hours, minutes, description, date } = req.body;
    if (!projectId || hours === undefined) {
      return res.status(400).json({ error: 'projectId and hours required' });
    }
    const result = await logTime(projectId, taskId, hours, minutes || 0, description, date);
    res.status(201).json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /webhook/teamwork — receive Teamwork webhook events
app.post('/webhook/teamwork', (req, res) => {
  res.status(200).json({ received: true }); // Respond immediately

  setImmediate(() => {
    const event = req.body;
    const eventType = event.eventType || event.type;
    console.log('Teamwork webhook:', eventType);

    if (eventType === 'task.completed' || eventType === 'TASK_COMPLETED') {
      const task = event.task || event.item;
      console.log('Task completed:', task?.name, 'in project:', task?.project?.name);
      // TODO: notify Slack, update external systems, log to analytics
    } else if (eventType === 'time.created' || eventType === 'TIME_CREATED') {
      const entry = event.timeEntry || event.item;
      console.log(`Time logged: ${entry?.hours}h ${entry?.minutes}m by ${entry?.person?.name}`);
      // TODO: update billing dashboard, check against budget
    } else if (eventType === 'milestone.completed' || eventType === 'MILESTONE_COMPLETED') {
      const milestone = event.milestone || event.item;
      console.log('Milestone reached:', milestone?.name);
      // TODO: send client update email
    }
  });
});

app.listen(3000, '0.0.0.0', () => {
  console.log('Teamwork integration server running on port 3000');
  console.log('Set webhook URL in Teamwork: https://yourapp.replit.app/webhook/teamwork');
});
```

**Expected result:** The Express server starts on port 3000. POST /projects creates a Teamwork project with the specified task lists. POST /time logs a time entry. Teamwork webhooks trigger the event handler and log event details.

## Best practices

- Store TEAMWORK_API_KEY and TEAMWORK_SITE in Replit Secrets (lock icon 🔒) — the API key provides the same access as the user account that owns it
- Create a dedicated Teamwork user account (api-bot@yourcompany.com) for API token ownership so that API activity is clearly labeled in Teamwork's audit logs
- Be aware that Teamwork uses different API versions (v1 vs v3) for different features — always check which base URL to use for each operation
- Match date formats to the API version: v1 requires YYYYMMDD (no dashes), v3 requires YYYY-MM-DD
- Deploy as Autoscale for webhook receivers — project management events are infrequent and bursty, and Autoscale handles this pattern efficiently without keeping a server running 24/7
- Log Teamwork entity IDs (project ID, task list ID, task ID) to your database when creating them — you need these for subsequent time logging, task updates, and deletion
- Use Teamwork's batch task creation endpoint when onboarding new projects to avoid API rate limits from sequential individual creates
- Implement idempotency checks before creating projects or tasks to prevent duplicates if your webhook triggers fire multiple times for the same event

## Use cases

### Automated Client Onboarding Project Setup

When a new client contract is signed in your CRM, automatically create a Teamwork project with the standard task lists and initial tasks for your onboarding process. Instead of manually setting up the same project structure every time, a Replit webhook receives the contract-signed event and creates a fully populated project ready for the team to start working.

Prompt example:

```
Build a webhook receiver that triggers when a new client is added to your CRM, creates a Teamwork project named after the client, adds standard task lists (Discovery, Design, Development, Launch), creates initial onboarding tasks in each list, and assigns the project manager as the default assignee.
```

### Automatic Time Logging from External Tools

Capture time entries from development tools, support platforms, or custom applications and log them directly to Teamwork. When a developer closes a GitHub issue, a support agent resolves a ticket, or a consultant marks work complete in another system, the Replit backend automatically creates a Teamwork time entry on the appropriate project and task — eliminating the manual time entry that many team members routinely forget.

Prompt example:

```
Create a webhook endpoint that receives GitHub pull request closed events, calculates the time from PR open to merge, looks up the associated Teamwork project by repository name, and logs a time entry for the assigned developer on the relevant task.
```

### Client Project Status Report

Generate automated weekly project status reports by querying Teamwork for task completion rates, logged hours vs. budget, upcoming milestones, and overdue tasks. The Replit server assembles the report data and delivers it via email or Slack — giving clients and stakeholders regular visibility without manual report creation.

Prompt example:

```
Build a scheduled report generator that queries Teamwork for all active projects, retrieves task completion percentages, logged hours vs. estimated budget, and upcoming milestone dates, formats the data into a project status report, and emails it to stakeholders every Monday morning.
```

## Troubleshooting

### Teamwork API returns 401 Unauthorized on all requests

Cause: The TEAMWORK_API_KEY is incorrect, or the Basic Auth header is not constructed correctly. Teamwork's Basic Auth requires base64 encoding of 'apiKey:x' (with the colon and 'x' as the password).

Solution: In Teamwork, click your avatar → Edit Profile → API Token to verify your API key. In your code, confirm the Authorization header is constructed as 'Basic ' + Buffer.from(apiKey + ':x').toString('base64') in Node.js or by passing auth=(apiKey, 'x') in Python requests.

```
// Verify the Authorization header is formatted correctly
const auth = Buffer.from(`${process.env.TEAMWORK_API_KEY}:x`).toString('base64');
console.log('Auth header:', `Basic ${auth.slice(0, 20)}...`);
// Test by calling the /me.json endpoint
fetch(`https://${process.env.TEAMWORK_SITE}/me.json`, {
  headers: { 'Authorization': `Basic ${auth}` }
}).then(r => r.json()).then(d => console.log('Auth test:', d?.person?.firstName || 'FAILED'));
```

### Tasks are created but due dates are not being set correctly

Cause: Teamwork's v1 API uses YYYYMMDD date format (no dashes or slashes), while v3 uses YYYY-MM-DD. Using the wrong format for the endpoint version causes the date to be silently ignored or rejected.

Solution: For v1 API task creation (/tasklists/{id}/tasks.json), format due dates as YYYYMMDD (e.g., '20260401'). For v3 API, use YYYY-MM-DD. Check which API version endpoint you are calling and match the date format accordingly.

```
// Format date for Teamwork v1 API (YYYYMMDD)
const formatDateV1 = (date) => date.toISOString().split('T')[0].replace(/-/g, '');
console.log(formatDateV1(new Date('2026-04-01'))); // '20260401'

// Format date for Teamwork v3 API (YYYY-MM-DD)
const formatDateV3 = (date) => date.toISOString().split('T')[0];
console.log(formatDateV3(new Date('2026-04-01'))); // '2026-04-01'
```

### Webhook events are not arriving at the Replit endpoint

Cause: The webhook URL is set to a development Replit URL that goes offline when the browser is closed, or webhooks are not enabled/configured in Teamwork Settings.

Solution: Deploy your Replit app and use the deployed HTTPS URL in Teamwork Settings → Webhooks. Verify the webhook is Active (not paused) in Teamwork's webhook management interface. Test the endpoint directly by sending a manual POST request to verify it is accessible.

### Time entries are created but not appearing in project reports

Cause: Time entries logged without a person ID may not be attributed correctly, or the date format was wrong causing the entry to be filed on the wrong date.

Solution: Call GET /people.json to get all user IDs and pass the correct person-id when logging time. Verify the date format is YYYYMMDD for v1 API calls. Check Teamwork's time tracking view filtered by date range to find the misattributed entry.

```
// Get people list to find valid person IDs
fetch(`https://${process.env.TEAMWORK_SITE}/people.json`, {
  headers: { 'Authorization': 'Basic ' + Buffer.from(`${process.env.TEAMWORK_API_KEY}:x`).toString('base64') }
}).then(r => r.json()).then(d => d.people.forEach(p => console.log(`${p.id}: ${p['first-name']} ${p['last-name']}`)));
```

## Frequently asked questions

### How do I find my Teamwork API key?

In Teamwork, click your avatar or profile picture in the top-right corner → Edit Profile (or Profile Settings) → scroll down to find 'API Token' → click 'Show your token'. Copy the token and add it to Replit Secrets as TEAMWORK_API_KEY. The token is tied to your user account and provides API access at the same permission level as your account.

### Does Teamwork have a free plan that includes API access?

Teamwork's Free Forever plan includes limited API access for basic operations. Paid plans (Deliver, Grow, Scale) unlock higher API rate limits and advanced features. For most integration use cases, even the free plan provides sufficient API access for testing. Check Teamwork's pricing page for current plan limits.

### How do I log time to a specific task vs. a project in Teamwork?

Use the task-specific endpoint (POST /tasks/{taskId}/time_entries.json) to log time against a specific task, which is more precise for reporting. Use the project-level endpoint (POST /projects/{projectId}/time_entries.json) to log time at the project level without task attribution. Both endpoints require the same request body format with hours, minutes, date, and description fields.

### What deployment type should I use for Teamwork webhook receivers on Replit?

Autoscale deployment is ideal for Teamwork webhook receivers. Project management events (task completions, time entries, milestone updates) are infrequent and bursty — the load pattern is well-suited to Autoscale which scales to zero when idle and scales up when events arrive. The 1-3 second cold start on Autoscale is well within Teamwork's webhook timeout window.

### What is the difference between Teamwork's v1 and v3 APIs?

Teamwork's v3 API (base URL: /projects/api/v3) is the newer, more RESTful design with cleaner JSON responses and modern conventions. The v1 API (base URL at the domain root) is older but has broader feature coverage — some operations like time entry creation and task list management still use v1 endpoints. A complete integration often needs to use both versions. The v3 API is preferred when available.

---

Source: https://www.rapidevelopers.com/replit-integration/teamwork
© RapidDev — https://www.rapidevelopers.com/replit-integration/teamwork
