Connect FlutterFlow to Todoist using a FlutterFlow API Call with a Bearer personal API token. Create an API Group pointed at api.todoist.com/rest/v2, set Authorization: Bearer {{token}} at group level, then call GET /tasks with Todoist's built-in filter parameter to fetch today's or overdue tasks server-side. Remember: the task completion endpoint returns 204 No Content — do not map a JSON response for that call.
| Fact | Value |
|---|---|
| Tool | Todoist |
| Category | Productivity |
| Method | FlutterFlow API Call |
| Difficulty | Beginner |
| Time required | 30 minutes |
| Last updated | July 2026 |
Build a Personal or Team Task App on Todoist in FlutterFlow
Todoist is one of the cleaner task APIs in the productivity space — a single Bearer token covers all endpoints, responses are flat arrays without envelopes, and the built-in filter system handles most common query patterns without requiring client-side filtering. If you're building a focused personal task companion or a lightweight team task view in FlutterFlow, Todoist is a great first API integration to learn on.
The REST API v2 is available on all plans including the free tier. Your personal API token is found in Todoist Settings → Integrations → Developer → API token. This is a long-lived static token that grants full access to your Todoist account — all projects, tasks, sections, labels, and comments. The rate limit is 450 requests per 15-minute window (some tiers get 1,000), which is generous for most mobile apps.
The standout feature for FlutterFlow integrations is Todoist's server-side filter parameter. Instead of fetching all 500 tasks and filtering them in a Dart Custom Function, you pass ?filter=today%20%7C%20overdue to GET /tasks and Todoist returns only the matching tasks. This keeps response payloads small, reduces app startup time, and conserves your rate limit budget. You can also pass the filter as a FlutterFlow API Call variable, letting a single call power multiple views (today's tasks, work tasks, personal tasks) by changing the filter string.
Integration method
FlutterFlow connects to Todoist's REST API v2 using a Bearer personal API token configured at the API Group level. Todoist's API returns flat JSON arrays with no envelope wrapper, making response binding straightforward. The integration's key technique is passing Todoist's native filter query parameter (e.g., 'today | overdue') to GET /tasks so the server filters results, keeping request counts low and response sizes small.
Prerequisites
- A Todoist account (any plan including Free — the REST API v2 is available on all plans)
- Your Todoist personal API token from Settings → Integrations → Developer → API token
- A FlutterFlow project with the API Calls panel available
- Basic familiarity with FlutterFlow's Backend Queries and how to bind API responses to widgets
Step-by-step guide
Find your Todoist personal API token
Open Todoist in your browser and go to Settings (click your profile picture in the top right → Settings). Navigate to the Integrations tab. Scroll down to find the Developer section, which contains your personal API token — a 40-character hexadecimal string. Click the Copy button next to the token to copy it to your clipboard. This is a long-lived static token that doesn't expire unless you manually revoke it from the same settings page. Treat it like a password. The personal API token gives full read and write access to your entire Todoist account: all projects, tasks, labels, sections, filters, and comments. For a personal task app where you're the only user, this is exactly what you want. For a multi-user app where each user sees their own Todoist data, each user would need to authenticate with their own token via OAuth — a more complex flow not covered in this tutorial, which focuses on a single-account setup. Note the rate limit: Todoist enforces 450 requests per 15-minute window on most plans. That's 30 requests per minute — plenty for personal use, but worth keeping in mind if your app makes many calls per page load.
Pro tip: Your API token is in Todoist Settings → Integrations → Developer. If you don't see the Developer section, scroll down — it's near the bottom of the Integrations tab.
Expected result: You have your Todoist personal API token (40-character hex string) copied and ready to use in FlutterFlow.
Create the Todoist API Group with Bearer auth
In FlutterFlow, click API Calls in the left navigation sidebar. Click + Add → Create API Group. Name the group Todoist. Set the Base URL to https://api.todoist.com/rest/v2 — this covers all Todoist REST API v2 endpoints: tasks, projects, sections, labels, and comments. Add the group-level Authorization header. In the Headers section, click Add Header. Set the key to Authorization. For the value, you have two options: Option A (simpler): type Bearer followed by a space and then paste your actual API token directly. Example: Bearer abc123yourtokenhere. Option B (recommended): go to Settings → App Values (App Constants) first, create a constant named TODOIST_TOKEN with your token value, then set the header value to Bearer {{TODOIST_TOKEN}}. This way if you need to update the token later, you change one constant instead of hunting through API Call configurations. Save the API Group. With this header set at the group level, every API Call you create inside the Todoist group will automatically send the Authorization header without any additional configuration per call.
1// API Group configuration reference2// Group name: Todoist3// Base URL: https://api.todoist.com/rest/v24// Headers (inherited by all child calls):5// Authorization: Bearer {{TODOIST_TOKEN}}6// Content-Type: application/jsonPro tip: Add Content-Type: application/json as a second group-level header so POST requests to create tasks are handled correctly without needing to set it per-call.
Expected result: A Todoist API Group is saved with the base URL and Bearer Authorization header. All child calls will inherit the auth header automatically.
Fetch tasks with the filter parameter
Inside the Todoist group, click Add API Call. Name it GetTasks. Set the method to GET. For the endpoint, use: /tasks?filter={{taskFilter}}. Now add a variable named taskFilter with type String and a default value of today | overdue. This default will show tasks due today and any overdue tasks — the most useful starting view for a personal task app. Todoist's filter syntax is powerful: - today | overdue — tasks due today or past due - today — only today's tasks - @work — tasks with the 'work' label - p1 — priority 1 tasks - assigned to: me — tasks assigned to you in shared projects - due before: +7 days — tasks due in the next week By exposing the filter as a variable, a single GetTasks API Call can power multiple views in your app (a tab bar, a segmented control, or a filter dropdown) just by changing the filter string passed to the call. Go to Response & Test. Paste a sample task response and click Generate from Response. Key JSON Paths: - $[*].content → the task text - $[*].due.date → due date (IMPORTANT: this is nested under 'due', which can be null) - $[*].priority → priority number (1-4) - $[*].project_id → the project this task belongs to - $[*].id → task ID (needed for complete/update calls) Test the call with a real filter string. You should see your actual Todoist tasks in the response.
1// GET /tasks?filter=today%20%7C%20overdue2// (URL-encoded: today | overdue)34// Sample Todoist tasks response — flat array, no envelope5[6 {7 "id": "2995104339",8 "content": "Write weekly report",9 "description": "",10 "project_id": "2203306141",11 "section_id": null,12 "parent_id": null,13 "order": 1,14 "priority": 4,15 "due": {16 "string": "today at 9am",17 "date": "2026-07-10",18 "datetime": "2026-07-10T09:00:00Z",19 "is_recurring": false,20 "timezone": "America/New_York"21 },22 "is_completed": false,23 "labels": ["work"]24 },25 {26 "id": "2995104340",27 "content": "Call plumber",28 "description": "",29 "project_id": "2203306142",30 "section_id": null,31 "parent_id": null,32 "order": 2,33 "priority": 1,34 "due": null,35 "is_completed": false,36 "labels": ["personal"]37 }38]3940// Key JSON Paths41// $[*].content → task text42// $[*].due.date → due date (CAREFUL: due can be null)43// $[*].due.datetime → due date+time (null if no time set)44// $[*].priority → 1 (normal) to 4 (urgent)45// $[*].id → task ID for close/update calls46// $[*].is_completed → booleanPro tip: URL-encode special characters in filter strings. The | character becomes %7C and spaces become %20. Alternatively, pass the filter as a POST body using the /tasks endpoint's filter field if URL encoding causes issues.
Expected result: GetTasks API Call returns a flat JSON array of tasks matching the filter. JSON Paths for content, due.date, priority, and id are generated. Real tasks appear in the Response tab.
Bind tasks to a ListView with null-safe due dates
On your main tasks page in FlutterFlow, add a ListView. Set its Backend Query to Todoist group → GetTasks. Pass the filter string you want for this page as the taskFilter variable (e.g., 'today | overdue'). Inside the ListView, add a ListTile or Card widget as the child. Add Text widgets for: - Task content: bind to $[*].content - Due date: bind to $[*].due.date — BUT this requires special handling - Priority: use a Conditional Widget or color-code based on $[*].priority value The due date binding is the critical part. The due field itself can be null for tasks without a due date — tasks like 'Call plumber someday' have no due date at all. In Dart/Flutter (and FlutterFlow), accessing .date on a null object causes a 'Null check operator used on a null value' error that crashes the widget. In FlutterFlow, handle this by wrapping the due date Text widget in a Conditional Widget. Set the condition to: API Response → GetTasks → $[*].due != null. Show the due date Text only when due is not null; show a different widget (e.g., Text('No due date') in gray) when it is null. For the priority display, add a Container or Icon and color-code it based on the priority value: 4 = red (urgent), 3 = orange (high), 2 = blue (medium), 1 = gray (normal). Use FlutterFlow's Conditional Color feature or a Custom Function to map priority numbers to colors. Test in Run Mode to see your actual Todoist tasks displayed in the app.
Pro tip: For tasks with a datetime value (specific time set, not just a date), $[*].due.datetime will be an ISO string you can parse with Dart's DateTime.parse(). For tasks with only a date (no specific time), $[*].due.datetime will be null but $[*].due.date will be a YYYY-MM-DD string.
Expected result: A ListView shows tasks from Todoist with content text, conditional due date display, and priority color coding. Tasks without due dates show 'No due date' without any crashes.
Complete tasks and create new tasks
Completing a task in Todoist uses the endpoint POST /tasks/{id}/close. This endpoint returns HTTP 204 No Content with an empty body — it doesn't send back any JSON. This is an important difference from most API responses you've configured so far. Add a new API Call inside the Todoist group. Name it CloseTask. Method: POST. Endpoint: /tasks/{{taskId}}/close. Add a variable named taskId. In the Response & Test tab, DO NOT paste a sample response and DO NOT generate JSON Paths. Leave the response mapping completely empty. FlutterFlow will flag an issue if you try to map a response from an endpoint that returns nothing. Instead, treat any 2xx response as success. In your task ListView, add a Checkbox or circle Checkmark button to each task row. Wire its on-tap Action to Backend/API Call → CloseTask, passing the current row's $[*].id as taskId. After a successful call, either: - Remove the item from the list using App State manipulation, or - Trigger a page refresh to re-fetch the task list For creating tasks, add a POST /tasks API Call. Method: POST. Endpoint: /tasks. Body: {"content": "{{taskContent}}", "due_string": "{{dueString}}", "priority": {{priority}}}. The due_string can be natural language like 'tomorrow at 9am' or 'next Monday' — Todoist parses it automatically. Rate limit awareness: 450 requests per 15 minutes means you can complete about 450 tasks per 15 minutes before hitting limits. For personal apps, this is essentially unlimited. If you're building a team app where multiple users share one token (not recommended), be mindful of the cumulative rate.
1// POST /tasks/{taskId}/close — complete a task2// Returns: 204 No Content (empty body)3// DO NOT configure response mapping for this call4// Treat any 2xx response as success56// POST /tasks — create a new task7// Method: POST8// Endpoint: /tasks9// Body:10{11 "content": "{{taskContent}}",12 "due_string": "{{dueString}}",13 "priority": 4,14 "project_id": "{{projectId}}",15 "labels": ["{{label}}"]16}17// due_string accepts natural language: 'today', 'tomorrow', 'next Monday at 9am'18// priority: 1=normal, 2=medium, 3=high, 4=urgent1920// GET /projects — list all projects21// Method: GET, Endpoint: /projects22// Response: flat array, same structure as tasks23// $[*].id → project ID24// $[*].name → project name25// $[*].color → color labelPro tip: Todoist's due_string parameter accepts natural language date strings in English — 'today', 'tomorrow at 3pm', 'every Monday', 'next week'. This saves you from building a date picker for simple task creation.
Expected result: Tasks can be marked complete from the ListView (CloseTask fires and the task disappears). New tasks can be added from a form using natural language due string input.
Common use cases
Daily task companion showing today's and overdue tasks
Build a focused mobile dashboard that shows only what needs attention right now. The app calls GET /tasks with filter='today | overdue', displays tasks in a flat list with task name and due time, and lets users check off tasks with a completion button. Completed tasks disappear from the list immediately using local App State updates.
Show all Todoist tasks due today or overdue, sorted by due time, with task name and a button to mark each one complete.
Copy this prompt to try it in FlutterFlow
Project-based team task tracker
Build a FlutterFlow app for small teams where members can browse their Todoist projects and see tasks per project. The app fetches GET /projects to build a selector, then calls GET /tasks?project_id={id} to show tasks for the selected project. Team members can add tasks via a form and assign priorities.
Show a list of all Todoist projects. When a user selects a project, display all incomplete tasks for that project with priority color coding.
Copy this prompt to try it in FlutterFlow
Label-based work context switcher
Build a context-aware task view that switches between 'work', 'personal', and 'errands' contexts using Todoist labels. The app displays a segmented tab bar where each tab sends a different filter string to GET /tasks (e.g., filter='@work', filter='@personal'). Users see only tasks matching the selected context without separate API calls per context.
Show a tab bar with context labels. Each tab filters Todoist tasks by label and shows matching tasks sorted by due date.
Copy this prompt to try it in FlutterFlow
Troubleshooting
Null check operator used on a null value — app crashes when tasks have no due date
Cause: The 'due' field in Todoist's task response is a nested object that is completely null (not just empty) for tasks without a due date. Binding $[*].due.date without guarding for null throws a Dart null-check error when Flutter tries to access .date on a null object.
Solution: Wrap the due date Text widget in a Conditional Widget in FlutterFlow. Set the condition to check whether the due JSON Path is not null. Only render the due date widget when due is present; show a 'No due date' placeholder widget otherwise. This prevents the crash without filtering out undated tasks from the list.
FlutterFlow shows a parse error or response mapping error for the CloseTask call
Cause: Todoist's POST /tasks/{id}/close endpoint returns HTTP 204 with no response body. FlutterFlow expects a JSON response when you configure response mapping, and if you've added JSON Paths or tried to map the empty response, it will flag an error.
Solution: Remove all response mapping from the CloseTask API Call. Go to the Response & Test tab, delete any JSON Paths you added, and leave the response section empty. Check for success by looking at the HTTP status code: any 2xx response (especially 204) means the task was closed successfully.
429 Too Many Requests after extended use
Cause: Todoist enforces 450 requests per 15-minute window. Apps that poll for tasks on a timer, or pages that make many calls on load (fetching tasks plus projects plus labels simultaneously), can accumulate requests quickly.
Solution: Replace polling with explicit user-initiated refresh (pull-to-refresh). Cache the task list in App State and only re-fetch after a task is completed or created. Fetch projects and labels once on app start and store in App State — they rarely change during a session. Respect the Retry-After header value in 429 responses before retrying.
Filter parameter returns unexpected results or no results
Cause: Special characters in filter strings (| for OR, & for AND) must be URL-encoded in query parameters. Also, filter strings are case-sensitive for labels (@Work is different from @work).
Solution: URL-encode the filter string: replace | with %7C and & with %26 when building the endpoint URL. In FlutterFlow, add the filter as a Variable and FlutterFlow may handle URL encoding automatically — test with a simple filter like 'today' first to confirm the call works, then add special characters. Also verify label names match exactly (case-sensitive) as they appear in Todoist.
Best practices
- Use Todoist's server-side filter parameter instead of fetching all tasks and filtering in Dart — server-side filtering is faster, uses fewer requests, and keeps response payloads small.
- Store your API token as an App Constant named TODOIST_TOKEN and reference it as Bearer {{TODOIST_TOKEN}} in the group-level Authorization header for easy credential rotation.
- Always guard due date bindings with Conditional Widgets — the due field is null for tasks without dates, and null access crashes the widget tree.
- Never configure response mapping on the POST /tasks/{id}/close call — it returns 204 No Content and has no JSON body to parse.
- Cache the project list in App State on app launch so your project picker doesn't make a new GET /projects call every time the user opens it.
- Use Todoist's natural language due_string parameter for task creation ('tomorrow at 9am', 'every Monday') instead of requiring users to pick from a date picker.
- Make the filter string a user-configurable App State variable so you can build tab bars or filter dropdowns that switch views without creating multiple API Calls.
- Implement pull-to-refresh rather than a polling timer to stay well within the 450 req/15-min rate limit.
Alternatives
Choose Asana for team task management with richer project structures and assignee tracking — Asana's opt_fields system offers more control but requires the GID-chain setup this tutorial skips.
Choose Notion if your team tracks tasks inside Notion databases rather than a dedicated task tool — Notion's API provides access to pages and database rows with more freeform structure.
Frequently asked questions
Does FlutterFlow have a native Todoist integration?
No. There is no native Todoist connector in FlutterFlow's Settings & Integrations panel. The integration is built manually using FlutterFlow's API Calls panel with an API Group configured for Bearer authentication. This tutorial walks through the complete setup including the Bearer auth group, the filter parameter, null-safe due date bindings, and the 204 response from the task completion endpoint.
Why does my app crash when some tasks have no due date?
Todoist's due field is a JSON object ({ "date": "...", "datetime": "..." }) when a task has a due date, and completely null when it doesn't. Dart's null safety means accessing .date on null throws a 'Null check operator used on a null value' error. The fix is to wrap your due date widget in a Conditional Widget in FlutterFlow, only rendering the date when the due field is not null.
Can I use this tutorial for a multi-user app where each user sees their own Todoist tasks?
Not directly. This tutorial uses a single personal API token (one Todoist account). For a multi-user app, you'd need each user to authenticate with their own Todoist token via OAuth 2.0 — a more complex flow involving an authorization redirect and token exchange. The personal token approach here works well for single-account apps (your own task dashboard) or internal tools where all users share one Todoist workspace.
What happens if I call the close task endpoint and it returns 204? Will FlutterFlow show an error?
Only if you've configured response mapping for that API Call. FlutterFlow's response mapping expects JSON, and when it receives a 204 with no body, it may flag a parse issue. The solution is to leave the Response & Test tab completely empty for the CloseTask call — no JSON Paths, no sample response. FlutterFlow will still treat 2xx status codes as success for the action flow to continue.
How do I show tasks from a specific Todoist project rather than using filter strings?
Use the project_id query parameter on GET /tasks instead of the filter parameter: /tasks?project_id={{projectId}}. First call GET /projects to get the list of projects and their IDs, store the selected project ID in App State, then pass it to GetTasks as the projectId variable. The two approaches (filter strings and project_id) can be combined: /tasks?project_id={{projectId}}&filter=overdue shows overdue tasks in a specific project.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation