Automation
Authentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer $TOKENCreating a token
# Docker
docker compose exec opentask tsx scripts/create-token.ts <username> [token-name]
# Bare metal
npm run db:create-token -- <username> [token-name]The token is printed once. Store it securely — it cannot be retrieved later. Tokens are stored as SHA-256 hashes on the server.
Auth methods
OpenTask checks authentication in this order:
- Bearer token — For scripts and automation
- Proxy header — For reverse proxy auth (Authelia, Authentik)
- Session cookie — For the web UI (managed automatically)
If a Bearer token is present but invalid, the request is rejected immediately — it never falls through to session auth.
Quick Add
Send raw dictated text and let AI enrichment handle the rest:
curl -X POST https://tasks.example.com/api/tasks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "call the dentist tomorrow morning high priority"}'AI extracts: clean title, target, priority, labels, recurrence, project, and notes, plus a deadline or horizon when the text states one. The raw input is preserved in original_title for retry if enrichment fails.
See How Enrichment Works for the full list of extracted fields.
Structured Create (Skip AI)
Send structured fields to bypass enrichment:
curl -X POST https://tasks.example.com/api/tasks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Team standup",
"target_at": "2026-02-15T15:00:00Z",
"priority": 2,
"rrule": "FREQ=WEEKLY;BYDAY=MO,WE,FR",
"labels": ["work"],
"create_label": true,
"project_id": 1
}'When AI is enabled, enrichment only runs for title-only tasks (no target_at/due_at, deadline_at, or horizon, priority 0, no labels, no rrule).
Target, deadline, and horizon
A task's dates are three separate fields (see Target, Deadline, and Horizon):
| Field | Meaning |
|---|---|
target_at | When the task starts nagging. due_at is accepted and returned as an older alias with the same value; sending both with different values is a 400 |
deadline_at | The real cutoff. A snooze never moves it, and every move of the target stops at it while it's still ahead. A one-off task with a deadline and no target gets an automatic one |
horizon | days, weeks, months, eventually, or null (unset, treated as weeks) |
Task responses also include target_auto (the target is the deadline's automatic target), target_set_at (when the target was last set), occurrence_at (a recurring task's scheduled occurrence, which a snooze doesn't move), horizon_set_at, last_considered_at, segment_deferred_at (for task segments), and original_due_at (the first target). Send "enrich": true to request enrichment even when you include structured fields.
Labels must exist
Labels are a registry. Writing a label that doesn't exist yet returns 400 ("Unknown label: …"). Either create it first with POST /api/labels, or add "create_label": true to the task request to create any missing labels as part of the write. GET /api/labels lists existing labels.
Tasks, Reminders, and Quotas
Every item is stored as a task. A few fields decide which kind it is:
| Kind | How to create it | Behavior |
|---|---|---|
| Task | Default | Optional target, deadline, and horizon. Can be over target and snoozed |
| Reminder | "is_reminder": true | Grouped by reminder period. Never over target, can't be snoozed |
| Quota | "progress_target" greater than 1 (or "is_tracked": true), usually with a period rule like FREQ=WEEKLY | Counts progress per period. No target, never over target |
An item can't be both a reminder and a quota. Filter lists by kind with GET /api/tasks?kind=task|reminder|quota. See Examples for requests.
Listing Tasks
GET /api/tasks returns open (not done) items, sorted by target:
{ "data": { "tasks": [ { "id": 42, "title": "..." } ], "count": 1 } }| Query parameter | Meaning |
|---|---|
project | Project ID (e.g. ?project=3) |
kind | task, reminder, or quota |
overdue | true for tasks that are over target right now, including deadline tasks whose automatic target has arrived (never reminders or quotas) |
done | true for completed items instead of open ones |
search | Text match in title and notes |
label | Items with this label |
recurring / one_off | true to list only recurring or only one-off items |
archived / trashed | true to list archived or trashed items |
limit / offset | Pagination (default limit 200, max 1000) |
Response Format
Success: { "data": { ... } }
Error: { "error": "message", "code": "ERROR_CODE" }. Validation errors also include a details object keyed by field.
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Validation error |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not found |
| 409 | Conflict |
| 500 | Internal error |
OpenAPI Specification
A machine-readable API spec is available at GET /api/openapi (no auth required). OpenAPI 3.1 YAML covering all public endpoints. Use it to generate client libraries or browse the interactive API Reference.
iOS Shortcuts Tips
- Use "Get Contents of URL" action with method POST
- Add
Authorization: Bearer $TOKENandContent-Type: application/jsonheaders - Set Request Body to JSON with a single
titlekey containing the dictated text - Name the shortcut "Add Task" for "Hey Siri, Add Task"
- Parse
data.idfrom the response to confirm creation
