tasuki

tasuki API guide

For developers and agent builders. This page covers the public REST API and MCP surface.

1. Overview and base URLs

tasuki is a task system for people and agents. The same account data is available over REST and MCP.

Surface Base
Web app https://app.usetasuki.com
REST and MCP https://api.usetasuki.com (alias of the app host for token traffic)

REST lives under /api/v1. OpenAPI is at GET /openapi.json and is the source of truth for routes and fields. Every REST response carries X-Tasuki-API-Version: 1.

Discover features with:

GET /api/v1/capabilities

No sign-in is needed for the static part. The body includes API version, server build, feature flags, limits, rate-limit policy, supported MCP protocol versions, and deprecations. Prefer this over guessing from a version string.

2. Authentication

Call REST with a bearer credential:

Authorization: Bearer YOUR_TOKEN

A missing, invalid, expired or revoked credential returns 401 with WWW-Authenticate set.

Personal access tokens and agents

Agents are created with POST /agents. The create response holds a one-time token that is never stored again. That token is scoped to the account (and optionally to projects).

A personal access token can also carry the webhooks scope. That scope is not part of any trust level. Tick it under Advanced when you create the token.

Scopes and trust presets

Scope is checked before existence. A grant that lacks a scope returns 403 (<agent> lacks '<scope>' scope). Another account’s data, or a task outside an agent’s projects, returns 404, never 403.

Trust levels for agents:

Level Can
suggest Propose only (status: proposed). New field values are allowed on a proposal.
trusted Add and edit tasks. Create and update projects.
full Also complete and delete tasks, and delete projects.

Useful scopes called out in the contract: read, complete, webhooks. Completing needs complete. Humans (and the native iOS app acting as the person) can call human-only routes such as accept, reject, settings, import and export.

A native app token is refused on /mcp.

3. Conventions

Versioning

Within /api/v1 and the MCP tool set, v1 is additive only. tasuki never removes or renames a route, field, tool, argument, scope or response key; never changes a field’s type or meaning; never tightens validation so yesterday’s request fails today; never changes an existing success or error status.

tasuki may add routes, tools, optional fields, response keys and open enum values. Closed enums (status, next_actor) do not grow silently. Clients must ignore unknown response keys. Feature-detect with /capabilities. A breaking change would be /api/v2 beside v1.

Nothing is deprecated today (capabilities.deprecations is []). When something is, it is listed there with replacement and sunset, and responses carry Deprecation and Sunset headers. Sunset is at least 90 days after the changelog entry.

Errors

Every error is JSON:

{"error": "<human readable message>"}

Extra keys are additive per route (code, current, open_subtasks, evidence_required, reason, retry_after_seconds, and so on).

Status Meaning
400 Bad JSON, unknown field, wrong type, validation
401 Bad or missing credential
403 Missing scope, human-only action, or project create limit
404 Missing or not visible to you
409 Version conflict, open sub-tasks, caps, undo refused, idempotency in flight
413 Body over 256 KB
422 Idempotency key reused with a different request
429 Rate limited
500 Server bug (no internals in the body)

Pagination

GET /tasks with no paging args still returns a bare array. With limit (1–500, default 50), offset or cursor the body is:

{"tasks": [], "total": 0, "limit": 50, "offset": 0, "next_cursor": "..."}

Follow next_cursor until it is absent. Cursors are opaque. Batches are at most 50 items per request.

Rate limits

Fixed window per credential: 600 requests per minute (iOS app: 240). A batch counts as one request. Every /api/v1 response includes RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. A 429 also sends Retry-After.

Idempotency

REST: header Idempotency-Key: <1–200 printable characters, no spaces> on POST, PATCH, PUT, DELETE.

MCP: argument idempotency_key on write tools (add_task, update_task, complete_task, delete_task, add_tasks, update_tasks, add_comment, undo_change).

Same key and same request within 24 hours returns the original result with idempotent_replay: true (and REST header Idempotent-Replay: true). Same key with a different request is 422 idempotency_key_conflict. An in-flight key is 409 with Retry-After: 1. Only successful results are stored. Keys are per account. POST /agents ignores the key. Dry runs never touch the key table.

POST /tasks / add_task by an agent also de-duplicates an open task with the same normalised title in the same project (and same source if given), returning it with duplicate_of_existing: true (REST 200). Pass allow_duplicate: true to opt out.

Dry run

?dry_run=true (REST) or dry_run: true (MCP) on add, update, complete and delete returns something like:

{
  "dry_run": true,
  "would_apply": true,
  "would_require_review": false,
  "diff": {"before": null, "after": {}, "changed": []},
  "trust_check": {}
}

Nothing is written. Scope, trust and validation still apply, so a denied action is denied in a dry run too.

Dates are YYYY-MM-DD, times HH:MM (24 h), in the user’s timezone (GET /api/v1/settings, default Australia/Perth). Empty string clears an optional string or date. null or 0 clears numbers. Unknown request fields are still 400.

4. Tasks

Key fields

Core fields: id, title, status (todo | doing | done | proposed), next, due, effort, project, notes, origin, created, updated, version.

Also:

Field Notes
next_action Alias of next. Responses carry both.
next_actor human | agent | other
next_actor_name Up to 80 chars
waiting_on / waiting_until Blocker text and follow-up date
due_date, due_time, due_tz Structured due; due is legacy
start_date Hide until
recurrence Object or text on input
priority 1–4 (1 highest)
estimate_minutes Integer
project / project_id Name and/or id; must agree if both sent
labels Up to 10, each up to 40 chars
parent_id One level of sub-tasks
done_when Checklist, up to 20 items
evidence {text?, links?[]} on complete
source Provenance {url?, label?}

Task ids look like t_ plus hex. Treat them as opaque.

List and get

GET /api/v1/tasks?status=todo&project_id=p_...&limit=50
Authorization: Bearer YOUR_TOKEN

Filters include status, project, project_id, label, priority, due helpers (due_before, due_after, due_on, due_today, overdue, has_due), q, next_actor, waiting, parent_id, top_level, available, sort, order, and paging.

POST /api/v1/tasks
Content-Type: application/json

{"title": "Send quote", "project": "Sales", "next_actor": "human", "priority": 2}

Complete

POST /api/v1/tasks/{id}/complete
Content-Type: application/json

{"evidence": {"text": "Quote emailed", "links": ["https://..."]}, "complete_children": true}

If the owner requires agent evidence, an agent completion without evidence is 400 with evidence_required: true. Open sub-tasks are 409 unless complete_children: true. Completing a recurring task creates the next instance and may return recurred_to.

Comments, dependencies, batch

Comments appear on the change feed as comment events with parent: {entity: "task", id}. MCP write tools include add_comment. Sub-tasks use parent_id (one level). Batch writes accept at most 50 items (add_tasks, update_tasks).

Related reads: GET /api/v1/today, /projects, /labels, and human accept/reject on proposed tasks.

5. The change feed and delta sync

One ordered stream of account changes, built from the activity log in the same transaction as each write.

GET /api/v1/changes?format=events&since=<cursor>&limit=100&view=compact

Needs read. Response shape:

{
  "events": [],
  "next_cursor": "c_412",
  "has_more": false,
  "reset": false,
  "retention_days": 90,
  "workspace_id": "w_..."
}

Event sketch:

{
  "id": "ev_411",
  "cursor": "c_411",
  "at": "2026-11-04T01:02:03.456Z",
  "type": "task.completed",
  "entity": "task",
  "entity_id": "t_1a2b3c",
  "action": "completed",
  "actor": {"kind": "agent", "name": "Hermes", "id": "ag_..."},
  "origin": "mcp",
  "change_id": "411",
  "version": 5,
  "project_id": "p_...",
  "changed": ["completed_at", "status"],
  "after": {"id": "t_1a2b3c", "title": "Send quote", "status": "done", "version": 5}
}

Guarantees: strict cursor order, at-least-once delivery, de-dupe on event id, never another account’s data. Retention window for the feed is 90 days. An old or ahead cursor returns reset: true and a baseline fetch list. On resync: "tasks", re-list tasks. Project-limited agents only see their projects; a task leaving scope arrives as a delete with scope_exit: true.

Suggested sync:

  1. GET .../changes?format=events&since=now and store next_cursor.
  2. Fetch baseline: tasks, projects (?archived=true or include_archived=true as documented), labels.
  3. Poll with since=<cursor>; apply by entity / entity_id.
  4. On reset: true, start again.

Long-poll: add &wait=25 (0–25 seconds). At most 3 waiting requests per account.

The older GET /changes?since=<seq> task-snapshot feed remains for the iOS app.

6. Webhooks

Webhooks deliver the same change events. Create with a human session or a PAT that has the webhooks scope:

POST /api/v1/webhooks
Content-Type: application/json

{"url": "https://example.com/tasuki-hook", "events": ["task.created", "suggestion.created"]}
{"id": "wh_...", "url": "...", "events": [], "created_at": "...", "secret": "***"}

The secret is returned once. Limits: 5 webhooks per user; HTTPS only; public hostnames; no private DNS targets; redirects never followed; 10 s timeout. Allowed event names include task.created, task.updated, task.completed, task.deleted, suggestion.created, suggestion.accepted, suggestion.rejected, comment.created.

Delivery headers: Tasuki-Event, Tasuki-Delivery, Tasuki-Signature: t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Reject when |now - t| > 300 seconds. De-dupe on Tasuki-Delivery or body id. Retries: 6 attempts over about 55 minutes. 50 consecutive failures auto-disables the hook.

Verify (curl)

t=1793000000; body='{"id":"ev_1"}'
printf '%s.%s' "$t" "$body" | openssl dgst -sha256 -hmac "$WHSEC" -hex

Verify (Node)

import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, rawBody, header, now = Date.now() / 1000) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!m || Math.abs(now - Number(m[1])) > 300) return false;
  const want = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest();
  return timingSafeEqual(want, Buffer.from(m[2], "hex"));
}

Verify (Python)

import hmac, hashlib, re, time
def verify(secret: str, raw_body: bytes, header: str, now=None) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
    if not m or abs((now or time.time()) - int(m[1])) > 300:
        return False
    want = hmac.new(secret.encode(), m[1].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, m[2])

List and delete: GET /api/v1/webhooks, DELETE /api/v1/webhooks/{id}. Deliveries: GET /api/v1/webhooks/{id}/deliveries (last 50, no bodies stored). Test: POST /api/v1/webhooks/{id}/test sends a signed webhook.ping.

7. MCP

Point your MCP client at the tasuki MCP endpoint on api.usetasuki.com with the agent token. Supported protocol versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05. Structured tool output (outputSchema / structuredContent) is only sent on 2025-06-18 or newer; text content is always present.

Tools by group

Capabilities and sync

Tasks

Projects and labels

Review signal

Safety

Server instructions tell agents to read get_today first and list_rejections before proposing. Trust levels match REST (suggest / trusted / full).

8. Undo and safety

When in doubt, call GET /api/v1/capabilities, prefer dry run on writes, and treat OpenAPI as canonical for field-level detail.