# Todoist

- Tool: Bubble
- Difficulty: Beginner
- Time required: 1–2 hours
- Last updated: July 2026

## TL;DR

Connect Bubble to Todoist using the API Connector plugin with a Private Bearer token header. Todoist's REST API v2 returns lists as bare JSON arrays with no wrapper key, which changes how Bubble's initialization works. Create tasks from Bubble forms using a simple POST call with only the content field required. Embed a full personal task widget in a Bubble CRM or project portal — the free tier works for all plan levels.

## Embed a Todoist task widget inside your Bubble app

Todoist's simple priority-and-due-date model makes it the most beginner-friendly task API in this category. Its free tier supports full API access with 5 active projects — enough for most early-stage founders to build a personal productivity widget inside a larger Bubble app without any additional subscription cost.

The top Bubble use case for Todoist is an embedded task inbox: a panel inside a Bubble CRM, project portal, or internal tool where users can see their Todoist tasks and create new ones without leaving the Bubble app. This is the kind of integration that takes less than 2 hours to build and immediately saves team members context-switching time.

Two non-obvious details about the Todoist API that affect Bubble setup:

First, Todoist REST API v2 returns list endpoints as bare JSON arrays — just `[{...}, {...}]` with no wrapper key like `data` or `results`. This is different from most APIs. During Bubble API Connector initialization, you must confirm that the response is a list, and in some cases manually set 'Use as' = Data (list) if Bubble misdetects it as a single object.

Second, Todoist's priority scale is inverted: 1 is the lowest priority (normal), and 4 is the highest (urgent). New integrations commonly map this backwards. This tutorial shows you the correct conditional logic to display priority labels and colors.

## Before you start

- A Todoist account (free tier supports 5 active projects and full API access — no paid plan required for this integration)
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
- Basic familiarity with Bubble's workflow editor, repeating groups, and conditional styling

## Step-by-step guide

### 1. Find your Todoist Personal API token

Log into Todoist at todoist.com. Click your profile avatar in the top-right corner and select 'Settings'. In the Settings page, click the 'Integrations' tab in the left menu. Scroll to the bottom of the Integrations page to find the 'Developer' section. Your Personal API token appears here as a 40-character hexadecimal string.

Copy the token. Unlike some APIs, Todoist does not let you generate multiple tokens or revoke individual ones — this single token is tied to your account. If you ever need to invalidate it, you can regenerate it from the same page, but all existing integrations using the old token will stop working immediately.

Note that this token never expires unless you regenerate it. It authenticates all API requests as your personal Todoist account, giving access to all your projects, tasks, and labels.

**Expected result:** You have a 40-character Todoist API token copied and ready. The Todoist settings page confirms it under Settings → Integrations → Developer.

### 2. Configure the API Connector with Private Bearer token

In your Bubble app, open the Plugins tab and confirm the API Connector plugin (by Bubble) is installed. Click 'Add another API' and name it 'Todoist'.

Set the Authentication to 'Private key in header'. In the key name field, enter `Authorization`. In the value field, enter `Bearer YOUR_TOKEN_HERE` — replace YOUR_TOKEN_HERE with your actual 40-character Todoist API token. Tick the 'Private' checkbox. A Private header is stored encrypted on Bubble's servers and never included in browser-visible requests.

Set the base URL to `https://api.todoist.com/rest/v2`.

Add a shared header: Content-Type: application/json.

Bubble will now proxy all Todoist calls from its servers using your token. You do not need to configure CORS or create any proxy functions — Bubble's server-side execution handles this automatically.

```
{
  "api_name": "Todoist",
  "base_url": "https://api.todoist.com/rest/v2",
  "headers": [
    {
      "key": "Authorization",
      "value": "Bearer <private>",
      "private": true
    },
    {
      "key": "Content-Type",
      "value": "application/json",
      "private": false
    }
  ]
}
```

**Expected result:** The Todoist API group appears in the API Connector plugin with the Authorization header showing a Private lock icon. The base URL shows https://api.todoist.com/rest/v2.

### 3. Add and initialize the projects and tasks calls

Inside the Todoist API group, click 'Add another call'. Name it 'Get Projects'. Set method to GET and path to `/projects`. Click 'Initialize call'.

Todoist returns a bare JSON array: `[{"id": "123", "name": "My Project", ...}, ...]`. There is no wrapper key. Bubble initializes this correctly and detects it as a list. In the 'Use as' dropdown, select 'Data' and tick 'List'. If Bubble tries to set it as a single object, manually switch to List. The data type will be named 'Todoist Get Projects' — rename it to 'Todoist Project' in the Bubble data types panel for cleanliness.

Add a second call: 'Get Tasks'. Path: `/tasks`. Add two optional query parameters:
- project_id (text, dynamic, optional)
- filter (text, dynamic, optional — valid values include 'today', 'overdue', 'p1', '@label_name')

For initialization, use a filter value of 'today' as the default. Click 'Initialize call'. Todoist returns the task list including fields: id, content, description, project_id, priority (1-4), due (object with date field), labels (array), and is_completed.

In the 'Use as' dropdown, select 'Data' and tick 'List'. Bubble detects the nested `due` object — the `due.date` field contains the task due date as YYYY-MM-DD string.

```
GET https://api.todoist.com/rest/v2/projects
Authorization: Bearer <private>

GET https://api.todoist.com/rest/v2/tasks?filter=today
Authorization: Bearer <private>

// Example task object in response:
// {
//   "id": "2995104339",
//   "content": "Buy groceries",
//   "description": "",
//   "project_id": "2203306141",
//   "priority": 4,
//   "due": {
//     "date": "2026-07-10",
//     "is_recurring": false
//   },
//   "labels": [],
//   "is_completed": false
// }
```

**Expected result:** Two initialized calls in the Todoist API group: Get Projects returns your Todoist projects as a list, Get Tasks returns today's tasks as a list with all fields including the nested due object.

### 4. Add a task creation call and connect it to a Bubble form

Add a third call: 'Create Task'. Set method to POST and path to `/tasks`. Set body type to JSON. Add the following fields to the body:

- content (text, dynamic) — the task title; this is the ONLY required field
- project_id (text, dynamic, optional)
- due_date (text, dynamic, optional — format YYYY-MM-DD)
- priority (number, dynamic, optional — values 1, 2, 3, or 4)
- description (text, dynamic, optional)

For initialization, enter a real content value ('Test task from Bubble') and click 'Initialize call'. Todoist creates a real task and returns the task object. After initialization, delete the test task from Todoist.

On your Bubble page, add:
- A text input: 'Task content' (required)
- A dropdown: 'Project' with choices from Get Projects call
- A date picker: 'Due date'
- A dropdown: 'Priority' with choices: Normal (value: 1), Medium (value: 2), High (value: 3), Urgent (value: 4)
- A 'Create Task' button

Create a workflow triggered by the 'Create Task' button: call 'Todoist - Create Task' with content=Task content input's value, project_id=Project dropdown's value, due_date=Due date picker's value formatted as YYYY-MM-DD, priority=Priority dropdown's value.

In the On success event, reset the inputs and refresh the tasks repeating group by calling 'Display data → Get Tasks' again.

```
POST https://api.todoist.com/rest/v2/tasks
Authorization: Bearer <private>
Content-Type: application/json

{
  "content": "<task_content>",
  "project_id": "<project_id>",
  "due_date": "<due_date_YYYY-MM-DD>",
  "priority": <priority_1_to_4>
}
```

**Expected result:** Filling in the task form and clicking Create Task adds a new task to Todoist and reloads the tasks repeating group showing the newly created task.

### 5. Add task completion and build priority color-coding

To mark a Todoist task as complete, add a call: 'Close Task'. Method: POST. Path: `/tasks/{task_id}/close`. Add a path parameter: task_id (text, dynamic). The close endpoint accepts no body and returns a 204 No Content response on success.

In your tasks repeating group, add a checkbox element (or a checkmark icon button). Create a workflow: on click, call 'Todoist - Close Task' with task_id = current cell's task's id. On success, refresh the repeating group.

For priority color-coding, Todoist uses this scale:
- priority = 1 → Normal (show grey badge)
- priority = 2 → Medium (show blue badge)
- priority = 3 → High (show orange badge)
- priority = 4 → Urgent (show red badge)

In your repeating group cell, add a Group element sized to a small pill/badge. Add conditionals on this Group:
- When current cell's task's priority = 4 → Background color: #EF4444 (red)
- When current cell's task's priority = 3 → Background color: #F97316 (orange)
- When current cell's task's priority = 2 → Background color: #3B82F6 (blue)
- When current cell's task's priority = 1 → Background color: #9CA3AF (grey)

Add a text element inside the badge Group with corresponding conditional labels: 'Urgent', 'High', 'Medium', 'Normal'.

```
POST https://api.todoist.com/rest/v2/tasks/{task_id}/close
Authorization: Bearer <private>
// No body needed; returns 204 No Content on success

// Priority mapping (Todoist inverted scale):
// priority = 1 → Normal (lowest)
// priority = 2 → Medium
// priority = 3 → High
// priority = 4 → Urgent (highest)
```

**Expected result:** Clicking a task's checkbox calls the close endpoint and removes the task from the active tasks view. Priority badges display with color-coded labels (grey Normal, blue Medium, orange High, red Urgent).

### 6. Handle due dates and add a project-filter dropdown

Todoist due dates live inside a nested `due` object on each task. The `due.date` field is a YYYY-MM-DD string. When a task has no due date, the `due` field itself is null — not an empty object, but null. This means Bubble may show an error or blank text if you reference `task's due's date` without checking for null first.

Add a conditional in your due date text element: 'When current cell's task's due is empty, display No due date in grey text; otherwise, display the due's date value.'

For overdue detection: compare `due.date` (as a string) with today's date (formatted as YYYY-MM-DD). When due.date < today's date string, the task is overdue. Add a conditional on the row Group: 'When current cell's task's due is not empty AND current cell's task's due's date < Current date/time formatted as YYYY-MM-DD → Background color: light red (#FEE2E2)'. This gives visual overdue alerts.

For the projects filter, add a Dropdown above the repeating group. Populate it with the Get Projects call. Add an 'All Projects' option. When the dropdown value changes, update the repeating group data source: 'Get Tasks' with project_id = selected dropdown's value (leave blank for all projects).

RapidDev's team builds Todoist widgets embedded in larger Bubble productivity dashboards regularly — if you want to combine this with calendar views, Stripe billing, or team reporting, book a free scoping call at rapidevelopers.com/contact.

```
// Due date handling in Bubble conditional expressions:
// Check: 'current cell's task's due is empty'
// If not empty: 'current cell's task's due's date'
// Overdue check: compare date string with formatted current date

// Todoist filter parameter examples:
// filter=today          → tasks due today
// filter=overdue        → all overdue tasks
// filter=p1             → priority 4 (urgent) tasks
// filter=@work          → tasks with 'work' label
// filter=today | overdue → today and overdue combined
```

**Expected result:** Tasks with due dates display the date properly, tasks without due dates show 'No due date' in grey. Overdue tasks highlight in light red. The projects dropdown filters the task list to the selected project.

## Best practices

- Always mark the Todoist Bearer token as Private in Bubble API Connector — it authenticates your entire Todoist account and must never appear in browser network requests.
- Use Todoist's server-side filter parameter (today, overdue, p1, @label) to pre-filter tasks on the API side rather than loading all tasks and filtering in Bubble. This reduces data transfer and Workload Unit consumption.
- Handle null due dates explicitly with Bubble conditionals. Tasks without due dates return null for the due field — referencing due.date on a null value will break your repeating group display.
- Remember that Todoist priority is inverted: 1=Normal, 4=Urgent. Document this mapping clearly in any dropdown choices or conditional labels in your Bubble app.
- For task deletion, show a confirmation popup before calling DELETE /tasks/{id}. The Todoist API has no undo endpoint — deleted tasks cannot be recovered.
- Distinguish between closing a task (POST /tasks/{id}/close — marks complete, task is recoverable) and deleting it (DELETE /tasks/{id} — permanent). For most UI interactions, close is the safer default.
- Add Privacy Rules (Data tab → Privacy) if you store any Todoist task data in Bubble's database, restricting access to the owning user record.

## Use cases

### Personal task inbox panel embedded in a Bubble CRM

Add a collapsible Todoist task panel to a Bubble CRM's main dashboard. Team members see their overdue and today's tasks from Todoist alongside their CRM records. They can mark tasks complete and create new tasks directly from the panel. The filter parameter today or overdue drives smart server-side filtering without loading all tasks.

Prompt example:

```
Create a collapsible sidebar panel in a Bubble CRM page. Add a repeating group with data source from Todoist GET /tasks?filter=today. Inside each row, show the task content text, a priority color badge based on the priority field (4=red, 3=orange, 2=blue, 1=grey), and a checkbox button. On checkbox click, call POST https://api.todoist.com/rest/v2/tasks/{task_id}/close. Add a text input and create button at the bottom to POST new tasks.
```

### Project task board with priority sorting

Build a Bubble page that shows a Todoist project's tasks sorted by priority (urgent first) with color-coded badges. A projects dropdown lets users switch between their Todoist projects. An add-task form at the top creates new tasks in the selected project. Overdue tasks appear highlighted in red.

Prompt example:

```
Create a Bubble page with a projects dropdown populated by GET https://api.todoist.com/rest/v2/projects. When the selected project changes, reload the tasks repeating group with GET /tasks?project_id={selectedProject}. Sort by priority descending. Apply conditional background colors: priority=4 → red, priority=3 → orange, priority=2 → blue. Add a form with content input, due date picker, and priority dropdown to create new tasks via POST /tasks.
```

### Daily standup summary from Todoist

Create a Bubble page that generates a daily standup view: tasks completed yesterday, tasks due today, and overdue tasks. Use Todoist's filter parameter with overdue and today values to make three separate API calls. Display the results in three grouped sections, giving team members a quick standup report without opening Todoist.

Prompt example:

```
Create a Bubble page with three repeating groups stacked vertically. First group: GET /tasks?filter=overdue (label: Overdue, highlight red). Second group: GET /tasks?filter=today (label: Due Today). Third group: a completed tasks section using a recent completions query. Add a refresh button that re-runs all three calls. Display each task with content, project name, and due date.
```

## Troubleshooting

### Initialize call returns 401 Unauthorized

Cause: The Todoist API token is invalid, or the Authorization header value is not formatted correctly as 'Bearer TOKEN' with a space.

Solution: Go to Todoist Settings → Integrations → Developer and confirm your API token. In Bubble API Connector, check that the Authorization header value is exactly 'Bearer ' followed by the token with a single space. Copy-paste the token fresh from the Todoist settings page to rule out whitespace issues.

### Bubble initializes the task or project list as a single object instead of a list

Cause: Todoist returns bare JSON arrays without a wrapper key. Bubble sometimes misdetects these as single objects rather than lists during initialization.

Solution: After initializing the call, manually set 'Use as' to 'Data' and tick the 'List of...' checkbox in the API Connector initialization dialog. If the initialization completes with a single-object detection, re-initialize the call and explicitly confirm the List option.

### 'There was an issue setting up your call' error during initialization

Cause: Bubble requires a real successful response with JSON data to detect field types. This error appears if the call returns an error, if no tasks exist for the filter used, or if a required parameter is missing.

Solution: Use a broad filter like 'today' or leave the filter parameter empty (which returns all active tasks) during initialization to ensure there are real tasks to detect. Confirm your API token works by testing the call URL in your browser or with a tool like Postman before initializing in Bubble.

### The due date field shows blank or causes an error in the repeating group

Cause: Tasks without a due date return null for the due field (not an empty object). Trying to access due.date on a null value causes an error in Bubble.

Solution: Add a conditional in the due date text element: 'When current cell's task's due is empty, show No due date; otherwise show current cell's task's due's date'. The 'is empty' check in Bubble handles null values correctly.

### Priority badges show the wrong label — high-priority tasks appear as low priority

Cause: Todoist's priority scale is inverted from user intuition: 1=Normal (lowest), 4=Urgent (highest). A common mistake is mapping priority=1 as the highest priority.

Solution: Verify your priority conditionals use the correct mapping: priority=4 → Urgent (red), priority=3 → High (orange), priority=2 → Medium (blue), priority=1 → Normal (grey). The number 4 is the most urgent in Todoist.

### Close task call returns 404 Not Found

Cause: The task ID being passed to /tasks/{task_id}/close is incorrect or the task was already completed or deleted.

Solution: Confirm you are passing the task's id field (not its project_id or other identifier) as the path parameter. In the repeating group, the reference should be 'current cell's task's id'. Also verify the task exists and has not already been closed.

## Frequently asked questions

### Does Todoist's free tier work for this Bubble integration?

Yes. Todoist's REST API v2 is available on all plans including the free tier. The free tier supports 5 active projects and 5 collaborators — sufficient for building a personal productivity widget or a small-team task inbox. Paid plans (Pro $4/month, Business $6/user/month) add features like reminders, calendar sync, and more collaborators, but none of these are required for the core API integration.

### Why does Todoist return a bare array instead of an object with a data field?

Todoist's REST API v2 was designed with a minimal response format — list endpoints return bare JSON arrays like [{...}, {...}] instead of wrapping them in a parent object. This is valid JSON but differs from many other APIs. In Bubble, this means you must explicitly confirm 'Use as = Data (list)' during initialization, rather than relying on Bubble's auto-detection which may interpret the response as a single object.

### What is the difference between closing a task and deleting it in Todoist?

Closing a task (POST /tasks/{id}/close) marks it as completed. Completed tasks are kept in Todoist's history and can be un-completed via the Todoist app. Deleting a task (DELETE /tasks/{id}) permanently removes it with no recovery option. For most Bubble UI patterns, use the close endpoint — it is reversible. Only delete tasks when your workflow explicitly requires permanent removal, and always show a confirmation popup first.

### How do I display task priority correctly? My urgent tasks show as low priority.

Todoist's priority numbering is inverted from user intuition: priority=1 means Normal (lowest), priority=2 means Medium, priority=3 means High, and priority=4 means Urgent (highest). This is a well-known gotcha documented by Todoist. In Bubble conditionals, map priority=4 to 'Urgent' with red color, and priority=1 to 'Normal' with grey color. The number 4 represents the highest urgency.

### Can I use Todoist as a shared task list for multiple Bubble users?

With a single shared API token, all Bubble users would read from and write to the same Todoist account. For a truly multi-user scenario where each person sees their own Todoist tasks, you need each user's individual API token. Store tokens in a Bubble User data type field (with strict privacy rules) and pass the current user's token dynamically in API calls. Todoist also supports shared projects if multiple users are collaborators — all collaborators' tasks in a shared project are visible via any collaborator's token.

### How do I filter tasks by label in Bubble?

Use the Todoist filter parameter with the @ prefix for labels: filter=@work filters to tasks with the 'work' label. You can combine filters: filter=today & @work shows today's work-labeled tasks. Alternatively, use the GET /tasks?label_id={id} parameter with the label's ID fetched from GET /labels. The filter parameter approach is simpler for Bubble since it uses natural-language filter strings.

---

Source: https://www.rapidevelopers.com/bubble-integrations/todoist
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/todoist
