Tasks
Tasks and task lists, and the counts behind the Tasks sidebar.
On this page
List task lists
GET/task_lists
Task lists, in the owner's order.
curl "$NIFTY_URL/api/v1/task_lists" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
orderposition | id | name | updated_at · optional | The field to sort by; ties are broken by id, so the order is stable. |
directionasc | desc · optional | asc (ascending) or desc (descending), for order. |
updated_sincedate-time · optional | Only records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id. |
cursorstring · optional | meta.next_cursor from the previous page. |
limitinteger · optional | Items per page, up to 100; a larger number is taken as 100. |
counttrue | false · optional | true adds meta.total_count: the size of the filtered set (with updated_since too), ignoring cursor and limit. Anything but true or false is a 400. |
Response
200 OK Errors: 400 401 429
{
"data": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Garden",
"color": "emerald",
"icon": "leaf",
"position": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"meta": {
"next_cursor": "eyJrIjpbIjIwMjYtMTAtMDlUMDg6MzA6MDBaIl19",
"limit": 1,
"total_count": 1
}
}
Add a task list
POST/task_lists
Needs a name. Goes last unless position is sent. A blank color gets the first one no other list uses
(colours and icons are those of GET /category_choices).
curl -X POST "$NIFTY_URL/api/v1/task_lists" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task_list":{"name":"Garden","color":"emerald","icon":"leaf"}}'
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Request body
| Name | Description |
|---|---|
task_listobject | |
task_list.namestring · optional | Spaces are squished. A name another list has, ignoring case, is a 422. |
task_list.colorstring or null · optional | A key from GET /category_choices. Blank picks the first one no other list uses. |
task_list.iconstring or null · optional | An icon from GET /category_choices, or null for the default list icon. |
task_list.positioninteger · optional | 1-based; out-of-range values are clamped. Moves the list there. |
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Garden",
"color": "emerald",
"icon": "leaf",
"position": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Get a task list
GET/task_lists/{id}
curl "$NIFTY_URL/api/v1/task_lists/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
200 OK Errors: 401 404 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Garden",
"color": "emerald",
"icon": "leaf",
"position": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change or move a task list
PATCH/task_lists/{id}
Also PUT /task_lists/{id}, the same.
Only the fields sent change. Sending position moves the list there and renumbers the live lists 1..n; the
others whose position changed get a new updated_at, so re-sync with updated_since after a move. Tasks
hold only task_list_id, so renaming or recolouring a list changes no task.
curl -X PATCH "$NIFTY_URL/api/v1/task_lists/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task_list":{"name":"Garden","color":"emerald","icon":"leaf"}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
task_listobject | |
task_list.namestring · optional | Spaces are squished. A name another list has, ignoring case, is a 422. |
task_list.colorstring or null · optional | A key from GET /category_choices. Blank picks the first one no other list uses. |
task_list.iconstring or null · optional | An icon from GET /category_choices, or null for the default list icon. |
task_list.positioninteger · optional | 1-based; out-of-range values are clamped. Moves the list there. |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Garden",
"color": "emerald",
"icon": "leaf",
"position": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Delete a task list
DELETE/task_lists/{id}
Delete a task list and its tasks.
Restorable for 20 seconds (POST /task_lists/{id}/restoration), then permanent. The list and its tasks are
gone from every endpoint at once, with deletion records for the list and each of its tasks. Its name is
free for a new list at once. Deleting again is a 404.
curl -X DELETE "$NIFTY_URL/api/v1/task_lists/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429
Restore a task list
POST/task_lists/{task_list_id}/restoration
Undo deleting a task list.
Within 20 seconds of DELETE /task_lists/{id}: brings the list back with its tasks. Their deletion records are
removed, and they get a new updated_at. A task deleted on its own before the list stays deleted. If another
list took its name meanwhile, it comes back renamed "Name (2)", so check name. No body. Restoring a list
that isn't deleted changes nothing and returns it, so a retry is safe. Once the 20 seconds have passed, or for an
unknown id, it's a 404. If other lists take the name it picks twice in a row, it's a 422 on name; try again.
curl -X POST "$NIFTY_URL/api/v1/task_lists/01j9zq3k8m5x2v7c4n6b0t1r9e/restoration" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
task_list_idULID | A record's ID. |
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Response
200 Restored. The list, as GET /task_lists/{id} returns it. Errors: 400 401 403 404 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Garden",
"color": "emerald",
"icon": "leaf",
"position": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
List tasks
GET/tasks
Live tasks: not deleted, and not in a deleted list, person or project. Filters combine (AND); they're for
browsing, so sync unfiltered. Without view or status, every status is listed. Before answering, open tasks
whose expires_on has passed are archived (as every task endpoint does), so none is listed as open.
curl "$NIFTY_URL/api/v1/tasks" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
statusstring · optional | Comma-separated statuses, e.g. open,completed. Anything else, or with view, is 400. |
viewoverview | today | planned | one_day | due_soon | due_not_today | today_queue | logbook · optional | A smart list, with today in the owner's time zone. Each lists only open tasks, except logbook. Combines
with one home filter (inbox, task_list_id, person_id or project_id).
- overview: everything open in the Inbox, and open tasks overdue or due within 14 days, from every home.
- today: started today or earlier (start_on ≤ today), from every home, the Inbox too.
- planned: starting after today, not in the Inbox. Ordered by start_on by default.
- one_day: no start date, not in the Inbox.
- due_soon: overdue, or due within 14 days, from every home. Ordered by deadline_on by default.
- due_not_today: due_soon without today's (no start date, or starting after today). Ordered by deadline_on by default.
- today_queue: Today's queue: overdue, due today, or started (start_on ≤ today); no undated task unless it's due. Ordered by deadline (none last) by default.
- logbook: completed and archived tasks, by when they closed (completed_at or archived_at, whichever is set; ordered as closed_at), newest first by default.
|
inboxtrue · optional | true: only the Inbox (tasks with no home). Anything else is 400. |
task_list_idULID · optional | Only this list's tasks. An unknown or deleted list is an empty page. At most one home filter (inbox, task_list_id, person_id, project_id); two is 400. A home filter combines with view (e.g. view=logbook&task_list_id=…, one list's Logbook). |
person_idULID · optional | Only the tasks whose home is this person. |
project_idULID · optional | Only the tasks whose home is this project. |
orderposition | created_at | updated_at | id | start_on | deadline_on | closed_at | completed_at · optional | position (the default: the home's manual order; ties, e.g. across homes, by id), created_at,
updated_at or id. view=planned adds start_on (its default), view=due_soon, view=due_not_today and
view=today_queue deadline_on (their default; today_queue puts tasks with none last) and view=logbook closed_at (its default: completed_at or archived_at, whichever is set; not
a field of its own). status=completed (alone) adds completed_at,
e.g. the latest completed with direction=desc. Anything else is 400.
|
directionasc | desc · optional | asc by default; desc for view=logbook. |
updated_sincedate-time · optional | Only records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id. |
cursorstring · optional | meta.next_cursor from the previous page. |
limitinteger · optional | Items per page, up to 100; a larger number is taken as 100. |
counttrue | false · optional | true adds meta.total_count: the size of the filtered set (with updated_since too), ignoring cursor and limit. Anything but true or false is a 400. |
Response
200 OK Errors: 400 401 429
{
"data": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Book the plumber",
"status": "open",
"start_on": "2026-10-10",
"deadline_on": "2026-10-10",
"expires_on": "2026-10-10",
"completed_at": "2026-10-10T09:30:00Z",
"archived_at": "2026-10-10T09:30:00Z",
"task_list_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"position": 1,
"description_html": "<p>Ask about the boiler too.</p>",
"description_text": "Ask about the boiler too.",
"has_documents": true,
"expired": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"meta": {
"next_cursor": "eyJrIjpbIjIwMjYtMTAtMDlUMDg6MzA6MDBaIl19",
"limit": 1,
"total_count": 1
}
}
Add a task
POST/tasks
Needs a title. A new task is open unless status is sent, in the Inbox unless a home id is sent, and goes to
the top of its home's open tasks unless position is sent (a closed one, to the bottom). Going to the top
changes no other task: its position is one less than the first's, so it may be zero or negative.
curl -X POST "$NIFTY_URL/api/v1/tasks" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":{"title":"Book the plumber","deadline_on":"2026-10-16","project_id":"01j9zq3p4q6r8s0t2v5w7x9y1z"}}'
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Request body
| Name | Description |
|---|---|
taskTaskWrite | A home id that isn't an id, an unknown or deleted home, or two home ids at once (a 422 on base), are 422s.
|
task.titlestring · optional | Spaces are squished. |
task.description_htmlstring or null · optional | See "Note bodies": mentions, tags and project references by id. An image (signed-id) is a 422 on description. Null or "" clears it. |
task.start_ondate or null · optional | YYYY-MM-DD. Today (owner's zone) or earlier puts it in view=today. One that isn't a date is a 422. |
task.deadline_ondate or null · optional | Not before start_on (a 422), unless the deadline has already passed: an overdue task can start today. |
task.expires_ondate or null · optional | Not before start_on or deadline_on, nor, on an open task, before today (each a 422). The task stays
open through that day and is archived from the next, with archived_at that midnight in the owner's zone.
|
task.statusTaskStatus · optional | Any status can change to any other. completed sets completed_at, archived sets archived_at, open
clears both, and reopening a task whose expires_on has passed clears it. Sending the current status
changes nothing.
|
task.task_list_idULID or null · optional | The home. Sending one home id clears the other two; all three null is the Inbox. |
task.person_idULID or null · optional | A record's ID. |
task.project_idULID or null · optional | A record's ID. |
task.positioninteger · optional | 1-based among the (new) home's open tasks; out-of-range values are clamped. |
task.document_idsULID[] or null · optional | The task's documents after the save: the full set, linked and unlinked in the save's transaction. Omitted
leaves them as they are; null or [] unlinks them all. An unknown or deleted one is a 422 on documents,
and nothing saves. Not an array of ids is a 400.
|
task.document_uploadsstring[] or null · optional | signed_ids of PDFs from POST /uploads, each becoming a document (named from its filename) linked to the
task, in the save. One that isn't an unused PDF upload is a 422 on documents, and nothing saves.
|
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Book the plumber",
"status": "open",
"start_on": "2026-10-10",
"deadline_on": "2026-10-10",
"expires_on": "2026-10-10",
"completed_at": "2026-10-10T09:30:00Z",
"archived_at": "2026-10-10T09:30:00Z",
"task_list_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"position": 1,
"description_html": "<p>Ask about the boiler too.</p>",
"description_text": "Ask about the boiler too.",
"has_documents": true,
"expired": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Get a task
GET/tasks/{id}
A deleted task, or one in a deleted list, person or project, is a 404.
curl "$NIFTY_URL/api/v1/tasks/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
200 OK Errors: 401 404 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Book the plumber",
"status": "open",
"start_on": "2026-10-10",
"deadline_on": "2026-10-10",
"expires_on": "2026-10-10",
"completed_at": "2026-10-10T09:30:00Z",
"archived_at": "2026-10-10T09:30:00Z",
"task_list_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"position": 1,
"description_html": "<p>Ask about the boiler too.</p>",
"description_text": "Ask about the boiler too.",
"has_documents": true,
"expired": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change a task
PATCH/tasks/{id}
Also PUT /tasks/{id}, the same.
Change, move, complete or reopen a task.
Only the fields sent change. Moving it to another home puts it at the bottom there unless position is
sent too. Sending position moves it among its home's open tasks and renumbers them 1..n; the others
whose position changed get a new updated_at, so re-sync with updated_since after a move.
curl -X PATCH "$NIFTY_URL/api/v1/tasks/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":{"title":"Book the plumber","deadline_on":"2026-10-16","project_id":"01j9zq3p4q6r8s0t2v5w7x9y1z"}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
taskTaskWrite | A home id that isn't an id, an unknown or deleted home, or two home ids at once (a 422 on base), are 422s.
|
task.titlestring · optional | Spaces are squished. |
task.description_htmlstring or null · optional | See "Note bodies": mentions, tags and project references by id. An image (signed-id) is a 422 on description. Null or "" clears it. |
task.start_ondate or null · optional | YYYY-MM-DD. Today (owner's zone) or earlier puts it in view=today. One that isn't a date is a 422. |
task.deadline_ondate or null · optional | Not before start_on (a 422), unless the deadline has already passed: an overdue task can start today. |
task.expires_ondate or null · optional | Not before start_on or deadline_on, nor, on an open task, before today (each a 422). The task stays
open through that day and is archived from the next, with archived_at that midnight in the owner's zone.
|
task.statusTaskStatus · optional | Any status can change to any other. completed sets completed_at, archived sets archived_at, open
clears both, and reopening a task whose expires_on has passed clears it. Sending the current status
changes nothing.
|
task.task_list_idULID or null · optional | The home. Sending one home id clears the other two; all three null is the Inbox. |
task.person_idULID or null · optional | A record's ID. |
task.project_idULID or null · optional | A record's ID. |
task.positioninteger · optional | 1-based among the (new) home's open tasks; out-of-range values are clamped. |
task.document_idsULID[] or null · optional | The task's documents after the save: the full set, linked and unlinked in the save's transaction. Omitted
leaves them as they are; null or [] unlinks them all. An unknown or deleted one is a 422 on documents,
and nothing saves. Not an array of ids is a 400.
|
task.document_uploadsstring[] or null · optional | signed_ids of PDFs from POST /uploads, each becoming a document (named from its filename) linked to the
task, in the save. One that isn't an unused PDF upload is a 422 on documents, and nothing saves.
|
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Book the plumber",
"status": "open",
"start_on": "2026-10-10",
"deadline_on": "2026-10-10",
"expires_on": "2026-10-10",
"completed_at": "2026-10-10T09:30:00Z",
"archived_at": "2026-10-10T09:30:00Z",
"task_list_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"position": 1,
"description_html": "<p>Ask about the boiler too.</p>",
"description_text": "Ask about the boiler too.",
"has_documents": true,
"expired": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Delete a task
DELETE/tasks/{id}
Restorable for 20 seconds (POST /tasks/{id}/restoration), then permanent. Writes a task deletion record at once. Deleting again is a 404.
curl -X DELETE "$NIFTY_URL/api/v1/tasks/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429
Restore a task
POST/tasks/{task_id}/restoration
Undo deleting a task.
Within 20 seconds of DELETE /tasks/{id}: brings the task back, removes its deletion record and gives it a new
updated_at. No body. Restoring a task that isn't deleted changes nothing and returns it, so a retry is safe.
Once the 20 seconds have passed, for an unknown id, or while its list, person or project is deleted (restore that
instead), it's a 404. If its expiry passed meanwhile, it comes back archived.
curl -X POST "$NIFTY_URL/api/v1/tasks/01j9zq3k8m5x2v7c4n6b0t1r9e/restoration" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
task_idULID | A record's ID. |
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Response
200 Restored. The task, as GET /tasks/{id} returns it. Errors: 400 401 403 404 409 415 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Book the plumber",
"status": "open",
"start_on": "2026-10-10",
"deadline_on": "2026-10-10",
"expires_on": "2026-10-10",
"completed_at": "2026-10-10T09:30:00Z",
"archived_at": "2026-10-10T09:30:00Z",
"task_list_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"position": 1,
"description_html": "<p>Ask about the boiler too.</p>",
"description_text": "Ask about the boiler too.",
"has_documents": true,
"expired": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Task counts per list
GET/task_counts
How many open tasks are in the Inbox, Today, due soon, overdue and each list; and one page's counts.
Live, open tasks, with today in the owner's time zone; each count except overdue matches its GET /tasks
listing. Expired tasks are archived first. Not a record: there's nothing to sync, so ask again when tasks
change.
Name a page with view, or one home (inbox, task_list_id, person_id or project_id), to get its
counts in page. view=logbook may also name one home. An unknown or hidden home counts zeros.
curl "$NIFTY_URL/api/v1/task_counts" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
viewoverview | today | planned | one_day | logbook · optional | A Tasks page. Anything else, or any but logbook with a home, is 400. |
inboxtrue · optional | true: the Inbox's page. Anything else is 400. At most one home; two is 400. |
task_list_idULID · optional | A list's page. Not an id is 400. |
person_idULID · optional | A person's Tasks. |
project_idULID · optional | A project's Tasks. |
Response
200 OK Errors: 400 401 429
{
"data": {
"inbox": 1,
"today": 1,
"due_soon": 1,
"overdue": 1,
"task_lists": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"open": 1
}
],
"page": {
"open": 1,
"completed": 1,
"completed_today": 1,
"overdue": 1,
"wont_do": 1,
"expired": 1
}
}
}
Task counts per project
GET/project_task_counts
Each project's open and completed tasks, and its next task.
Live tasks of live projects, by project_id; a project with none is left out (archived tasks aren't
counted). next_task is the open task to do next: started ones (no start_on, or today or earlier in the
owner's time zone) first, then by deadline_on (none last), position and id; null when none is open.
Expired tasks are archived first. Not paginated, and not a record: ask again when tasks change.
curl "$NIFTY_URL/api/v1/project_task_counts" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": [
{
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"open": 1,
"completed": 1,
"next_task": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "string",
"start_on": "2026-10-10",
"deadline_on": "2026-10-10"
}
}
]
}