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.

EnvironmentBase URL
Productionhttps://app.tasklogy.eu/api/v1

Authentication

Every request needs a raw API key as a Bearer token:

Header
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_EXPIRED rather than API_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 a meta.pagination object:
200 OK
{
  "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.

HTTPCodeMeaning
401API_KEY_INVALIDMissing, malformed, or unrecognized key
401API_KEY_EXPIREDKey exists but is past its expires_at date
403ACCOUNT_SUSPENDEDKey is valid, but its owner's account has been suspended
403FORBIDDENValid 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"
404NOT_FOUNDResource does not exist
422VALIDATION_ERRORRequest body failed validation
422APPROVAL_REQUIREDCan't complete the task — an active approval request locks completion
422INBOX_LIST_LOCKEDTarget list is your auto-created Inbox, which can't be renamed
429RATE_LIMITEDRate 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

MethodPathNotes
GET/tasksList 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/tasksCreate 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}/completeToggles completion — see Gotchas below
POST/tasks/{id}/restoreRestore a task from Trash
POST/tasks/{id}/tagsAttach 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

POST /tasks
{
  "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.

MethodPathNotes
GET/tasks/{id}/subtasksRequires view access to the parent task
POST/tasks/{id}/subtasksBody: {"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

MethodPathNotes
GET/listsLists owned by, or shared with, the caller
POST/listsBody: {"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

MethodPathNotes
GET/tagsTags owned by the caller
POST/tagsBody: {"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

Shell
# 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

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}/complete toggles, 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 call complete if its status isn't already completed.
  • 403 vs 404 is meaningful, not incidental. If you're scripting against IDs you didn't just create yourself, a 403 means the resource exists under a different account, while 404 means it's genuinely gone or the ID is wrong.
  • Tag attach is find-or-create by name, not by ID. POST /tasks/{id}/tags always resolves by case-insensitive name — there's no way to attach an existing tag by its numeric id.
  • recurrence_rule isn't validated against the supported grammar at write time. The API will store a string the app can't parse. Stick to daily/weekly/monthly or a minimal FREQ=...;INTERVAL=n rule, 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.