API Reference
Every list, task, tag, and subtask in Tasklogy is reachable through a versioned REST API. Generate a key from your Profile page and start scripting — no separate developer account needed.
API version: v1 · Last updated: 18 July 2026
Base URL & authentication
The public API is versioned and lives under /api/v1. It's distinct from the app's own session-authenticated frontend routes — this API is Bearer-token authenticated instead, and built for third-party integrations and scripts.
| Environment | Base URL |
|---|---|
| Production | https://app.tasklogy.eu/api/v1 |
Authentication
Every request needs a raw API key as a Bearer token:
Authorization: Bearer <your-api-key>
- Keys are generated from the web app's Profile page — there's no API endpoint to mint one. The raw key is shown exactly once at creation; if it's lost, generate a new one.
- No cookies or CSRF tokens needed — this is a pure Bearer-token API, unlike the session-based web app.
- A key can carry an expiry date. An expired key returns
401 API_KEY_EXPIREDrather thanAPI_KEY_INVALID, so you can tell the two apart when debugging.
Response envelope
Every successful response is wrapped consistently, whether it's one resource or many:
- Single resource —
{ "data": { ... } } - Plain collection —
{ "data": [ ... ] } - Paginated collection (currently only
GET /tasks) — includes ameta.paginationobject:
{
"data": [ ... ],
"meta": {
"pagination": {
"current_page": 1,
"per_page": 20,
"total": 42,
"last_page": 3
}
}
}
Errors
Error bodies follow one shape: { "error": "human-readable message", "code": "ERROR_CODE" }. Validation failures add a details object mapping field name to messages, matching Laravel's standard validation error shape.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | API_KEY_INVALID | Missing, malformed, or unrecognized key |
| 401 | API_KEY_EXPIRED | Key exists but is past its expires_at date |
| 403 | ACCOUNT_SUSPENDED | Key is valid, but its owner's account has been suspended |
| 403 | FORBIDDEN | Valid key, but not authorized for this resource (e.g. another user's task) — the same authorization rules as the web app apply, so you can tell "exists but isn't yours" apart from "doesn't exist" |
| 404 | NOT_FOUND | Resource does not exist |
| 422 | VALIDATION_ERROR | Request body failed validation |
| 422 | APPROVAL_REQUIRED | Can't complete the task — an active approval request locks completion |
| 422 | INBOX_LIST_LOCKED | Target list is your auto-created Inbox, which can't be renamed |
| 429 | RATE_LIMITED | Rate limit exceeded — see the Retry-After header |
Rate limiting
The API allows 50 requests per minute per API key by default. Requests made without a valid key are rate-limited by IP address instead.
When a request is throttled, you'll get back 429 with the RATE_LIMITED error code above and a Retry-After header telling you how many seconds to wait.
Always check the response status. A 429 still returns a normal-looking JSON body — scripts that assume success without checking response.ok (or your HTTP client's equivalent) will silently skip requests instead of failing loudly. If you're driving the API in a loop (bulk import/export, sync jobs), batch your calls or add a small delay to stay comfortably under the limit.
Tasks
| Method | Path | Notes |
|---|---|---|
| GET | /tasks | List tasks owned by, or shared with, the caller. Filters: list_id, status (csv: open,in_progress,completed,cancelled), priority (csv: 0,1,2,3), due_before/due_after (YYYY-MM-DD), tag (tag id), page, per_page (default 20, max 100) |
| POST | /tasks | Create a task. Requires edit access on task_list_id. Returns 201 |
| GET | /tasks/{id} | Fetch one task, with tags and subtasks eager-loaded |
| PUT | /tasks/{id} | Partial update — every field is optional |
| DELETE | /tasks/{id} | Soft-delete (moves to Trash), cancels any active approval request. Returns 204 |
| POST | /tasks/{id}/complete | Toggles completion — see Gotchas below |
| POST | /tasks/{id}/restore | Restore a task from Trash |
| POST | /tasks/{id}/tags | Attach a tag by name — find-or-create, case-insensitive. Body: {"name": "groceries"} |
| DELETE | /tasks/{id}/tags/{tag_id} | Detach a tag from the task |
Create / update body
{
"task_list_id": 4,
"title": "Ship F016",
"description": "Optional",
"priority": 2,
"status": "open",
"due_date": "2026-07-10",
"due_time": "17:00",
"reminder_offset_minutes": 30,
"recurrence_rule": null
}
task_list_id and title are required on create only. priority: 0=None, 1=Low, 2=Medium, 3=High. recurrence_rule accepts null, "daily"/"weekly"/"monthly", or a minimal RRULE like "FREQ=DAILY;INTERVAL=3".
Subtasks
Nested under a task. Subtasks are one level deep only — there's no support for sub-subtasks.
| Method | Path | Notes |
|---|---|---|
| GET | /tasks/{id}/subtasks | Requires view access to the parent task |
| POST | /tasks/{id}/subtasks | Body: {"title": "..."}. Requires edit access to the parent task |
| PATCH | /tasks/{id}/subtasks/{sid} | All fields optional: title, description, priority, due_date, due_time, is_completed, sort_order |
| DELETE | /tasks/{id}/subtasks/{sid} | Returns 204 |
Subtasks support the richer fields (description, priority, due_date, due_time) via PATCH, but creation only accepts title — set the rest in a follow-up PATCH.
Lists
| Method | Path | Notes |
|---|---|---|
| GET | /lists | Lists owned by, or shared with, the caller |
| POST | /lists | Body: {"name": "Groceries", "color": "#4F46E5"}. name required, unique among the caller's own lists |
| PUT | /lists/{id} | Same body shape. Caller must own the list. Returns 422 INBOX_LIST_LOCKED for the Inbox |
| DELETE | /lists/{id} | Deletes the list and soft-deletes all its tasks. Owner-only, not the Inbox. Returns 204 |
Tags
| Method | Path | Notes |
|---|---|---|
| GET | /tags | Tags owned by the caller |
| POST | /tags | Body: {"name": "groceries", "color": "#4F46E5", "icon": "🛒"}. name required, unique per-user, case-insensitive; color/icon optional |
There's no rename or delete endpoint for tags yet — that's web-app-only. To remove a tag from a task without deleting the tag itself, use DELETE /tasks/{id}/tags/{tag_id}.
Examples
curl
# List open/in-progress high-priority tasks
curl -s "https://app.tasklogy.eu/api/v1/tasks?status=open,in_progress&priority=2,3" \
-H "Authorization: Bearer $TASKLOGY_API_KEY"
# Create a task
curl -s -X POST "https://app.tasklogy.eu/api/v1/tasks" \
-H "Authorization: Bearer $TASKLOGY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"task_list_id": 4, "title": "Ship F016", "priority": 2, "due_date": "2026-07-10"}'
# Toggle-complete a task (check status first if you need idempotence)
curl -s -X POST "https://app.tasklogy.eu/api/v1/tasks/123/complete" \
-H "Authorization: Bearer $TASKLOGY_API_KEY"
Python
import os
import requests
BASE = "https://app.tasklogy.eu/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TASKLOGY_API_KEY']}"}
def create_task(list_id, title, **fields):
r = requests.post(f"{BASE}/tasks", headers=HEADERS,
json={"task_list_id": list_id, "title": title, **fields})
r.raise_for_status() # don't silently swallow a 429/422
return r.json()["data"]
def list_open_tasks(list_id=None):
params = {"status": "open,in_progress", "per_page": 100}
if list_id:
params["list_id"] = list_id
r = requests.get(f"{BASE}/tasks", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()["data"]
Gotchas
POST /tasks/{id}/completetoggles, it doesn't set. Calling it on an already-completed task reopens it. If you need idempotent "mark complete," fetch the task first and only callcompleteif its status isn't alreadycompleted.403vs404is meaningful, not incidental. If you're scripting against IDs you didn't just create yourself, a403means the resource exists under a different account, while404means it's genuinely gone or the ID is wrong.- Tag attach is find-or-create by name, not by ID.
POST /tasks/{id}/tagsalways resolves by case-insensitive name — there's no way to attach an existing tag by its numeric id. recurrence_ruleisn't validated against the supported grammar at write time. The API will store a string the app can't parse. Stick todaily/weekly/monthlyor a minimalFREQ=...;INTERVAL=nrule, or the task won't show a next-occurrence date.- Dates are stored exactly as sent, with no per-user timezone conversion on the API layer. Convert to the target account's local day before sending, especially when importing from tools that export UTC timestamps.