DEV Community

Cover image for Solved: Syncing Todoist Tasks with Microsoft To Do via API
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Syncing Todoist Tasks with Microsoft To Do via API

🚀 Executive Summary

TL;DR: This guide provides a Python script to automatically sync tasks from Todoist to Microsoft To Do, addressing the inefficiency of manual task duplication across platforms. It leverages the Todoist and Microsoft Graph APIs to create a unified task management experience for SysAdmins, Developers, and DevOps Engineers.

🎯 Key Takeaways

  • API Authentication requires a Todoist API Token and an Azure AD application registration for Microsoft Graph, providing Client ID, Client Secret, Tenant ID, and an initial refresh token for non-interactive access.
  • The Python script orchestrates the sync by fetching Todoist tasks, obtaining/refreshing Microsoft Graph access tokens, identifying or creating a designated Microsoft To Do list, and creating new tasks while preventing duplicates based on task titles.
  • For continuous synchronization, the script can be scheduled using tools like cron. Key considerations include handling API rate limits with exponential backoff, managing Microsoft Graph access token expiry via refresh tokens, and ensuring correct Azure AD application permissions (Tasks.ReadWrite, offline_access).

Syncing Todoist Tasks with Microsoft To Do via API

Introduction

In today’s fast-paced digital environment, managing tasks across various platforms is a common challenge for SysAdmins, Developers, and DevOps Engineers. While tools like Todoist excel in personal productivity and rapid task capture, Microsoft To Do often integrates seamlessly into organizational ecosystems, especially for those heavily invested in Microsoft 365. The friction of manually duplicating tasks, switching contexts, and the risk of overlooking critical items due to platform segregation can significantly impede efficiency.

Imagine a world where your quick “note to self” in Todoist automatically appears in your work-focused Microsoft To Do list, without a single manual copy-paste. This tutorial aims to bring that vision to life. We will walk you through the process of building a simple Python script to automatically sync your Todoist tasks with Microsoft To Do using their respective APIs, providing a unified task management experience and liberating you from tedious manual synchronization.

Prerequisites

Before we dive into the integration, ensure you have the following:

  • Python 3.x: Installed on your system. You can download it from the official Python website.
  • pip: Python’s package installer, usually included with Python 3.x.
  • Todoist Account: With an active subscription (or a free account if the features you need are covered).
  • Todoist API Token: This is your personal authentication key for the Todoist API.
  • Microsoft 365 Account: Required to use Microsoft To Do and its underlying Microsoft Graph API.
  • Azure AD Application Registration: You’ll need an Azure Active Directory application registered in your tenant with appropriate permissions for Microsoft Graph API. This will provide you with a Client ID, Client Secret, and Tenant ID.
  • Microsoft Graph API Refresh Token: For non-interactive access to Microsoft To Do, you will need an initial refresh token obtained via an OAuth2 authorization flow. This tutorial assumes you have a refresh token.

Step-by-Step Guide: Syncing Tasks via API

Step 1: Obtain API Credentials and Set Up Azure AD Application

1.1 Todoist API Token

Log in to your Todoist account. Navigate to “Settings” > “Integrations” > “Developer”. You will find your “API token” there. Copy this token; it’s essential for authenticating your requests to the Todoist API.

1.2 Microsoft Graph API Credentials (Azure AD App)

To interact with Microsoft To Do, you’ll use the Microsoft Graph API. This requires registering an application in Azure Active Directory:

  1. Go to the Azure portal and sign in.
  2. Navigate to “Azure Active Directory” > “App registrations” > “New registration”.
  3. Give your application a name (e.g., “TodoistToToDoSync”).
  4. For “Supported account types”, select “Accounts in any organizational directory (Any Azure AD directory – Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)”.
  5. For “Redirect URI”, select “Web” and enter http://localhost:8000 (or any valid redirect URI for development; for production, use a secure endpoint).
  6. Once registered, note down the “Application (client) ID” and “Directory (tenant) ID” from the application’s “Overview” page.
  7. Go to “Certificates & secrets” > “New client secret”. Add a description and select an expiry. Copy the “Value” of the secret immediately after creation, as it won’t be visible again.
  8. Go to “API permissions” > “Add a permission” > “Microsoft Graph” > “Delegated permissions”. Search for and add Tasks.ReadWrite and offline_access.
  9. Grant admin consent for the added permissions (if available and necessary for your tenant).

Note on Refresh Token: For non-interactive scripts, you need a refresh token. This is typically obtained by performing an initial OAuth2 authorization flow (e.g., using a tool like Postman, or a small web application to generate the initial token). Once you have a refresh token, you can use it to get new access tokens without user interaction. This tutorial assumes you have already obtained and will store your refresh token.

Step 2: Set Up Your Development Environment

Create a project directory and set up your Python environment.

mkdir todoist_todo_sync
cd todoist_todo_sync
python3 -m venv venv
source venv/bin/activate <!-- On Windows, use `venv\Scripts\activate` -->
Enter fullscreen mode Exit fullscreen mode

Install the necessary Python packages:

pip install requests python-dotenv
Enter fullscreen mode Exit fullscreen mode

Create a file named requirements.txt:

requests
python-dotenv
Enter fullscreen mode Exit fullscreen mode

Create a configuration file named config.env to store your API keys and secrets securely:

TODOIST_API_TOKEN="your_todoist_api_token_here"
MS_GRAPH_CLIENT_ID="your_azure_ad_client_id"
MS_GRAPH_CLIENT_SECRET="your_azure_ad_client_secret"
MS_GRAPH_TENANT_ID="your_azure_ad_tenant_id"
MS_GRAPH_REFRESH_TOKEN="your_microsoft_graph_refresh_token"
MS_TODO_LIST_NAME="Todoist Sync" <!-- The name of the To Do list to sync to -->
Enter fullscreen mode Exit fullscreen mode

Replace the placeholder values with your actual credentials.

Step 3: Develop the Sync Script

Create a Python file named sync_script.py. This script will handle authentication, task retrieval, and task synchronization.

3.1 Authenticate with Microsoft Graph API

First, we need a way to obtain a fresh access token for Microsoft Graph using our refresh token.

import os
import requests
from dotenv import load_dotenv

load_dotenv('config.env')

# Microsoft Graph API Configuration
MS_GRAPH_CLIENT_ID = os.getenv("MS_GRAPH_CLIENT_ID")
MS_GRAPH_CLIENT_SECRET = os.getenv("MS_GRAPH_CLIENT_SECRET")
MS_GRAPH_TENANT_ID = os.getenv("MS_GRAPH_TENANT_ID")
MS_GRAPH_REFRESH_TOKEN = os.getenv("MS_GRAPH_REFRESH_TOKEN")

def get_ms_graph_access_token():
    url = f"https://login.microsoftonline.com/{MS_GRAPH_TENANT_ID}/oauth2/v2.0/token"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {
        "client_id": MS_GRAPH_CLIENT_ID,
        "client_secret": MS_GRAPH_CLIENT_SECRET,
        "refresh_token": MS_GRAPH_REFRESH_TOKEN,
        "grant_type": "refresh_token",
        "scope": "Tasks.ReadWrite offline_access User.Read" # User.Read is often implicitly needed
    }
    try:
        response = requests.post(url, headers=headers, data=data)
        response.raise_for_status() # Raise an exception for HTTP errors
        token_data = response.json()
        print("Successfully obtained Microsoft Graph access token.")
        return token_data.get("access_token"), token_data.get("refresh_token")
    except requests.exceptions.RequestException as e:
        print(f"Error getting MS Graph access token: {e}")
        print(f"Response: {response.text if 'response' in locals() else 'No response'}")
        return None, None

# Update the refresh token if a new one is returned (good practice for long-lived tokens)
def update_refresh_token(new_refresh_token):
    if new_refresh_token and new_refresh_token != MS_GRAPH_REFRESH_TOKEN:
        print("New refresh token received. Updating config.env...")
        with open('config.env', 'r') as f:
            lines = f.readlines()
        with open('config.env', 'w') as f:
            for line in lines:
                if line.startswith('MS_GRAPH_REFRESH_TOKEN='):
                    f.write(f'MS_GRAPH_REFRESH_TOKEN="{new_refresh_token}"\n')
                else:
                    f.write(line)
        global MS_GRAPH_REFRESH_TOKEN
        MS_GRAPH_REFRESH_TOKEN = new_refresh_token
        print("Refresh token updated.")

# In a real scenario, you'd call update_refresh_token
# For simplicity, we'll just get the token for now.
# access_token, new_refresh_token = get_ms_graph_access_token()
# if access_token:
#    update_refresh_token(new_refresh_token)
Enter fullscreen mode Exit fullscreen mode

Logic Explanation: This function uses your client ID, client secret, and existing refresh token to request a new access token from Microsoft’s OAuth 2.0 endpoint. Access tokens are short-lived, so using a refresh token is crucial for long-running scripts. It also handles updating the refresh token in your config.env if a new one is issued, ensuring continued access.

3.2 Fetch Todoist Tasks

Next, we’ll retrieve tasks from your Todoist inbox or a specific project.

# Todoist API Configuration
TODOIST_API_TOKEN = os.getenv("TODOIST_API_TOKEN")

def get_todoist_tasks():
    url = "https://api.todoist.com/rest/v2/tasks"
    headers = {
        "Authorization": f"Bearer {TODOIST_API_TOKEN}"
    }
    try:
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        tasks = response.json()
        print(f"Fetched {len(tasks)} tasks from Todoist.")
        return tasks
    except requests.exceptions.RequestException as e:
        print(f"Error fetching Todoist tasks: {e}")
        return []
Enter fullscreen mode Exit fullscreen mode

Logic Explanation: This function makes a GET request to the Todoist API’s tasks endpoint, authenticating with your personal API token. It retrieves all active tasks associated with your account.

3.3 Sync Tasks to Microsoft To Do

Now, let’s put it all together. We’ll get your To Do list ID, fetch existing tasks from To Do, and then iterate through Todoist tasks to create new ones in Microsoft To Do if they don’t already exist.

MS_TODO_LIST_NAME = os.getenv("MS_TODO_LIST_NAME")

def get_ms_todo_list_id(access_token):
    url = "https://graph.microsoft.com/v1.0/me/todo/lists"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    try:
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        lists = response.json().get("value", [])
        for todo_list in lists:
            if todo_list.get("displayName") == MS_TODO_LIST_NAME:
                print(f"Found Microsoft To Do list: '{MS_TODO_LIST_NAME}' with ID: {todo_list['id']}")
                return todo_list["id"]

        # If list not found, create it
        print(f"Microsoft To Do list '{MS_TODO_LIST_NAME}' not found. Creating it...")
        create_url = "https://graph.microsoft.com/v1.0/me/todo/lists"
        create_data = {"displayName": MS_TODO_LIST_NAME}
        create_response = requests.post(create_url, headers=headers, json=create_data)
        create_response.raise_for_status()
        new_list = create_response.json()
        print(f"Created new list: '{new_list['displayName']}' with ID: {new_list['id']}")
        return new_list["id"]

    except requests.exceptions.RequestException as e:
        print(f"Error getting or creating MS To Do list: {e}")
        return None

def get_ms_todo_tasks(access_token, todo_list_id):
    url = f"https://graph.microsoft.com/v1.0/me/todo/lists/{todo_list_id}/tasks"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    try:
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        tasks = response.json().get("value", [])
        print(f"Fetched {len(tasks)} tasks from Microsoft To Do list '{MS_TODO_LIST_NAME}'.")
        return tasks
    except requests.exceptions.RequestException as e:
        print(f"Error fetching MS To Do tasks: {e}")
        return []

def create_ms_todo_task(access_token, todo_list_id, task_title, due_date=None):
    url = f"https://graph.microsoft.com/v1.0/me/todo/lists/{todo_list_id}/tasks"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    task_data = {
        "title": task_title
    }
    if due_date:
        task_data["dueDateTime"] = {
            "dateTime": due_date, # Format: YYYY-MM-DDTHH:MM:SSZ (or YYYY-MM-DD)
            "timeZone": "UTC" # Or your local timezone
        }
    try:
        response = requests.post(url, headers=headers, json=task_data)
        response.raise_for_status()
        print(f"Created task in MS To Do: '{task_title}'")
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error creating task in MS To Do ('{task_title}'): {e}")
        print(f"Response: {response.text if 'response' in locals() else 'No response'}")
        return None

def main():
    ms_access_token, new_refresh_token = get_ms_graph_access_token()
    if not ms_access_token:
        print("Failed to get Microsoft Graph access token. Exiting.")
        return

    update_refresh_token(new_refresh_token) # Update refresh token if a new one was issued

    ms_todo_list_id = get_ms_todo_list_id(ms_access_token)
    if not ms_todo_list_id:
        print("Failed to get or create Microsoft To Do list. Exiting.")
        return

    todoist_tasks = get_todoist_tasks()
    ms_todo_tasks = get_ms_todo_tasks(ms_access_token, ms_todo_list_id)

    # Simple mechanism to avoid duplicates: use task title for comparison
    ms_todo_task_titles = {task["title"] for task in ms_todo_tasks}

    synced_count = 0
    for task in todoist_tasks:
        task_title = task["content"]
        if task_title not in ms_todo_task_titles:
            due_date = None
            if task.get("due") and task["due"].get("date"):
                # Todoist date format is YYYY-MM-DD, suitable for Graph API
                due_date = task["due"]["date"] 
                # If you need time, Todoist has 'datetime' and 'string' options
                # For simplicity, using just date here.
            created_task = create_ms_todo_task(ms_access_token, ms_todo_list_id, task_title, due_date)
            if created_task:
                synced_count += 1
            else:
                print(f"Warning: Could not sync Todoist task: '{task_title}'")
        else:
            print(f"Task already exists in MS To Do: '{task_title}'. Skipping.")

    print(f"Synchronization complete. {synced_count} tasks synced from Todoist to Microsoft To Do.")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Logic Explanation:

  1. The main function orchestrates the process: it first obtains a Microsoft Graph access token.
  2. It then identifies (or creates if not present) the target Microsoft To Do list named “Todoist Sync”.
  3. It fetches all active tasks from Todoist.
  4. It retrieves existing tasks from the designated Microsoft To Do list.
  5. To prevent duplicates, it compares task titles. If a Todoist task’s title doesn’t exist in the Microsoft To Do list, a new task is created in Microsoft To Do.
  6. Basic due date handling is included, translating Todoist’s due date to the Graph API’s expected format.

Step 4: Execute and Schedule the Script

To run your script, simply execute it from your terminal:

python3 sync_script.py
Enter fullscreen mode Exit fullscreen mode

For continuous synchronization, you can schedule this script to run periodically. On Linux/Unix systems, you can use a cron job:

crontab -e
Enter fullscreen mode Exit fullscreen mode

Add a line like the following to run the script every 30 minutes:

*/30 * * * * /usr/bin/python3 /home/user/todoist_todo_sync/sync_script.py >> /tmp/logs/todoist_sync.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Ensure the paths to your Python executable and script are correct. Create the /tmp/logs/ directory if it doesn’t exist.

Common Pitfalls

  1. API Rate Limits: Both Todoist and Microsoft Graph APIs have rate limits. If you’re syncing a large number of tasks frequently, you might hit these limits. Implement exponential backoff for retries to handle this gracefully.
  2. Authentication Token Expiry: Microsoft Graph access tokens are short-lived (typically 1 hour). The provided script uses a refresh token to obtain new access tokens. Ensure your refresh token is properly stored and updated in config.env. If your initial refresh token expires (e.g., after 90 days of inactivity), you’ll need to re-authenticate interactively to get a new one.
  3. Permission Issues (Microsoft Graph): Double-check that your Azure AD application has the necessary delegated permissions (Tasks.ReadWrite and offline_access). If admin consent is required for your tenant, ensure it has been granted.
  4. Duplicate Tasks: The current script uses a simple title-based check for duplication. For more robust syncing, you might consider storing a mapping of Todoist task IDs to Microsoft To Do task IDs in a small local database or a custom extended property on the To Do tasks themselves. This would allow for updates and deletions, not just creations.

Conclusion

By leveraging the power of APIs, we’ve successfully built a practical solution to bridge the gap between Todoist and Microsoft To Do. This automation not only saves valuable time previously spent on manual data entry but also enhances your productivity by consolidating your task management view. As a SysAdmin, Developer, or DevOps Engineer, such integrations are key to building efficient workflows and reducing operational overhead.

From here, you can expand this script significantly. Consider implementing bi-directional synchronization, adding support for task completion status, priorities, or labels. You might also explore deploying this as a serverless function (e.g., Azure Functions or AWS Lambda) or as a containerized application for more robust and scalable operation. The foundation is laid; the possibilities are endless.


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)