Connect FlutterFlow to Teamwork via a FlutterFlow API Call pointing at your tenant subdomain. Teamwork uses HTTP Basic auth where the API key is the username and any string is the password — encode them as Base64 and store the result in App Constants. Every endpoint requires a `.json` suffix. Build a client-project dashboard by binding `/projects.json` to a ListView and drilling into tasks and milestones per project.
| Fact | Value |
|---|---|
| Tool | Teamwork |
| Category | Productivity |
| Method | FlutterFlow API Call |
| Difficulty | Intermediate |
| Time required | 35 minutes |
| Last updated | July 2026 |
Building a Client-Project Dashboard with Teamwork in FlutterFlow
Teamwork is built for agencies and client-services firms — it organizes work around projects, milestones, task lists, time logs, and client billing rather than the personal-productivity model of tools like Todoist or the issue-tracker model of Jira. If you are building a mobile app for an agency to monitor multiple client engagements at a glance, Teamwork's REST API gives you structured access to all of that data.
Two quirks set Teamwork apart from most REST APIs and trip up first-time integrators. First, the API uses HTTP Basic auth, but not with an email and password. Instead, your API key is the username and any arbitrary string (commonly just 'X' or 'xxx') serves as the password. This combination is then Base64-encoded and placed in an Authorization header as `Basic {base64string}`. FlutterFlow has no built-in Basic auth helper, so you pre-compute the Base64 value outside FlutterFlow and store it in an App Constant. Second, every Teamwork endpoint requires a `.json` suffix on the URL path. Calling `/projects` instead of `/projects.json` returns XML or HTML that FlutterFlow's JSON parser cannot process.
Your API key is found in Teamwork under your profile picture in the top-right corner, then 'Edit My Details', then the 'API' tab. The key is read-write and grants access to all projects your Teamwork user can see. Keep write operations (creating tasks, logging time) behind a Firebase Cloud Function rather than calling Teamwork directly from the app, so the key is not hardcoded in the client binary. Read-only calls (browsing projects, viewing tasks) are lower-risk when the key is stored only in App Constants rather than sent in server-side code, but be aware that a determined person could extract App Constants from a compiled app.
Integration method
Teamwork exposes a REST API on your account's own subdomain (yourcompany.teamwork.com) using HTTP Basic auth where the API key serves as the username. You pre-compute the Base64 encoding of 'apiKey:X' and store it in an App Constant, then set the Authorization header at the API Group level so every child call inherits it. Response data is flat JSON with a `.json` suffix required on every endpoint path.
Prerequisites
- An active Teamwork account on a paid plan (API access requires a paid subscription)
- Your Teamwork API key from Profile → Edit My Details → API tab
- Your Teamwork subdomain (e.g., yourcompany.teamwork.com)
- The Base64 encoding of 'apiKey:X' — compute this using a free online Base64 encoder or the browser console with btoa('yourApiKey:X')
- A FlutterFlow project open and ready to add API Calls
Step-by-step guide
Find your API key and compute the Basic auth header value
Log into Teamwork and click your profile picture in the top-right corner. Select 'Edit My Details' from the dropdown. On the edit page, switch to the 'API' tab — you will see your personal API key displayed. Copy this key carefully; it is a long alphanumeric string. Now you need to create the Base64 value that will go in the Authorization header. Teamwork's Basic auth scheme treats the API key as the username and accepts any string as the password. The most common convention is to use 'X' or 'xxx' as the dummy password. Open your browser's developer console (right-click → Inspect → Console) and type: `btoa('YOUR_API_KEY:X')` — substitute your real API key. Press Enter and the console returns a Base64 string like 'dGhpcyBpcyBhIHRlc3Q6WA=='. Copy this value. In FlutterFlow, go to App Settings → App Constants (also called App Values in some FlutterFlow versions) and add two constants: one named `teamworkBase64Auth` set to the Base64 string you just computed, and one named `teamworkSubdomain` set to your subdomain (just the subdomain portion, like 'yourcompany' if your Teamwork URL is yourcompany.teamwork.com). Storing the subdomain as a constant means you can swap Teamwork accounts or environments by changing one value. Important: the Base64 value includes your API key in an easily reversible encoding. Anyone who can access your FlutterFlow App Constants or decompile your app can decode it. For write operations (creating tasks, logging time), route the call through a Firebase Cloud Function that holds the API key as an environment variable, so the key never reaches the client device at all.
1// Compute in browser console:2// btoa('YOUR_TEAMWORK_API_KEY:X')3// Example output: 'dGhpcyBpcyBhIHRlc3Q6WA=='4//5// App Constants to create in FlutterFlow:6// teamworkBase64Auth = '<result of btoa above>'7// teamworkSubdomain = 'yourcompany' (just the prefix, not the full URL)8Pro tip: The dummy password after the colon can be anything — 'X', 'xxx', or even your email. Teamwork ignores the password field entirely and authenticates solely on the API key.
Expected result: You have a Base64 auth string and your subdomain stored in FlutterFlow App Constants, ready to be referenced in API Call headers.
Create an API Group with Basic auth for Teamwork
In FlutterFlow, click 'API Calls' in the left navigation panel. Click '+ Add' and select 'Create API Group'. Name this group 'Teamwork'. For the Base URL, enter `https://{{subdomain}}.teamwork.com` — but FlutterFlow's Base URL field does not support App Constant references directly in the URL in all versions. A practical workaround is to enter the full domain directly: `https://yourcompany.teamwork.com`. If you need to switch accounts, you will update this one field. Switch to the 'Headers' section of the API Group. Add one header: - Key: `Authorization` - Value: `Basic {{base64Auth}}` Now switch to the Variables tab of the API Group and add a variable named `base64Auth` (type String, default value: the App Constant `teamworkBase64Auth`). This variable is what gets substituted into `{{base64Auth}}` in the header above. Setting the default to your App Constant means every API Call in this group inherits the correct auth header automatically without you needing to set it per call. This is a critical step that new FlutterFlow users often get wrong: Teamwork does not accept a Bearer token, and setting `Authorization: Bearer {{apiKey}}` will return 401 Unauthorized. The value must be `Basic ` followed by the Base64-encoded `apiKey:X` string, not the raw API key itself.
1// API Group configuration reference:2// Name: Teamwork3// Base URL: https://yourcompany.teamwork.com4//5// Headers:6// Authorization: Basic {{base64Auth}}7//8// Group Variable:9// base64Auth (String) — default: FFAppConstants.teamworkBase64Auth10Pro tip: Set the `base64Auth` variable default to `FFAppConstants.teamworkBase64Auth` in the FlutterFlow variable picker. This way the API Group auto-populates the correct value and you never have to hardcode it in a Call.
Expected result: The Teamwork API Group appears in your API Calls panel with an Authorization header already configured. You are ready to add individual API Calls to this group.
Add the /projects.json call and test it
Inside the Teamwork API Group, click '+ Add API Call'. Name it 'Get Projects'. Set the method to GET and the endpoint to `/projects.json`. Notice the `.json` suffix — this is mandatory for every Teamwork endpoint. Omitting it returns a 406 or HTML/XML response that FlutterFlow cannot parse, which will appear as an empty response or a 'JSON parse error' in the Response tab. You can optionally add query parameters to filter the results. Common ones for a project health dashboard include `status=active` (to exclude archived projects) and `orderby=lastupdated` (to sort by most recent activity). Add these as API Call Variables (type String) and append them to the endpoint like `/projects.json?status={{status}}&orderby={{orderby}}`. Click 'Response & Test' and run the call. If your credentials are correct, you will see a JSON response like `{ "STATUS": "OK", "projects": [ ... ] }`. The project data lives inside the `projects` key. Click 'Generate from Response' to create JSON Paths such as `$.projects[*].id`, `$.projects[*].name`, `$.projects[*].status`, and `$.projects[*].lastChangedOn`. Common JSON Paths you will use in your ListView: - `$.projects[*].id` — numeric project ID for drilling into tasks - `$.projects[*].name` — project display name - `$.projects[*].status` — 'active', 'archived', 'completed' - `$.projects[*].startDate` and `$.projects[*].endDate` - `$.projects[*].company.name` — client company name If you see a 401 response, double-check that your Authorization header value starts with 'Basic ' (with a space) followed by the Base64 string — missing the space is the second most common setup mistake after forgetting the `.json` suffix.
1// API Call reference:2// Name: Get Projects3// Method: GET4// Endpoint: /projects.json5//6// Optional query params (add as Variables):7// ?status=active&orderby=lastupdated8//9// Key JSON Paths from response:10// $.projects[*].id11// $.projects[*].name12// $.projects[*].status13// $.projects[*].company.name14// $.projects[*].endDate15// $.STATUS — check this equals 'OK'16Pro tip: The Teamwork response wraps everything in a `STATUS: OK` field. If your ListView appears empty despite a 200 response, check whether your JSON Paths start with `$.projects[*]` and not `$[*]` — the array is nested under the `projects` key, not at the root.
Expected result: Running the 'Get Projects' call in the Response tab shows your Teamwork projects as JSON. FlutterFlow generates JSON Paths including `$.projects[*].name` and `$.projects[*].company.name`.
Add /tasks.json per project and bind to a detail screen
Add a second API Call inside the Teamwork group. Name it 'Get Tasks'. Set the method to GET and add a variable named `projectId` (type Integer). Set the endpoint to `/projects/{{projectId}}/tasks.json`. This returns all tasks for the given project. Common optional query parameters for tasks: `filter=incomplete` (show only open tasks), `orderby=duedate`, and `includeCompletedSubtasks=false`. Add these as additional Variables if you want the detail screen to filter tasks by status. Key JSON Paths from the tasks response (Teamwork wraps tasks under `todo-items`): - `$['todo-items'][*].id` - `$['todo-items'][*].content` — task name - `$['todo-items'][*].due-date` — in YYYYMMDD format - `$['todo-items'][*].completed` - `$['todo-items'][*]responsible-party-summary` — assignee name Add a third API Call named 'Get Milestones'. Endpoint: `/projects/{{projectId}}/milestones.json`. Key paths from the response: `$.milestones[*].title`, `$.milestones[*].deadline`, `$.milestones[*].status`. For the project detail screen in FlutterFlow: create a new Page called 'Project Detail'. Pass the project ID as a Route Parameter from the projects ListView (tap action → Navigate → Project Detail → set projectId parameter to `$.projects[*].id`). On the detail page, add two ListViews: one for tasks (Backend Query = Get Tasks, pass projectId from Route Parameter) and one for milestones (Backend Query = Get Milestones, same projectId). Add a Tab Bar widget at the top so users can switch between the tasks view and milestones view without scrolling past both lists. For the time-log summary, add a fourth API Call: 'Get Time Totals', endpoint `/projects/{{projectId}}/time/total.json`. This returns a single object with `timeTotals.hoursLogged` and `timeTotals.minutesLogged` — bind these to summary Text widgets at the top of the detail screen.
1// Get Tasks:2// Method: GET3// Endpoint: /projects/{{projectId}}/tasks.json4// Variable: projectId (Integer)5//6// Key JSON Paths (note hyphenated keys need bracket notation in paths):7// $['todo-items'][*].id8// $['todo-items'][*].content9// $['todo-items'][*]['due-date']10// $['todo-items'][*].completed11//12// Get Milestones:13// Method: GET14// Endpoint: /projects/{{projectId}}/milestones.json15// $.milestones[*].title16// $.milestones[*].deadline17// $.milestones[*].status18Pro tip: Teamwork task fields use hyphenated keys like `due-date` and `responsible-party-summary`. In JSON Path notation these need bracket syntax: `$['todo-items'][*]['due-date']`. If FlutterFlow's Generate from Response creates dot-notation paths for these, switch them manually to bracket notation.
Expected result: Tapping a project in the ListView navigates to the Project Detail screen showing that project's tasks and milestones in separate tabs, loaded via Backend Queries using the projectId Route Parameter.
Handle rate limits and cache project lists in App State
Teamwork's rate limits vary by plan — typically between 150 and 300 requests per minute. A multi-client agency app can trip this limit quickly if you fan out several API calls simultaneously. For example, if your dashboard loads 20 client projects and fires a separate tasks call for each one on load, that is 21 requests in a single screen render — well above the per-minute budget if users navigate quickly. The most effective mitigation is to separate what loads eagerly versus on demand. Load the project list (`/projects.json`) eagerly when the screen opens and cache the result in App State (a FlutterFlow Page State or App State variable of type list). Do not load tasks or milestones until the user taps a specific project. This reduces page-load API calls from N+1 to just 1. For the project list itself, add a simple time-based cache: store the projects list in App State along with a `projectsLoadedAt` timestamp. When the screen loads, check whether `projectsLoadedAt` is less than 5 minutes ago — if it is, render from App State without calling the API. If it is older, refresh from the API and update both App State variables. This pattern is fully achievable using FlutterFlow's Conditional Actions and Update App State action without any custom Dart code. For write operations (creating tasks, logging time), route those calls through a Firebase Cloud Function rather than calling Teamwork directly. This protects your API key from being visible in the compiled app and lets you add extra validation or rate limiting at the Cloud Function level if needed.
Pro tip: If you need to show a client-scoped view (only their projects), filter by `company` in the Teamwork API response using a Custom Function on the App State projects list rather than calling a different API endpoint — this keeps the API call count at 1 regardless of how many clients you manage.
Expected result: The projects ListView loads quickly on first visit and serves from App State cache on subsequent visits. Task and milestone calls only fire when a user taps into a specific project, keeping total API call volume low.
Common use cases
Agency client-project health dashboard
A FlutterFlow app for account managers that shows all active Teamwork projects in a ListView, each with an overdue-task count, upcoming milestone, and a color-coded health badge. Tapping a project opens a detail screen with the full task list and milestone timeline.
Build a project dashboard that lists all our Teamwork projects with their status, next milestone date, and a count of overdue tasks. Tapping a project shows the task list and milestone calendar.
Copy this prompt to try it in FlutterFlow
Time-log entry screen for field staff
A mobile screen where team members pick a project from a dropdown, select a task, enter hours and a description, and submit a time log entry via the Teamwork API. The app pre-fills today's date and the logged-in user's Teamwork user ID.
Create a time-log entry form where I pick a project, pick a task within that project, enter hours and a note, and submit the time log to Teamwork for that day.
Copy this prompt to try it in FlutterFlow
Client-facing project status viewer
A read-only app given to clients that shows the progress of their specific Teamwork project — milestone completion, open vs closed tasks, and team activity feed — without exposing other clients' data. A Firebase Cloud Function filters results to the client's project ID.
Build a client portal that shows one Teamwork project's milestones, task completion percentage, and recent activity log. The client should only see their own project.
Copy this prompt to try it in FlutterFlow
Troubleshooting
401 Unauthorized on every API call even with the correct API key
Cause: The Authorization header format is wrong. Common mistakes: (1) using the raw API key instead of the Base64-encoded 'apiKey:X' string, (2) missing the 'Basic ' prefix with a trailing space, or (3) using 'Bearer' instead of 'Basic'.
Solution: Open a browser console and run `btoa('YOUR_API_KEY:X')`. Copy the result. In your FlutterFlow API Group header, set the Authorization value to exactly `Basic ` (with a space) followed by that Base64 string. Verify the App Constant `teamworkBase64Auth` contains only the Base64 string without any extra whitespace or newline characters.
API returns XML, HTML, or a 406 error instead of JSON
Cause: The endpoint URL is missing the `.json` suffix. Teamwork defaults to returning XML when the suffix is absent, which FlutterFlow's JSON parser cannot handle.
Solution: Append `.json` to every Teamwork endpoint path. For example: `/projects.json`, `/projects/{{projectId}}/tasks.json`, `/projects/{{projectId}}/milestones.json`. Check every API Call in your Teamwork group for this suffix.
ListView is empty despite a 200 response in the Response tab
Cause: JSON Path bindings do not match the response structure. Teamwork wraps results under named keys: `$.projects[*]` for projects, `$['todo-items'][*]` for tasks — not at the array root `$[*]`.
Solution: In the Response tab, expand the raw JSON to find the wrapper key. For projects, your ListView binding must start with `$.projects[*].name`, not `$[*].name`. For tasks, use `$['todo-items'][*].content`. Click 'Generate from Response' after a successful test to have FlutterFlow suggest the correct paths automatically.
429 Too Many Requests when loading a dashboard with many clients
Cause: The app fires one API call per project on page load (N+1 pattern), exceeding Teamwork's per-minute rate limit for accounts on lower plans.
Solution: Load only the project list on page open and cache it in App State. Defer tasks, milestones, and time-total calls until the user taps a project row. Add a 5-minute App State cache so repeated navigation to the project list does not re-fetch until data is stale.
Best practices
- Always append the .json suffix to Teamwork endpoint paths — omitting it returns XML that FlutterFlow cannot parse, with no obvious error message.
- Pre-compute the Base64 'apiKey:X' encoding outside FlutterFlow and store only the encoded string in an App Constant, never the raw API key.
- Store your Teamwork subdomain as an App Constant so you can swap accounts or point at different Teamwork instances without touching individual API Call configurations.
- Route write operations (task creation, time logging) through a Firebase Cloud Function to prevent the API key from shipping in the compiled app binary.
- Cache the project list in App State with a timestamp and serve from cache for 5 minutes to reduce API calls on repeated navigation.
- Lazy-load task and milestone data only when a user taps a project row — never fan out N calls on initial page load.
- Add a Conditional Visibility empty-state widget for lists that return zero items, since an empty `projects` or `todo-items` array is valid and should display a friendly message rather than a blank screen.
Alternatives
Choose Jira if your team needs a full issue-tracking and sprint management system with JQL search queries and deep developer integrations rather than client-services project management.
Choose Asana if you prefer a Bearer-token API with a cleaner JSON structure and no subdomain-specific base URLs — at the cost of Teamwork's agency-specific features like invoicing and client portals.
Frequently asked questions
Does Teamwork have a free plan that includes API access?
Teamwork's free plan is limited and does not include API access. You need at least the Starter or Deliver paid plan to generate an API key and make REST API calls. Check the current plan details at teamwork.com/pricing before building the integration.
Why does the Basic auth use a dummy password ('X') instead of my real password?
Teamwork's API authentication is token-based, not credential-based. The HTTP Basic auth format requires a 'username:password' pair, but Teamwork uses the API key as the username and ignores the password field entirely. Entering 'X', 'xxx', or any string after the colon works equally well — it is just there to satisfy the Basic auth format requirement.
Can I show only one client's projects in the app without seeing all projects?
Yes. The `/projects.json` endpoint accepts a `company` query parameter that filters to projects belonging to a specific Teamwork company (client). You can also filter client-side by adding a Custom Function that filters the App State projects list by `company.id`. Store the client's company ID as a Route Parameter or App State variable to apply the filter automatically when the screen loads.
Will this integration work in FlutterFlow Test Mode?
Yes. Test Mode proxies all API calls through FlutterFlow's servers, which handles the Basic auth header correctly. The `.json` suffix requirement and JSON Path bindings all work identically in Test Mode and in a deployed app. Test your calls in the Response tab first, then switch to Run Mode to preview the full widget binding.
How do I log time entries from FlutterFlow?
Time logging uses a POST call to `/projects/{projectId}/time_entries.json` with a JSON body containing `time-entry.description`, `time-entry.date`, `time-entry.hours`, `time-entry.minutes`, and `time-entry.person-id`. Because this is a write operation that uses your API key, route it through a Firebase Cloud Function rather than calling Teamwork directly from FlutterFlow.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation