API Reference
Last updated: 2026-02-22
REST API for managing tasks, categories, and integrating with external tools like Obsidian.
Base URL: https://todone.fyi
Authentication
All API endpoints require authentication via API key (recommended for scripts and external tools) or session cookie (used by the web UI).
Creating an API Key
- Log in to the ToDone web app.
- Navigate to your account settings and create a new API key, or use the API directly (requires an active session):
curl -X POST https://<your-domain>/api/keys \
-H "Content-Type: application/json" \
-b "session-cookie-here" \
-d '{"label": "Obsidian Sync"}'
- The response includes the raw key — save it immediately, it cannot be retrieved again:
{
"key": "td_a1b2c3d4e5f6...",
"label": "Obsidian Sync"
}
Using an API Key
Pass the key as a Bearer token in the Authorization header:
Authorization: Bearer td_a1b2c3d4e5f6...
Write Access
Endpoints that create, update, or delete resources require write access. Write access may be restricted based on the user's subscription status. If write access is denied, the API returns 403 Forbidden.
Field Validation Limits
All text fields enforce maximum length limits:
| Field | Max Length |
|---|---|
| Task title | 500 chars |
| Task body | 50,000 chars |
| Category / stage name | 100 chars |
| API key label | 100 chars |
| Markdown import | 1,000,000 bytes (1 MB) |
Importing Tasks from Obsidian (Markdown)
This is the primary endpoint for pushing tasks from an Obsidian vault.
POST /api/tasks/markdown
Accepts a block of markdown containing Obsidian Tasks-formatted checkboxes and creates corresponding tasks in ToDone. If an active task with the same title already exists, its metadata is updated; otherwise the title is reported as skipped.
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer td_<your-api-key> |
| Content-Type | application/json |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
category | string | No | Name of an existing category to assign all tasks to |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
markdown | string | Yes | Raw markdown text containing task checkboxes (max 1 MB) |
Response (201):
{
"created": [ /* array of newly created task objects */ ],
"updated": [ /* array of tasks whose metadata was updated */ ],
"skipped": [ "Task title with no changes", "..." ]
}
Stage Mapping
Imported tasks are assigned to stages based on their checkbox status:
| Checkbox | Stage assigned |
|---|---|
- [ ] | First non-complete stage (e.g. TODO) |
- [/] | Second non-complete stage (e.g. DOING), or first if only one exists |
- [x] | First complete stage (e.g. DONE) |
Upsert Behavior
When a task with the same title (case-insensitive) already exists as an active (non-completed) task:
- If the imported task has different metadata (priority, dates, recurrence), the existing task is updated and included in the
updatedarray. - If all metadata matches, the title is added to the
skippedarray. - Only completed tasks with matching titles do not block new task creation.
Supported Markdown Format
The parser understands Obsidian Tasks syntax — standard markdown checkboxes with emoji signifiers for metadata:
Basic tasks
- [ ] An open task
- [x] A completed task
Task with a body / description
Indented lines immediately after a checkbox become the task body:
- [ ] Plan the offsite
Book venue, send invites, arrange catering
Budget: $5,000
Priority emojis
| Emoji | Priority |
|---|---|
| 🔺 | Urgent |
| ⏫ | High |
| 🔼 | Medium |
| 🔽 | Low |
| ⏬ | Lowest |
Date signifiers
Each takes a YYYY-MM-DD date:
| Emoji | Meaning |
|---|---|
| 📅 | Due date |
| ⏳ | Scheduled date |
| 🛫 | Start date |
| ➕ | Created date |
| ✅ | Done date |
Recurrence
| Emoji | Usage |
|---|---|
| 🔁 | Followed by a pattern, e.g. every week |
Supported recurrence patterns: every day, every N days, every week, every N weeks, every month, every N months, every year, every N years, every Monday (or any day), every weekday. Append when done to calculate the next date from the completion date instead of the original date.
Full example
- [ ] Buy groceries 🔺 📅 2026-03-07 ⏳ 2026-03-01 🛫 2026-02-15 🔁 every week
Don't forget milk and eggs
- [ ] Write quarterly report ⏫ 📅 2026-03-31
- [x] File taxes 🔼 📅 2026-02-15 ✅ 2026-02-08
- [ ] Water the plants 🔽 🔁 every 3 days
Example: Push tasks from a file
# Push an entire markdown file
curl -X POST "https://<your-domain>/api/tasks/markdown" \
-H "Authorization: Bearer td_<your-api-key>" \
-H "Content-Type: application/json" \
-d "{\"markdown\": $(jq -Rs . < ~/vault/tasks.md)}"
Example: Push tasks into a specific category
curl -X POST "https://<your-domain>/api/tasks/markdown?category=Work" \
-H "Authorization: Bearer td_<your-api-key>" \
-H "Content-Type: application/json" \
-d "{\"markdown\": $(jq -Rs . < ~/vault/work-tasks.md)}"
Example: Inline markdown
curl -X POST "https://<your-domain>/api/tasks/markdown" \
-H "Authorization: Bearer td_<your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"markdown": "- [ ] Review PR #42 ⏫ 📅 2026-02-10\n- [ ] Deploy staging 🔼"
}'
Obsidian Shell Script
Here is a ready-to-use shell script you can save in your vault or run via cron to sync tasks automatically:
#!/usr/bin/env bash
# sync-to-todone.sh — Push Obsidian tasks to ToDone
# Usage: ./sync-to-todone.sh [file-or-directory] [category]
set -euo pipefail
TODONE_URL="${TODONE_URL:-https://<your-domain>}"
TODONE_API_KEY="${TODONE_API_KEY:-td_<your-api-key>}"
TARGET="${1:-.}"
CATEGORY="${2:-}"
# Build category query param
CATEGORY_PARAM=""
if [[ -n "$CATEGORY" ]]; then
CATEGORY_PARAM="?category=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$CATEGORY'))")"
fi
# Collect all markdown files
if [[ -d "$TARGET" ]]; then
FILES=$(find "$TARGET" -name "*.md" -type f)
elif [[ -f "$TARGET" ]]; then
FILES="$TARGET"
else
echo "Error: $TARGET is not a valid file or directory" >&2
exit 1
fi
TOTAL_CREATED=0
TOTAL_SKIPPED=0
for FILE in $FILES; do
echo "Processing: $FILE"
# Extract only lines that are task checkboxes (and their indented body lines)
MARKDOWN=$(awk '
/^- \[[ xX]\]/ { printing=1; print; next }
printing && /^[[:space:]]+[^[:space:]]/ { print; next }
{ printing=0 }
' "$FILE")
if [[ -z "$MARKDOWN" ]]; then
echo " No tasks found, skipping."
continue
fi
PAYLOAD=$(jq -n --arg md "$MARKDOWN" '{markdown: $md}')
RESPONSE=$(curl -s -X POST "${TODONE_URL}/api/tasks/markdown${CATEGORY_PARAM}" \
-H "Authorization: Bearer ${TODONE_API_KEY}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
CREATED=$(echo "$RESPONSE" | jq '.created | length')
SKIPPED=$(echo "$RESPONSE" | jq '.skipped | length')
echo " Created: $CREATED, Skipped (duplicates): $SKIPPED"
TOTAL_CREATED=$((TOTAL_CREATED + CREATED))
TOTAL_SKIPPED=$((TOTAL_SKIPPED + SKIPPED))
done
echo ""
echo "Done. Total created: $TOTAL_CREATED, Total skipped: $TOTAL_SKIPPED"
Usage:
# Set your credentials
export TODONE_URL="https://todone.example.com"
export TODONE_API_KEY="td_a1b2c3d4..."
# Sync a single file
./sync-to-todone.sh ~/vault/Projects/tasks.md
# Sync all markdown files in a directory, assigning to a category
./sync-to-todone.sh ~/vault/Work/ "Work"
# Sync your entire vault
./sync-to-todone.sh ~/vault/
Initialization
GET /api/init
Bulk endpoint for initial app load. Returns all data needed to render the dashboard in a single request.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
sort | string | position | Sort tasks by: position, dueDate, priority, scheduledDate |
Response (200):
{
"tasks": [ /* array of task objects */ ],
"categories": [ /* array of category objects */ ],
"stages": [ /* array of stage objects */ ],
"workspaces": [ /* array of workspace objects */ ],
"activeWorkspaceId": "...",
"isAdmin": false
}
Workspaces
GET /api/workspaces
List all workspaces for the authenticated user.
Response (200): Array of workspace objects.
POST /api/workspaces
Create a new workspace. Auto-creates default stages (TODO, DOING, DONE) and sets it as the active workspace.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Workspace name |
Response (201): The created workspace object.
GET /api/workspaces/active
Returns the authenticated user's active workspace ID.
Response (200):
{ "activeWorkspaceId": "..." }
POST /api/workspaces/:id/switch
Set the specified workspace as the active workspace.
Response (200):
{ "ok": true, "workspaceId": "..." }
PATCH /api/workspaces/:id
Update a workspace (name, position).
DELETE /api/workspaces/:id
Delete a workspace. Cannot delete the last workspace. Returns the new active workspace ID.
Response (200):
{ "activeWorkspaceId": "..." }
POST /api/workspaces/:id/connect-google
Initiate Google OAuth flow for the workspace. Returns a URL to redirect the user to.
Response (200):
{ "url": "https://accounts.google.com/..." }
POST /api/workspaces/:id/disconnect-google
Disconnect Google Calendar from the workspace.
Response (200):
{ "ok": true }
Tasks
GET /api/tasks
List tasks for the authenticated user.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
stageId | string | — | Filter by stage ID |
priority | string | — | Filter: LOWEST, LOW, MEDIUM, HIGH, URGENT |
categoryId | string | — | Filter by category ID |
sort | string | position | Sort by: position, dueDate, priority, scheduledDate |
order | string | asc | Sort order: asc or desc |
Response (200): Array of task objects (includes nested category and stage).
Note: List responses exclude body and updatedAt for performance. Use GET /api/tasks/:id for the full task object.
curl "https://<your-domain>/api/tasks?sort=dueDate&order=asc" \
-H "Authorization: Bearer td_<your-api-key>"
POST /api/tasks
Create a single task (JSON, not markdown).
Request Body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | Yes | — | Task title (max 500 chars) |
body | string | No | null | Description / notes (max 50,000 chars) |
stageId | string | No | First non-complete stage | Stage ID |
priority | string | No | null | LOWEST, LOW, MEDIUM, HIGH, or URGENT |
duration | integer | No | null | Estimated duration in minutes |
dueDate | string | No | null | ISO 8601 date |
scheduledDate | string | No | null | ISO 8601 date |
startDate | string | No | null | ISO 8601 date |
recurrence | string | No | null | e.g. every week |
categoryId | string | No | null | ID of an existing category |
force | boolean | No | false | Skip fuzzy duplicate check |
Duplicate Detection:
Before creating a task, the API checks for duplicates among active (non-completed) tasks:
-
Exact match (case-insensitive title) — returns
409with:{ "error": "DUPLICATE", "message": "An active task with this title already exists", "existingId": "..." } -
Fuzzy match (Jaro-Winkler similarity >= 0.85) — returns
409with:{ "error": "SIMILAR", "message": "Similar active tasks found", "similarTasks": [{ "id": "...", "title": "..." }] }
Set force: true to skip the fuzzy similarity check. Exact duplicates are always rejected.
Response (201): The created task object.
curl -X POST "https://<your-domain>/api/tasks" \
-H "Authorization: Bearer td_<your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"title": "Review PR #42",
"priority": "HIGH",
"duration": 30,
"dueDate": "2026-02-10"
}'
PATCH /api/tasks/:id
Update a task. Send only the fields you want to change. All fields from POST /api/tasks are supported (except force), plus position for reordering.
Additional fields:
| Field | Type | Description |
|---|---|---|
position | float | Position for task ordering |
calendarEventId | string | Clear or set the linked Google Calendar event ID |
Completion behavior:
When moving a task to a stage with isComplete: true, completedAt is set automatically. Moving back to a non-complete stage clears completedAt.
Recurrence on completion:
If the completed task has a recurrence rule, a new task is created with shifted dates in the first non-complete stage. The response contains both tasks:
{
"completed": { /* the completed task */ },
"next": { /* the newly created recurring task */ }
}
Regular (non-recurring) updates return the updated task object directly.
curl -X PATCH "https://<your-domain>/api/tasks/clxyz123" \
-H "Authorization: Bearer td_<your-api-key>" \
-H "Content-Type: application/json" \
-d '{"stageId": "<done-stage-id>"}'
DELETE /api/tasks/:id
Delete a task. Returns 204 No Content.
curl -X DELETE "https://<your-domain>/api/tasks/clxyz123" \
-H "Authorization: Bearer td_<your-api-key>"
POST /api/tasks/batch
Create multiple tasks in a single request. Useful for reducing round-trips when creating several tasks at once (e.g. from a Claude skill or automation script).
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
tasks | array | Yes | Array of task objects (max 50 per request) |
Each task object accepts the same fields as POST /api/tasks:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | Yes | — | Task title (max 500 chars) |
body | string | No | null | Description / notes (max 50,000 chars) |
stageId | string | No | First non-complete stage | Stage ID |
priority | string | No | null | LOWEST, LOW, MEDIUM, HIGH, or URGENT |
duration | integer | No | null | Estimated duration in minutes |
dueDate | string | No | null | ISO 8601 date |
scheduledDate | string | No | null | ISO 8601 date |
startDate | string | No | null | ISO 8601 date |
recurrence | string | No | null | e.g. every week |
categoryId | string | No | null | ID of an existing category |
force | boolean | No | false | Skip fuzzy duplicate check |
Duplicate Detection: Same rules as POST /api/tasks — exact duplicates are rejected per-task, fuzzy matches rejected unless force: true. Additionally, duplicate titles within the same batch are rejected.
Response (201): Partial success supported — valid tasks are created, invalid ones reported as errors.
{
"created": [ { /* task object */ }, { /* task object */ } ],
"errors": [ { "index": 2, "error": "Title is required" } ]
}
If all tasks fail validation, returns 400 with empty created array.
curl -X POST "https://<your-domain>/api/tasks/batch" \
-H "Authorization: Bearer td_<your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"tasks": [
{ "title": "Review PR #42", "priority": "HIGH", "force": true },
{ "title": "Deploy staging", "force": true },
{ "title": "Update docs", "categoryId": "cat-123", "force": true }
]
}'
POST /api/tasks/:id/move
Move a task from the current workspace to a different workspace. Automatically maps stages and categories by name in the destination workspace. If no matching stage is found by name, a new stage is created in the destination workspace (copying name, color, and isComplete from the source). If no matching category is found by name, a new category is created (copying name and color from the source).
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId | string | Yes | Destination workspace ID |
unlinkCalendar | boolean | Conditional | Required when the task has a calendarEventId. Set true to clear the calendar link, false to keep it. |
Response (200): Updated task object with new workspace, stage, and category IDs.
Errors:
| Status | Condition |
|---|---|
| 400 | Missing workspaceId, same workspace, destination not owned by user, or missing unlinkCalendar when task has calendar event |
| 404 | Task not found in current workspace |
| 409 | Active task with same title exists in destination workspace |
curl -X POST "https://<your-domain>/api/tasks/clxyz123/move" \
-H "Authorization: Bearer td_<your-api-key>" \
-H "Content-Type: application/json" \
-d '{"workspaceId": "dest_workspace_id"}'
GET /api/tasks/duplicates
Find duplicate and similar tasks across all of the authenticated user's tasks.
Response (200):
{
"groups": [
{
"type": "exact",
"normalizedTitle": "buy groceries",
"tasks": [ /* tasks with identical titles */ ]
},
{
"type": "similar",
"normalizedTitle": "buy groceries",
"tasks": [ /* tasks with Jaro-Winkler similarity >= 0.85 */ ]
}
],
"count": 2
}
Each task in a group includes: id, title, completedAt, stageId, stage, category, createdAt, updatedAt.
Response includes Cache-Control: private, no-cache.
curl "https://<your-domain>/api/tasks/duplicates" \
-H "Authorization: Bearer td_<your-api-key>"
Insights
GET /api/insights
Returns productivity analytics for the active workspace. Metrics are computed over a configurable time window and include task completion trends, peak productivity hours, category breakdowns, avoided tasks, and on-time delivery rates.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
days | string | 7 | Time window for retrospective metrics: 7, 14, or 30 |
Response (200):
{
"completed": {
"count": 23,
"previous": 20,
"daily": [2, 4, 3, 5, 3, 2, 4]
},
"peakHours": {
"start": 9,
"end": 11,
"distribution": [0, 0, 0, 0, 0, 0, 1, 2, 4, 8, 9, 6, 3, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0]
},
"categories": [
{ "id": "...", "name": "Work", "color": "#3b82f6", "count": 12 }
],
"avoided": [
{ "id": "...", "title": "File taxes", "ageDays": 21, "pastDueDates": 1, "score": 9.2 }
],
"onTime": {
"onTime": 18,
"late": 7,
"previousOnTimeRate": 0.77
}
}
Response fields:
| Field | Description |
|---|---|
completed.count | Tasks completed in the current period |
completed.previous | Tasks completed in the equivalent prior period (for comparison) |
completed.daily | Array of daily completion counts (one entry per day in the window) |
peakHours.start | Hour (0-23) when the most productive block begins |
peakHours.end | Hour (0-23) when the most productive block ends |
peakHours.distribution | Array of 24 values representing completions per hour of the day |
categories | Breakdown of completed tasks by category |
avoided | Active tasks ranked by avoidance score (age, missed due dates) |
avoided[].ageDays | Days since the task was created |
avoided[].pastDueDates | Number of times the due date has passed without completion |
avoided[].score | Composite avoidance score (higher = more avoided) |
onTime.onTime | Tasks completed on or before their due date |
onTime.late | Tasks completed after their due date |
onTime.previousOnTimeRate | On-time rate (0-1) from the prior period |
Errors:
| Status | Condition |
|---|---|
| 400 | Invalid days value (must be 7, 14, or 30) |
| 401 | Not authenticated |
curl "https://<your-domain>/api/insights?days=14" \
-H "Authorization: Bearer td_<your-api-key>"
Stages
Stages define the workflow columns in the kanban view. Each user has their own set of stages.
GET /api/stages
List all stages for the authenticated user, ordered by position. Auto-creates default stages (TODO, DOING, DONE) if none exist.
Response (200): Array of stage objects.
POST /api/stages
Create a stage.
Request Body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Stage name (max 100 chars) |
color | string | No | null | Hex or OKLCH color value |
position | number | No | auto | Position for ordering (auto-assigned if omitted) |
isComplete | boolean | No | false | Whether this stage means "done" |
Returns 409 Conflict if a stage with the same name already exists in the workspace.
PATCH /api/stages/:id
Update a stage (name, color, position, isComplete).
DELETE /api/stages/:id
Delete a stage. Tasks in the deleted stage are moved to the first remaining stage (by position). Cannot delete the last stage. Returns 204 No Content.
Categories
GET /api/categories
List all categories for the authenticated user, ordered by position.
Response (200): Array of category objects. Supports ETag-based caching (304 Not Modified).
POST /api/categories
Create a category.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Category name (max 100 chars) |
color | string | No | Hex color code |
Position is auto-assigned.
Returns 409 Conflict if a category with the same name already exists in the workspace.
PATCH /api/categories/:id
Update a category (name, color, position).
DELETE /api/categories/:id
Delete a category. Returns 204 No Content.
Google Calendar
GET /api/calendar/connect
Initiates Google OAuth flow. Redirects to Google's consent screen. Requires session auth.
GET /api/calendar/callback
OAuth callback. Stores tokens and redirects back to the app.
GET /api/calendar/status
Returns { connected: true/false } indicating whether a Google Calendar is connected.
POST /api/calendar/disconnect
Revokes tokens and disconnects Google Calendar. Clears calendarEventId from all tasks.
POST /api/calendar/block
Block time on Google Calendar for a task.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
taskId | string | Yes | Task ID to block time for |
date | string | No | YYYY-MM-DD date to check (defaults to today) |
slotStart | string | No | ISO datetime — if provided, creates the event |
timeZone | string | No | IANA timezone (defaults to UTC) |
Uses the task's duration field, defaulting to 30 minutes if not set.
When slotStart is omitted, returns available time slots:
{
"available": [ /* array of available time slot objects */ ],
"duration": 30
}
When slotStart is provided, creates the calendar event and returns the updated task object with calendarEventId set.
PATCH /api/calendar/block/:taskId
Update a linked calendar event.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Event title |
description | string | No | Event description |
startTime | string | No | ISO datetime for new start time |
durationMinutes | integer | No | Duration in minutes (defaults to task duration or 30) |
timeZone | string | No | IANA timezone |
Response: { "ok": true }
DELETE /api/calendar/block/:taskId
Remove the linked calendar event and clear calendarEventId from the task.
Response: { "ok": true }
API Keys
API key endpoints require session authentication (not API key auth). Manage keys through the web UI or an authenticated session.
GET /api/keys
List your API keys, ordered by most recently created. Returns id, label, createdAt — not the key itself.
POST /api/keys
Create a new API key. Label max length is 100 characters. The raw key is returned once in the response.
DELETE /api/keys/:id
Revoke an API key. Returns 204 No Content.
Subscription & Billing
These endpoints integrate with Stripe for subscription management.
POST /api/stripe/checkout
Create a Stripe Checkout session to start a subscription.
Authentication: Required (session or API key).
Response (200):
{ "url": "https://checkout.stripe.com/..." }
Redirect the user to the returned URL to complete payment.
POST /api/stripe/portal
Create a Stripe Customer Portal session to manage an existing subscription (update payment method, cancel, etc.).
Authentication: Required (session or API key). User must have an existing Stripe customer ID.
Response (200):
{ "url": "https://billing.stripe.com/..." }
POST /api/stripe/webhook
Handles incoming Stripe webhook events. Authenticated via Stripe signature verification (not API key or session).
Handled events:
| Event | Action |
|---|---|
checkout.session.completed | Links Stripe customer ID to user |
customer.subscription.updated | Updates subscription status and plan |
customer.subscription.deleted | Marks subscription as canceled |
invoice.payment_failed | Marks subscription as past due |
Admin Endpoints
These endpoints require session authentication with the
ADMINrole. Returns403if the user is not an admin.
POST /api/admin/impersonate
Start impersonating another user.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | ID of user to impersonate |
Response (200): { "success": true, "userId": "..." }
POST /api/admin/stop-impersonate
Stop impersonating and return to the admin's own session.
Response (200): { "success": true }
Cron Jobs
POST /api/cron/prune-logs
Delete API log entries older than 90 days.
Authentication: Bearer token matching the CRON_SECRET environment variable.
curl -X POST "https://<your-domain>/api/cron/prune-logs" \
-H "Authorization: Bearer <CRON_SECRET>"
Response (200):
{
"success": true,
"deleted": 142,
"cutoffDate": "2025-11-16T00:00:00.000Z"
}
Error Responses
All errors follow this format:
{
"error": "Description of what went wrong"
}
| Status | Meaning |
|---|---|
| 400 | Bad request (missing or invalid fields) |
| 401 | Unauthorized (missing or invalid API key) |
| 403 | Forbidden (insufficient permissions or write access denied) |
| 404 | Resource not found or doesn't belong to you |
| 409 | Conflict (duplicate or similar task exists) |
| 500 | Internal server error |