Tasklife API Reference
Per-endpoint reference for the public API. Each section shows the exact path, every parameter (with type, required/default, and meaning), a full request example, and the response shape with a worked example.
Quickstart
Base URL
https://api.tasklife.com/v1/
Auth (all endpoints)
Every request must include a bearer token in the Authorization header. The token is an API key bound to a specific organization — endpoints return only data scoped to that org.
Authorization: Bearer <your_api_key>
Content-Type: application/json
Response envelope
All successful responses share a common envelope. Error responses are described in Errors below.
{
"success": true,
"data": { /* endpoint-specific payload */ }
}
Note: a few endpoints nest the result one level deeper than you'd expect (e.g. data.page.page.<field> for Page create/get). Each endpoint below shows its actual response shape — trust that, not this generic envelope.
Errors
Failed responses always have success: false and an error object with a stable code and a human-readable message:
{
"success": false,
"error": {
"code": "API_ERROR",
"message": "content must be a JSON string (got array)"
}
}
| HTTP status | When | What to do |
|---|---|---|
400 | Request body failed validation (missing required field, wrong type, etc.). | Read error.message; fix the offending field. |
401 | Missing or invalid Authorization header. | Check the API key; confirm the header is present. |
403 | API key is valid but the user lacks permission (org mismatch, admin-gated action, etc.). | Confirm the key's org matches the resource's org; check user role. |
404 | Resource doesn't exist, is soft-deleted, or isn't visible to this org. | Verify the ID and that the resource wasn't deleted. |
409 | Conflict (duplicate slug, replay attempt, etc.). | Read error.message for the specific conflict. |
422 | Request is well-formed but semantically invalid (e.g. nested reply to a reply). | Read error.message; restructure the request. |
500 | Unexpected server error. | Retry with backoff; report if persistent. |
Pages (CMS)
Create, read, update, and delete CMS pages. Pages have a hierarchical parent_id tree, store body content as Markdown, and support soft-delete via deleted_at.
⚠️ content must be a JSON string
content is stored as LONGTEXT — there is no practical length cap. The single most common failure is sending content as anything other than a JSON string. The server rejects this with 400:
| Sent as | Result |
|---|---|
String ("# Hello\nworld") | ✅ stored verbatim, any length |
Missing / null | ✅ stored as "" (empty page) |
Array (["# Hello", "world"]) | ❌ 400 — content must be a JSON string (got array) |
Object ({"text": "..."}) | ❌ 400 — content must be a JSON string (got object) |
Real-world cause: a "string-ish" wrapper that isn't actually a plain string before JSON-encoding (e.g. PowerShell's Get-Content -Raw returns a string decorated with extended-type-system note-properties; ConvertTo-Json then serializes it as {"value": …} rather than a string). Fix at the source: read the file as a plain string.
Endpoints at a glance
| Action | Method + Path | Summary |
|---|---|---|
| Create a page | POST /pages | Create a new page. Returns the new id and slug. |
| Get a page | GET /pages/{id} | Fetch a single page including its full body, metadata, children, and media. |
| List pages | GET /pages | List pages in the org, optionally filtered by parent, status, or visibility. |
| Search pages | GET /search?q=… | Full-text search across title + body. |
| Update a page | PUT /pages/{id} | Replace fields on a page. Body can replace or append (see append flag). |
| Append content | POST /pages/{id}/append | Append a string to the page body using a configurable separator. |
| Get page tree | GET /pages/tree | Full hierarchical tree of pages (parent → children → grandchildren). |
| Get version history | GET /pages/{id}/versions | List previous versions of a page (snapshotted on update). |
| Page diff (what's new) | GET /pages/{id}/diff | Word-level diff of the visible text between two versions (MCP: get_page_diff). |
| Transfer to another org | POST /pages/{id}/transfer | Move a page subtree (versions, comments, media) into another org you belong to; dry_run preview (MCP: transfer_page). |
| Delete a page | DELETE /pages/{id} | Soft-delete a page (sets deleted_at); children are cascaded. |
POST /pages
Create a new page.
Body parameters
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title |
string |
yes | — | 1–255 chars. Becomes the page record name and contributes to the slug. |
content |
string |
no | "" |
Markdown body. Must be a JSON string (see warning above). No practical length cap. |
parent_id |
integer |
no | null (root) |
Parent page id, or 0 / "0" / null / empty for root. |
Request example
curl -sS -X POST https://api.tasklife.com/v1/pages \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "My Page",
"content": "# Heading\nSome **markdown** body.",
"parent_id": 0
}'
Response 201 Created
The new page is nested under data.page.page:
| Field | Type | Description |
|---|---|---|
data.page.page.id | integer | New page id. Use this for follow-up updates, deletes, appends, etc. |
data.page.page.title | string | Echo of the title you sent. |
data.page.page.slug | string | URL-safe slug derived from the title. |
{
"success": true,
"data": {
"page": {
"page": {
"id": 776,
"title": "My Page",
"slug": "my-page"
}
}
}
}
GET /pages/{id}
Fetch a single page by id.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | Page id. |
Request example
curl -sS https://api.tasklife.com/v1/pages/776 \
-H "Authorization: Bearer $API_KEY"
Response 200 OK
The page is nested under data.page.page:
| Field | Type | Description |
|---|---|---|
id | integer | Page id. |
parent_id | integer | null | Parent page id, or null for root pages. |
title | string | Page title. |
content | string | Markdown body (may be empty). |
slug | string | URL-safe slug. |
status | string | "active" or "inactive". |
visibility | string | "public" or "private". |
version | integer | Current version number (incremented on each update). |
author_name | string | Display name of the page's creator. |
created_at | string (ISO 8601 datetime) | Page creation timestamp. |
updated_at | string (ISO 8601 datetime) | Last update timestamp. |
children | array | Direct child pages (nested). Each child follows the same shape recursively. |
media | array | Media attachments referenced in the page body. |
attachments | array | File attachments on the page. |
{
"success": true,
"data": {
"page": {
"page": {
"id": 776,
"parent_id": 589,
"title": "My Page",
"content": "# Heading\nSome **markdown**.",
"slug": "my-page",
"status": "active",
"visibility": "private",
"version": 1,
"author_name": "Sal Iozzia",
"created_at": "2026-06-15T14:32:11Z",
"updated_at": "2026-06-15T14:32:11Z",
"children": [],
"media": [],
"attachments": []
}
}
}
}
GET /pages
List pages visible to the API key's org. Soft-deleted pages are excluded.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
parent_id | integer | no | Filter to direct children of this page id. |
status | string | no | "active" or "inactive". |
visibility | string | no | "public" or "private". |
Request example
curl -sS "https://api.tasklife.com/v1/pages?parent_id=589" \
-H "Authorization: Bearer $API_KEY"
Response 200 OK
{
"success": true,
"data": {
"pages": [
{
"id": 776,
"title": "My Page",
"slug": "my-page",
"parent_id": 589,
"status": "active",
"visibility": "private",
"created_at": "2026-06-15T14:32:11Z",
"updated_at": "2026-06-15T14:32:11Z"
}
],
"pagination": { "limit": 50, "offset": 0, "total": 1 }
}
}
GET /search?q=…
Full-text search across page titles and bodies. Multi-word queries match pages containing all terms (AND).
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
q | string | yes | Search query. Must contain at least one non-space character. |
Request example
curl -sS "https://api.tasklife.com/v1/search?q=realtime+pubsub" \
-H "Authorization: Bearer $API_KEY"
Response 200 OK
{
"success": true,
"data": {
"matches": [
{
"id": 776,
"parent_id": 589,
"title": "Realtime pubsub plan",
"slug": "realtime-pubsub-plan",
"content": "# Heading\nSome **markdown**."
}
]
}
}
PUT /pages/{id}
Update fields on a page. Any subset of fields may be sent; omitted fields are unchanged. The previous version is snapshotted on every successful update.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | Page id. |
Body parameters
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | no | — | 1–255 chars. |
content | string | no | — | Markdown body. Must be a JSON string (see warning above). |
status | string | no | — | "active" or "inactive". |
visibility | string | no | — | "public" or "private". |
parent_id | integer | no | — | Move the page under a new parent. |
append |
boolean |
no | false |
When true, content is concatenated to the existing body using separator between them. When false / omitted, content replaces the body entirely. |
separator |
string |
no | "\n\n" |
Inserted between existing and new content when append=true. Ignored when append=false. |
Request examples
Replace body:
curl -sS -X PUT https://api.tasklife.com/v1/pages/776 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "My Page (renamed)",
"content": "# New body"
}'
Append to body:
curl -sS -X PUT https://api.tasklife.com/v1/pages/776 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "\n## Appended section",
"append": true,
"separator": ""
}'
Response 200 OK
Update is the odd endpoint out — it does not return the updated page. The response is just { success: true } nested under data.page:
| Field | Type | Description |
|---|---|---|
data.page.success | boolean | Always true on success. To read the new state, call GET /pages/{id}. |
{
"success": true,
"data": {
"page": { "success": true }
}
}
POST /pages/{id}/append
Append a string to the page body. Equivalent to PUT /pages/{id} with append: true, but with its own dedicated route for callers that only want to append.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | Page id. |
Body parameters
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
content |
string |
yes | — | String to append. Must be a JSON string (see warning above). |
separator |
string |
no | "\n\n" |
Inserted between the existing body and the new content. |
Request example
curl -sS -X POST https://api.tasklife.com/v1/pages/776/append \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "## Appended section",
"separator": "\n\n"
}'
Response 200 OK
{
"success": true,
"data": { "page": { "id": 776 }, "success": true }
}
GET /pages/tree
Return the full page hierarchy as a nested tree. Each node carries the same shape as a single page response, with children recursively populated.
Request example
curl -sS https://api.tasklife.com/v1/pages/tree \
-H "Authorization: Bearer $API_KEY"
Response 200 OK
{
"success": true,
"data": {
"tree": [
{
"id": 589, "title": "Engineering", "slug": "engineering", "parent_id": null,
"children": [
{ "id": 776, "title": "My Page", "slug": "my-page", "parent_id": 589, "children": [] }
]
}
]
}
}
GET /pages/{id}/versions
List the version history of a page. Every successful PUT writes a snapshot before applying the change.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | Page id. |
Request example
curl -sS https://api.tasklife.com/v1/pages/776/versions \
-H "Authorization: Bearer $API_KEY"
Response 200 OK
{
"success": true,
"data": {
"versions": [
{ "version": 2, "title": "My Page", "content": "# New body", "created_by": 1, "created_at": "2026-06-15T15:00:00Z" },
{ "version": 1, "title": "My Page", "content": "# Original body", "created_by": 1, "created_at": "2026-06-15T14:32:11Z" }
]
}
}
GET /pages/{id}/diff
"What's new" diff, the API/MCP equivalent of the Pages View changes button. Compares the visible text (markdown/HTML stripped) of two versions. Version N is the page state before the write recorded at that version's at/by; the live page is current (latest snapshot + 1). MCP tool: get_page_diff (page_id, from_version, to_version, since).
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
from | integer | no | Baseline version number. Default: the previous version (newest snapshot below to). |
to | integer | "current" | no | Default current (the live page). |
since | ISO-8601 | no | Alternative to from: baseline = the page as it was at that time. Not combinable with from. |
Request example
curl -sS "https://api.tasklife.com/v1/pages/832/diff?from=3" \
-H "Authorization: Bearer $API_KEY"
Response 200 OK
{
"success": true,
"data": { "diff": {
"page_id": 832, "title": "My Page", "current_version": 4,
"from": { "version": 3, "current": false, "title": "My Page", "at": "2026-10-01T15:00:00Z", "by": 1, "by_name": "Sal" },
"to": { "version": 4, "current": true, "title": "My Page", "at": "2026-10-01T16:00:00Z", "by": 1, "by_name": "Sal" },
"summary": { "additions": 6, "removals": 2, "title_changed": false, "words_before": 120, "words_after": 124 },
"mode": "word", "too_large": false, "truncated": false,
"changes": [
{ "type": "equal", "text": "... last words of unchanged context" },
{ "type": "del", "text": "old words" },
{ "type": "add", "text": "new words" }
],
"unified": "@@ -10,4 +10,4 @@\n context\n-old words\n+new words\n"
} }
}
Flags: too_large (content over the size cap; only title + word counts returned), truncated (segments capped), no_baseline (no earlier version yet). mode is word, block (large-content fallback) or none.
POST /pages/{id}/transfer
Moves the page and its whole subtree (every descendant via parent_id, including trashed ones) from the org your key is bound to (the source) into target_org_id. Page ids are kept, so version history, comments and media travel with the pages. Media used only by the moved pages is moved; media also used elsewhere in the source org is copied (same file, new id) and the moved content is repointed. The root becomes top-level in the target org, or a child of target_parent_id. Colliding slugs get a -2, -3… suffix; team_id and the Pages-home flag are cleared. Source-org task–page links and section-index pointers to the moved pages are removed. An audit entry is written in both orgs. MCP tool: transfer_page. Tasks are not transferred (yet).
Authorization: the platform user behind the key must have an active membership in both the source and the target org. Otherwise the call fails with a generic 403 FORBIDDEN. Read-only keys may only use dry_run.
Body
| Field | Type | Required | Description |
|---|---|---|---|
target_org_id | integer | yes | Destination org (must differ from the key's org). |
target_parent_id | integer | null | no | Active page in the target org to nest the root under. Omit/null = top-level. Invalid ids return 400. |
dry_run | boolean | no | true = preview only, nothing changes. Recommended first. |
Request example (dry run)
curl -sS -X POST "https://api.tasklife.com/v1/pages/837/transfer" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"target_org_id": 7, "dry_run": true}'
Response 200 OK
{
"success": true,
"data": {
"dry_run": true, "transferred": false,
"source_org_id": 1, "target_org_id": 7, "target_parent_id": null,
"root": { "id": 837, "title": "Ser.vi", "parent_id": null },
"pages": [
{ "id": 837, "title": "Ser.vi", "depth": 0, "parent_id": null, "new_parent_id": null,
"child_count": 2, "version_count": 12, "comment_count": 1, "media_count": 3,
"trashed": false, "slug": "ser-vi", "new_slug": "ser-vi" }
],
"totals": { "pages": 3, "descendants": 2, "trashed_pages": 0, "versions": 20, "comments": 1,
"media": 3, "media_moved": 3, "media_copied": 0, "task_links_removed": 0, "section_index_removed": 0 },
"media": [ { "id": 41, "file_name": "logo.png", "action": "move" } ],
"slug_renames": [],
"warnings": [],
"tasks": "not_supported: task transfer is a follow-up; only pages are moved"
}
}
Without dry_run the same payload is returned with transferred: true and a result object (pages_moved, comments_moved, media_moved, media_copied, audit_ids, …). Errors: 400 bad/missing target_org_id or invalid target_parent_id; 403 not a member of both orgs; 404 page not in the key's org; 409 tree changed mid-transfer (nothing moved).
DELETE /pages/{id}
Soft-delete a page. Sets deleted_at and removes the page from list/tree/search/get. Children are cascaded (also soft-deleted).
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | Page id. |
Request example
curl -sS -X DELETE https://api.tasklife.com/v1/pages/776 \
-H "Authorization: Bearer $API_KEY"
Response 200 OK / 204 No Content
{ "success": true }
Other sections
These will be added next, following the same per-endpoint layout used for Pages:
- Projects & columns
- Tasks
- Comments
- Teams
- Agents & credentials