todoist-api
Todoist API Integration Skill
Master the Todoist API for task management automation, including projects, tasks, labels, filters, webhooks, and Python SDK patterns. This skill covers REST API v2, Sync API v9, and integration patterns.
When to Use This Skill
USE Todoist API when:
- Automating task creation from external systems
- Building integrations with other productivity tools
- Creating custom task dashboards or reports
- Implementing GTD workflows programmatically
- Syncing tasks with calendar applications
- Building CLI tools for task management
- Automating recurring task patterns
- Integrating with CI/CD for project tracking
DON'T USE Todoist API when:
- Need complex project management (use Jira, Asana)
- Require database-style queries (use Notion API)
- Need real-time collaboration on tasks (use Linear)
- Building for enterprise with SSO requirements
- Need Gantt charts or resource management
Prerequisites
API Authentication
# Get your API token from:
# https://todoist.com/app/settings/integrations/developer
# Set environment variable
export TODOIST_API_KEY="your-api-token-here"
# Verify authentication
curl -s -X GET "https://api.todoist.com/rest/v2/projects" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq '.[0]'
Python SDK Installation
# Install official Python SDK
pip install todoist-api-python
# Or with uv
uv pip install todoist-api-python
# For sync API features
pip install todoist-api-python requests
Verify Setup
from todoist_api_python import TodoistAPI
api = TodoistAPI("your-api-token")
# Test connection
try:
projects = api.get_projects()
print(f"Connected! Found {len(projects)} projects")
except Exception as e:
print(f"Connection failed: {e}")
Core Capabilities
1. Projects Management
REST API - Projects:
# List all projects
curl -s -X GET "https://api.todoist.com/rest/v2/projects" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Get specific project
curl -s -X GET "https://api.todoist.com/rest/v2/projects/PROJECT_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Create project
curl -s -X POST "https://api.todoist.com/rest/v2/projects" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Work Tasks",
"color": "blue",
"is_favorite": true,
"view_style": "list"
}' | jq
# Create sub-project
curl -s -X POST "https://api.todoist.com/rest/v2/projects" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 Goals",
"parent_id": "PARENT_PROJECT_ID",
"color": "green"
}' | jq
# Update project
curl -s -X POST "https://api.todoist.com/rest/v2/projects/PROJECT_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Work Tasks - Updated",
"color": "red"
}' | jq
# Delete project
curl -s -X DELETE "https://api.todoist.com/rest/v2/projects/PROJECT_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY"
# Get project collaborators
curl -s -X GET "https://api.todoist.com/rest/v2/projects/PROJECT_ID/collaborators" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
Python SDK - Projects:
from todoist_api_python import TodoistAPI
import os
api = TodoistAPI(os.environ["TODOIST_API_KEY"])
# List all projects
projects = api.get_projects()
for project in projects:
print(f"{project.name} (ID: {project.id})")
# Get specific project
project = api.get_project(project_id="2345678901")
print(f"Project: {project.name}, Color: {project.color}")
# Create project
new_project = api.add_project(
name="New Project",
color="blue",
is_favorite=True,
view_style="board" # "list" or "board"
)
print(f"Created: {new_project.name} (ID: {new_project.id})")
# Create sub-project
sub_project = api.add_project(
name="Sub Project",
parent_id="2345678901",
color="green"
)
# Update project
updated = api.update_project(
project_id="2345678901",
name="Updated Name",
color="red"
)
# Delete project
api.delete_project(project_id="2345678901")
# Get project sections
sections = api.get_sections(project_id="2345678901")
for section in sections:
print(f" Section: {section.name}")
2. Tasks Management
REST API - Tasks:
# List all tasks
curl -s -X GET "https://api.todoist.com/rest/v2/tasks" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Get tasks with filter
curl -s -X GET "https://api.todoist.com/rest/v2/tasks?filter=today" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Get tasks for specific project
curl -s -X GET "https://api.todoist.com/rest/v2/tasks?project_id=PROJECT_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Get single task
curl -s -X GET "https://api.todoist.com/rest/v2/tasks/TASK_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Create task with all options
curl -s -X POST "https://api.todoist.com/rest/v2/tasks" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Complete project report",
"description": "Include Q4 metrics and projections",
"project_id": "PROJECT_ID",
"section_id": "SECTION_ID",
"parent_id": null,
"order": 1,
"labels": ["work", "urgent"],
"priority": 4,
"due_string": "tomorrow at 5pm",
"due_lang": "en",
"assignee_id": null
}' | jq
# Create task with natural language due date
curl -s -X POST "https://api.todoist.com/rest/v2/tasks" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Weekly review",
"due_string": "every friday at 4pm"
}' | jq
# Update task
curl -s -X POST "https://api.todoist.com/rest/v2/tasks/TASK_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Updated task content",
"priority": 3,
"due_string": "next monday"
}' | jq
# Complete task
curl -s -X POST "https://api.todoist.com/rest/v2/tasks/TASK_ID/close" \
-H "Authorization: Bearer $TODOIST_API_KEY"
# Reopen task
curl -s -X POST "https://api.todoist.com/rest/v2/tasks/TASK_ID/reopen" \
-H "Authorization: Bearer $TODOIST_API_KEY"
# Delete task
curl -s -X DELETE "https://api.todoist.com/rest/v2/tasks/TASK_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY"
Python SDK - Tasks:
from todoist_api_python import TodoistAPI
from datetime import datetime, timedelta
import os
api = TodoistAPI(os.environ["TODOIST_API_KEY"])
# Get all tasks
tasks = api.get_tasks()
for task in tasks:
due = task.due.string if task.due else "No due date"
print(f"- [{task.priority}] {task.content} (Due: {due})")
# Get tasks with filter
today_tasks = api.get_tasks(filter="today")
overdue_tasks = api.get_tasks(filter="overdue")
high_priority = api.get_tasks(filter="p1 | p2")
# Get tasks for project
project_tasks = api.get_tasks(project_id="2345678901")
# Create task
new_task = api.add_task(
content="Review pull requests",
description="Check all open PRs in main repo",
project_id="2345678901",
section_id="3456789012",
labels=["work", "development"],
priority=4, # 1=normal, 2=medium, 3=high, 4=urgent
due_string="tomorrow at 10am",
due_lang="en"
)
print(f"Created task: {new_task.id}")
# Create sub-task
sub_task = api.add_task(
content="Review frontend PR #123",
parent_id=new_task.id,
priority=3
)
# Create recurring task
recurring_task = api.add_task(
content="Weekly team standup",
due_string="every monday at 9am"
)
# Update task
updated_task = api.update_task(
task_id=new_task.id,
content="Review all pull requests",
priority=4,
due_string="today at 5pm"
)
# Complete task
api.close_task(task_id=new_task.id)
# Reopen task
api.reopen_task(task_id=new_task.id)
# Delete task
api.delete_task(task_id=new_task.id)
# Move task to different project
api.update_task(
task_id="1234567890",
project_id="NEW_PROJECT_ID"
)
3. Labels Management
REST API - Labels:
# List all labels
curl -s -X GET "https://api.todoist.com/rest/v2/labels" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Create label
curl -s -X POST "https://api.todoist.com/rest/v2/labels" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "urgent",
"color": "red",
"order": 1,
"is_favorite": true
}' | jq
# Update label
curl -s -X POST "https://api.todoist.com/rest/v2/labels/LABEL_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "high-priority",
"color": "orange"
}' | jq
# Delete label
curl -s -X DELETE "https://api.todoist.com/rest/v2/labels/LABEL_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY"
Python SDK - Labels:
from todoist_api_python import TodoistAPI
api = TodoistAPI(os.environ["TODOIST_API_KEY"])
# List all labels
labels = api.get_labels()
for label in labels:
print(f"@{label.name} (color: {label.color})")
# Create label
new_label = api.add_label(
name="review",
color="blue",
order=1,
is_favorite=True
)
# Update label
api.update_label(
label_id=new_label.id,
name="code-review",
color="green"
)
# Delete label
api.delete_label(label_id=new_label.id)
# Get tasks with specific label
review_tasks = api.get_tasks(filter="@code-review")
4. Sections Management
REST API - Sections:
# List sections for project
curl -s -X GET "https://api.todoist.com/rest/v2/sections?project_id=PROJECT_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Create section
curl -s -X POST "https://api.todoist.com/rest/v2/sections" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "In Progress",
"project_id": "PROJECT_ID",
"order": 2
}' | jq
# Update section
curl -s -X POST "https://api.todoist.com/rest/v2/sections/SECTION_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Currently Working"
}' | jq
# Delete section
curl -s -X DELETE "https://api.todoist.com/rest/v2/sections/SECTION_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY"
Python SDK - Sections:
# Create Kanban-style sections
sections = ["Backlog", "To Do", "In Progress", "Review", "Done"]
for i, section_name in enumerate(sections):
api.add_section(
name=section_name,
project_id="2345678901",
order=i
)
# Get sections
sections = api.get_sections(project_id="2345678901")
for section in sections:
print(f"Section: {section.name} (ID: {section.id})")
# Move task to section
api.update_task(
task_id="1234567890",
section_id="IN_PROGRESS_SECTION_ID"
)
5. Comments Management
REST API - Comments:
# Get comments for task
curl -s -X GET "https://api.todoist.com/rest/v2/comments?task_id=TASK_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Get comments for project
curl -s -X GET "https://api.todoist.com/rest/v2/comments?project_id=PROJECT_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" | jq
# Add comment to task
curl -s -X POST "https://api.todoist.com/rest/v2/comments" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_ID",
"content": "This is a comment on the task"
}' | jq
# Add comment with attachment
curl -s -X POST "https://api.todoist.com/rest/v2/comments" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"task_id": "TASK_ID",
"content": "See attached file",
"attachment": {
"file_name": "report.pdf",
"file_type": "application/pdf",
"file_url": "https://example.com/report.pdf"
}
}' | jq
# Update comment
curl -s -X POST "https://api.todoist.com/rest/v2/comments/COMMENT_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Updated comment content"
}' | jq
# Delete comment
curl -s -X DELETE "https://api.todoist.com/rest/v2/comments/COMMENT_ID" \
-H "Authorization: Bearer $TODOIST_API_KEY"
Python SDK - Comments:
# Get comments for task
comments = api.get_comments(task_id="1234567890")
for comment in comments:
print(f" {comment.posted_at}: {comment.content}")
# Add comment
new_comment = api.add_comment(
task_id="1234567890",
content="Added some notes about this task"
)
# Update comment
api.update_comment(
comment_id=new_comment.id,
content="Updated notes"
)
# Delete comment
api.delete_comment(comment_id=new_comment.id)
6. Filters and Queries
Filter Syntax:
# Date filters
"today" # Due today
"tomorrow" # Due tomorrow
"overdue" # Past due date
"next 7 days" # Due in next week
"no date" # No due date set
"Jan 15" # Specific date
"before: Jan 20" # Before date
"after: Jan 10" # After date
# Priority filters
"p1" # Priority 1 (urgent)
"p2" # Priority 2 (high)
"p3" # Priority 3 (medium)
"p4" # Priority 4 (normal)
"(p1 | p2)" # Priority 1 OR 2
# Label filters
"@work" # Has label "work"
"@work & @urgent" # Has both labels
"@work | @personal" # Has either label
"!@work" # Does NOT have label
# Project filters
"#Work" # In project "Work"
"##Work" # In project "Work" and sub-projects
"#Work & #Q1" # In both projects (intersection)
# Search filters
"search: meeting" # Content contains "meeting"
# Assignee filters
"assigned to: me" # Assigned to current user
"assigned to: John" # Assigned to John
"assigned by: me" # Assigned by current user
# Combined filters
"today & @work" # Due today with work label
"(today | overdue) & p1" # Today or overdue AND priority 1
"#Work & !@done" # In Work project without done label
Python Filter Examples:
# Get tasks with various filters
today_work = api.get_tasks(filter="today & @work")
urgent = api.get_tasks(filter="(p1 | p2) & (today | overdue)")
project_pending = api.get_tasks(filter="#ProjectAlpha & !@completed")
this_week = api.get_tasks(filter="next 7 days")
no_date = api.get_tasks(filter="no date & @inbox")
# Complex filter
complex_filter = api.get_tasks(
filter="(today | tomorrow) & (p1 | p2) & (#Work | #Personal) & !@waiting"
)
7. Sync API
Sync API Basics:
# Initial sync (full read)
curl -s -X POST "https://api.todoist.com/sync/v9/sync" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "sync_token=*&resource_types=[\"all\"]" | jq
# Incremental sync (with sync token)
SYNC_TOKEN="your-sync-token-from-previous-response"
curl -s -X POST "https://api.todoist.com/sync/v9/sync" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "sync_token=$SYNC_TOKEN&resource_types=[\"items\",\"projects\"]" | jq
# Batch operations with commands
curl -s -X POST "https://api.todoist.com/sync/v9/sync" \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"commands": [
{
"type": "item_add",
"temp_id": "temp1",
"uuid": "unique-uuid-1",
"args": {
"content": "Task 1",
"project_id": "PROJECT_ID"
}
},
{
"type": "item_add",
"temp_id": "temp2",
"uuid": "unique-uuid-2",
"args": {
"content": "Task 2",
"project_id": "PROJECT_ID"
}
}
]
}' | jq
Python Sync Operations:
import requests
import json
import uuid
TODOIST_API_KEY = os.environ["TODOIST_API_KEY"]
SYNC_URL = "https://api.todoist.com/sync/v9/sync"
def sync_read(sync_token="*", resource_types=None):
"""Read data using Sync API"""
if resource_types is None:
resource_types = ["all"]
response = requests.post(
SYNC_URL,
headers={"Authorization": f"Bearer {TODOIST_API_KEY}"},
data={
"sync_token": sync_token,
"resource_types": json.dumps(resource_types)
}
)
return response.json()
def batch_add_tasks(tasks):
"""Add multiple tasks in one request"""
commands = []
for task in tasks:
commands.append({
"type": "item_add",
"temp_id": f"temp_{uuid.uuid4().hex[:8]}",
"uuid": str(uuid.uuid4()),
"args": task
})
response = requests.post(
SYNC_URL,
headers={
"Authorization": f"Bearer {TODOIST_API_KEY}",
"Content-Type": "application/json"
},
json={"commands": commands}
)
return response.json()
# Example: Add multiple tasks at once
tasks_to_add = [
{"content": "Task 1", "project_id": "2345678901", "priority": 4},
{"content": "Task 2", "project_id": "2345678901", "priority": 3},
{"content": "Task 3", "project_id": "2345678901", "priority": 2},
]
result = batch_add_tasks(tasks_to_add)
print(f"Added {len(tasks_to_add)} tasks")
8. Webhooks
Webhook Setup:
from flask import Flask, request, jsonify
import hashlib
import hmac
import os
app = Flask(__name__)
TODOIST_CLIENT_SECRET = os.environ["TODOIST_CLIENT_SECRET"]
def verify_webhook(payload, signature):
"""Verify webhook signature"""
computed = hmac.new(
TODOIST_CLIENT_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
@app.route("/webhook/todoist", methods=["POST"])
def todoist_webhook():
# Verify signature
signature = request.headers.get("X-Todoist-Hmac-SHA256", "")
if not verify_webhook(request.data, signature):
return jsonify({"error": "Invalid signature"}), 401
# Parse event
event = request.json
event_name = event.get("event_name")
event_data = event.get("event_data", {})
print(f"Received event: {event_name}")
# Handle different event types
if event_name == "item:added":
handle_task_added(event_data)
elif event_name == "item:completed":
handle_task_completed(event_data)
elif event_name == "item:updated":
handle_task_updated(event_data)
elif event_name == "item:deleted":
handle_task_deleted(event_data)
elif event_name == "project:added":
handle_project_added(event_data)
return jsonify({"status": "ok"})
def handle_task_added(data):
print(f"New task: {data.get('content')}")
# Add your logic here
def handle_task_completed(data):
print(f"Task completed: {data.get('content')}")
# Add your logic here
def handle_task_updated(data):
print(f"Task updated: {data.get('content')}")
# Add your logic here
def handle_task_deleted(data):
print(f"Task deleted: {data.get('id')}")
# Add your logic here
def handle_project_added(data):
print(f"New project: {data.get('name')}")
# Add your logic here
if __name__ == "__main__":
app.run(port=5000)
Webhook Events:
# Available webhook events
WEBHOOK_EVENTS = {
# Task events
"item:added": "Task created",
"item:updated": "Task updated",
"item:deleted": "Task deleted",
"item:completed": "Task completed",
"item:uncompleted": "Task reopened",
# Project events
"project:added": "Project created",
"project:updated": "Project updated",
"project:deleted": "Project deleted",
"project:archived": "Project archived",
"project:unarchived": "Project unarchived",
# Note/Comment events
"note:added": "Comment added",
"note:updated": "Comment updated",
"note:deleted": "Comment deleted",
# Label events
"label:added": "Label created",
"label:updated": "Label updated",
"label:deleted": "Label deleted",
# Section events
"section:added": "Section created",
"section:updated": "Section updated",
"section:deleted": "Section deleted",
# Reminder events
"reminder:fired": "Reminder triggered",
}
Complete Examples
Example 1: GTD Weekly Review Automation
#!/usr/bin/env python3
"""gtd_weekly_review.py - Automate GTD weekly review"""
from todoist_api_python import TodoistAPI
from datetime import datetime, timedelta
import os
api = TodoistAPI(os.environ["TODOIST_API_KEY"])
def weekly_review():
"""Perform GTD weekly review"""
report = {
"completed_this_week": [],
"overdue": [],
"upcoming": [],
"no_due_date": [],
"waiting_for": []
}
# Get all tasks
all_tasks = api.get_tasks()
# Get completed tasks (last 7 days)
# Note: REST API doesn't provide completed tasks easily
# Would need Sync API for completed items
# Categorize active tasks
today = datetime.now().date()
week_ahead = today + timedelta(days=7)
for task in all_tasks:
# Check for waiting label
if "waiting" in [l.lower() for l in task.labels]:
report["waiting_for"].append(task)
continue
if task.due:
due_date = datetime.strptime(task.due.date, "%Y-%m-%d").date()
if due_date < today:
report["overdue"].append(task)
elif due_date <= week_ahead:
report["upcoming"].append(task)
else:
report["no_due_date"].append(task)
return report
def generate_review_report(report):
"""Generate markdown review report"""
today = datetime.now().strftime("%Y-%m-%d")
content = f"""# Weekly Review - {today}
## Overdue Tasks ({len(report['overdue'])})
"""
for task in report["overdue"]:
content += f"- [ ] {task.content} (Due: {task.due.string})\n"
content += f"""
## Upcoming This Week ({len(report['upcoming'])})
"""
for task in sorted(report["upcoming"], key=lambda t: t.due.date):
content += f"- [ ] {task.content} (Due: {task.due.string})\n"
content += f"""
## Waiting For ({len(report['waiting_for'])})
"""
for task in report["waiting_for"]:
content += f"- {task.content}\n"
content += f"""
## Tasks Without Due Date ({len(report['no_due_date'])})
"""
for task in report["no_due_date"][:10]: # Limit to 10
content += f"- {task.content}\n"
if len(report["no_due_date"]) > 10:
content += f"- ... and {len(report['no_due_date']) - 10} more\n"
return content
def create_review_task():
"""Create next week's review task"""
api.add_task(
content="Weekly Review",
due_string="next sunday at 6pm",
labels=["review"],
priority=4
)
if __name__ == "__main__":
report = weekly_review()
markdown_report = generate_review_report(report)
# Save report
filename = f"weekly_review_{datetime.now().strftime('%Y-%m-%d')}.md"
with open(filename, "w") as f:
f.write(markdown_report)
print(f"Report saved: {filename}")
print(f"\nSummary:")
print(f" Overdue: {len(report['overdue'])}")
print(f" Upcoming: {len(report['upcoming'])}")
print(f" Waiting: {len(report['waiting_for'])}")
print(f" No date: {len(report['no_due_date'])}")
# Create next review task
create_review_task()
Example 2: Project Template Creator
#!/usr/bin/env python3
"""project_template.py - Create projects from templates"""
from todoist_api_python import TodoistAPI
import os
import json
api = TodoistAPI(os.environ["TODOIST_API_KEY"])
# Define project templates
TEMPLATES = {
"sprint": {
"color": "blue",
"sections": ["Backlog", "To Do", "In Progress", "Review", "Done"],
"tasks": [
{"content": "Sprint Planning", "section": "To Do", "priority": 4},
{"content": "Daily Standups", "section": "To Do", "due_string": "every weekday at 9am"},
{"content": "Sprint Review", "section": "Backlog", "priority": 3},
{"content": "Sprint Retrospective", "section": "Backlog", "priority": 3},
]
},
"content_piece": {
"color": "green",
"sections": ["Research", "Writing", "Editing", "Publishing"],
"tasks": [
{"content": "Research topic", "section": "Research", "priority": 3},
{"content": "Create outline", "section": "Research", "priority": 3},
{"content": "Write first draft", "section": "Writing", "priority": 4},
{"content": "Self-review and edit", "section": "Editing", "priority": 3},
{"content": "Final review", "section": "Editing", "priority": 3},
{"content": "Publish", "section": "Publishing", "priority": 4},
]
},
"event": {
"color": "yellow",
"sections": ["Planning", "Preparation", "Execution", "Follow-up"],
"tasks": [
{"content": "Define event goals", "section": "Planning", "priority": 4},
{"content": "Create guest list", "section": "Planning", "priority": 3},
{"content": "Book venue", "section": "Preparation", "priority": 4},
{"content": "Send invitations", "section": "Preparation", "priority": 4},
{"content": "Confirm RSVPs", "section": "Preparation", "priority": 3},
{"content": "Event execution", "section": "Execution", "priority": 4},
{"content": "Send thank you notes", "section": "Follow-up", "priority": 3},
{"content": "Event retrospective", "section": "Follow-up", "priority": 2},
]
}
}
def create_project_from_template(name: str, template_name: str, due_date: str = None):
"""Create a project from template"""
if template_name not in TEMPLATES:
raise ValueError(f"Unknown template: {template_name}")
template = TEMPLATES[template_name]
# Create project
project = api.add_project(
name=name,
color=template["color"],
view_style="board"
)
print(f"Created project: {project.name} (ID: {project.id})")
# Create sections
section_ids = {}
for i, section_name in enumerate(template["sections"]):
section = api.add_section(
name=section_name,
project_id=project.id,
order=i
)
section_ids[section_name] = section.id
print(f" Created section: {section_name}")
# Create tasks
for task_def in template["tasks"]:
task_args = {
"content": task_def["content"],
"project_id": project.id,
"section_id": section_ids[task_def["section"]],
"priority": task_def.get("priority", 1)
}
if "due_string" in task_def:
task_args["due_string"] = task_def["due_string"]
elif due_date:
task_args["due_string"] = due_date
task = api.add_task(**task_args)
print(f" Created task: {task.content}")
return project
def list_templates():
"""List available templates"""
print("Available templates:")
for name, template in TEMPLATES.items():
print(f"\n {name}:")
print(f" Sections: {', '.join(template['sections'])}")
print(f" Tasks: {len(template['tasks'])}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Create Todoist project from template")
parser.add_argument("--list", action="store_true", help="List available templates")
parser.add_argument("--name", help="Project name")
parser.add_argument("--template", help="Template name")
parser.add_argument("--due", help="Due date for tasks (optional)")
args = parser.parse_args()
if args.list:
list_templates()
elif args.name and args.template:
create_project_from_template(args.name, args.template, args.due)
else:
parser.print_help()
Example 3: Daily Task Report
#!/usr/bin/env python3
"""daily_report.py - Generate daily task report"""
from todoist_api_python import TodoistAPI
from datetime import datetime
import os
api = TodoistAPI(os.environ["TODOIST_API_KEY"])
def generate_daily_report():
"""Generate a daily task report"""
today = datetime.now().strftime("%Y-%m-%d")
# Get tasks for different filters
overdue = api.get_tasks(filter="overdue")
due_today = api.get_tasks(filter="today")
high_priority = api.get_tasks(filter="(p1 | p2) & !today & !overdue")
# Group today's tasks by project
projects = {}
for task in due_today:
project_id = task.project_id
if project_id not in projects:
try:
project = api.get_project(project_id)
projects[project_id] = {"name": project.name, "tasks": []}
except:
projects[project_id] = {"name": "Unknown", "tasks": []}
projects[project_id]["tasks"].append(task)
# Generate report
report = f"""# Daily Task Report - {today}
## Summary
- Overdue: {len(overdue)}
- Due Today: {len(due_today)}
- High Priority (upcoming): {len(high_priority)}
## Overdue Tasks
"""
for task in overdue:
report += f"- [{priority_emoji(task.priority)}] {task.content} (Due: {task.due.string})\n"
report += "\n## Today's Tasks by Project\n"
for project_id, project_data in projects.items():
report += f"\n### {project_data['name']}\n"
for task in project_data["tasks"]:
report += f"- [{priority_emoji(task.priority)}] {task.content}\n"
report += "\n## High Priority (Upcoming)\n"
for task in high_priority[:5]:
due = task.due.string if task.due else "No date"
report += f"- [{priority_emoji(task.priority)}] {task.content} (Due: {due})\n"
return report
def priority_emoji(priority):
"""Convert priority number to visual indicator"""
return {4: "!", 3: "*", 2: "-", 1: " "}.get(priority, " ")
if __name__ == "__main__":
report = generate_daily_report()
print(report)
# Optionally save to file
filename = f"daily_report_{datetime.now().strftime('%Y-%m-%d')}.md"
with open(filename, "w") as f:
f.write(report)
print(f"\nReport saved to: {filename}")
Integration Examples
Integration with Slack
#!/usr/bin/env python3
"""slack_todoist.py - Post Todoist tasks to Slack"""
import os
import requests
from todoist_api_python import TodoistAPI
TODOIST_API_KEY = os.environ["TODOIST_API_KEY"]
SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
api = TodoistAPI(TODOIST_API_KEY)
def post_daily_tasks_to_slack():
"""Post today's tasks to Slack"""
tasks = api.get_tasks(filter="today")
if not tasks:
message = "No tasks due today!"
else:
task_list = "\n".join([f"- {t.content}" for t in tasks])
message = f"*Tasks for Today ({len(tasks)}):*\n{task_list}"
payload = {
"text": message,
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": message
}
}
]
}
response = requests.post(SLACK_WEBHOOK_URL, json=payload)
return response.status_code == 200
if __name__ == "__main__":
if post_daily_tasks_to_slack():
print("Posted to Slack successfully")
else:
print("Failed to post to Slack")
Integration with Calendar (Google Calendar)
#!/usr/bin/env python3
"""calendar_sync.py - Sync Todoist tasks with Google Calendar"""
from todoist_api_python import TodoistAPI
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from datetime import datetime, timedelta
import os
api = TodoistAPI(os.environ["TODOIST_API_KEY"])
def sync_tasks_to_calendar():
"""Sync tasks with due dates to Google Calendar"""
creds = Credentials.from_authorized_user_file("token.json")
service = build("calendar", "v3", credentials=creds)
# Get tasks with due dates in next 7 days
tasks = api.get_tasks(filter="next 7 days")
for task in tasks:
if not task.due:
continue
# Check if event already exists
existing = find_existing_event(service, task.id)
if existing:
continue
# Create calendar event
event = {
"summary": task.content,
"description": f"Todoist Task ID: {task.id}\nPriority: {task.priority}",
"start": {
"date": task.due.date,
},
"end": {
"date": task.due.date,
},
"extendedProperties": {
"private": {
"todoist_id": task.id
}
}
}
service.events().insert(calendarId="primary", body=event).execute()
print(f"Created calendar event: {task.content}")
def find_existing_event(service, todoist_id):
"""Find existing calendar event for Todoist task"""
events = service.events().list(
calendarId="primary",
privateExtendedProperty=f"todoist_id={todoist_id}"
).execute()
return events.get("items", [])
Best Practices
1. Rate Limiting
import time
from functools import wraps
def rate_limit(calls_per_minute=50):
"""Decorator to rate limit API calls"""
min_interval = 60.0 / calls_per_minute
last_called = [0.0]
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[0]
wait_time = min_interval - elapsed
if wait_time > 0:
time.sleep(wait_time)
result = func(*args, **kwargs)
last_called[0] = time.time()
return result
return wrapper
return decorator
@rate_limit(calls_per_minute=50)
def api_call(func, *args, **kwargs):
return func(*args, **kwargs)
2. Error Handling
from todoist_api_python import TodoistAPI
import requests
def safe_api_call(func, *args, max_retries=3, **kwargs):
"""Execute API call with retry logic"""
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
# Rate limited
wait_time = int(e.response.headers.get("Retry-After", 60))
print(f"Rate limited. Waiting {wait_time}s...")
time.sleep(wait_time)
elif e.response.status_code >= 500:
# Server error, retry
time.sleep(2 ** attempt)
else:
raise
except requests.exceptions.ConnectionError:
time.sleep(2 ** attempt)
raise Exception(f"Failed after {max_retries} retries")
3. Batch Operations
def batch_create_tasks(tasks, batch_size=50):
"""Create tasks in batches to avoid rate limits"""
results = []
for i in range(0, len(tasks), batch_size):
batch = tasks[i:i + batch_size]
batch_results = sync_batch_add(batch)
results.extend(batch_results)
if i + batch_size < len(tasks):
time.sleep(1) # Brief pause between batches
return results
4. Caching
import json
from pathlib import Path
from datetime import datetime, timedelta
CACHE_DIR = Path.home() / ".cache" / "todoist"
CACHE_TTL = timedelta(minutes=5)
def get_cached_or_fetch(key, fetch_func, ttl=CACHE_TTL):
"""Get from cache or fetch fresh data"""
CACHE_DIR.mkdir(parents=True, exist_ok=True)
cache_file = CACHE_DIR / f"{key}.json"
if cache_file.exists():
data = json.loads(cache_file.read_text())
cached_at = datetime.fromisoformat(data["cached_at"])
if datetime.now() - cached_at < ttl:
return data["value"]
value = fetch_func()
cache_data = {
"cached_at": datetime.now().isoformat(),
"value": value
}
cache_file.write_text(json.dumps(cache_data, default=str))
return value
Troubleshooting
Common Issues
Issue: 401 Unauthorized
# Verify your API token
curl -s -X GET "https://api.todoist.com/rest/v2/projects" \
-H "Authorization: Bearer $TODOIST_API_KEY"
# Check if token is set correctly
echo $TODOIST_API_KEY
# Regenerate token at:
# https://todoist.com/app/settings/integrations/developer
Issue: 429 Too Many Requests
# Implement exponential backoff
import time
def retry_with_backoff(func, max_retries=5):
for i in range(max_retries):
try:
return func()
except Exception as e:
if "429" in str(e):
wait = 2 ** i
print(f"Rate limited, waiting {wait}s")
time.sleep(wait)
else:
raise
Issue: Task not appearing
# Check if task was created in different project
all_tasks = api.get_tasks()
for task in all_tasks:
if "keyword" in task.content.lower():
print(f"Found: {task.content} in project {task.project_id}")
Issue: Due dates not parsing
# Use explicit date format
api.add_task(
content="Test task",
due_date="2025-01-20" # ISO format
)
# Or use due_datetime for specific time
api.add_task(
content="Test task",
due_datetime="2025-01-20T14:00:00Z" # ISO with time
)
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2025-01-17 | Initial release with comprehensive Todoist API coverage |
Resources
- Todoist REST API Documentation
- Todoist Sync API Documentation
- Todoist Python SDK
- Filter Query Syntax
- Webhook Events Reference
This skill enables powerful task management automation through Todoist's comprehensive API, supporting projects, tasks, labels, filters, webhooks, and batch operations.