This page covers the board and task surface of the udctl API, grouped by what you are doing rather than by swagger tag. Every endpoint comes with one full HTTP call: the request in raw HTTP and curl, the response per status code.
Board day-to-day
Reading columns, creating cards in a column, moving cards, reordering — the loop you live in.
POST/api/v1/boards/{boardId}/columns/{columnId}/queryDraft
See what is in a column
Semantic column read. You name the column and how to treat sprints; the server assembles the query — column query, board default_tags, active-sprint expansion, your ad-hoc filter, default ordering — and echoes the result as effective_query so you can see exactly what was asked.
Request body
| Field | Type | Description |
|---|
sprint | string | "active" (default) filters by the board's running sprint; "none" disables the filter; or a sprint id. |
filter | string | Ad-hoc filter fragment, ANDed onto the column query by the server. |
page / page_size | int | Per-column pagination, defaults 1 / 20. |
Request
POST /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/columns/col-doing/query HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json
{
"sprint": "active",
"filter": "title ~ \"callback\"",
"page": 1,
"page_size": 20
}
curl -s "$UD_API/api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/columns/col-doing/query" \
-H "Authorization: Bearer $UD_TOKEN" -H "Content-Type: application/json" \
-d '{"sprint":"active","filter":"title ~ \"callback\"","page":1,"page_size":20}'
Response
{
"data": [
{
"id": "7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10",
"title": "Retry payment callback on timeout",
"status": "doing",
"tags": ["dev", "urgent"],
"metadata": {"ud.sprint": "3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31", "order": 3}
}
],
"total": 2,
"page": 1,
"page_size": 20,
"effective_query": "(status = 'doing') AND ('q4' IN tags) AND ud.sprint = '3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31' AND (title ~ \"callback\") ORDER BY metadata.order ASC"
}
Highlighted: computed by the server, not sent by you — this line is the assembly every client used to do itself.
Errors
400Invalid filter fragment; the message points at the offending position.
404The column id is not on this board.
Column ids come from GET /api/v1/boards/{id} (the board carries its column definitions); translating a column name to its id is the client's one remaining job. Until this endpoint ships, column reads go through the raw query gate below.
POST/api/v1/boards/{boardId}/queryLive
Works today at: POST /api/v1/kanban/boards/{boardId}/query
Free query within a board
The raw query gate, scoped to one board. You send a complete query string and the server only applies the board's visibility scope. Free querying is a product feature and stays; the semantic column read above absorbs only the most common shape.
Request body
| Field | Type | Description |
|---|
query | string | Complete query string (same syntax as /api/v1/tasks/query). |
page | int | 1-indexed. |
page_size | int | Items per page. |
Request
POST /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/query HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json
{
"query": "(status = 'doing') ORDER BY metadata.order ASC",
"page": 1,
"page_size": 20
}
curl -s "$UD_API/api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/query" \
-H "Authorization: Bearer $UD_TOKEN" -H "Content-Type: application/json" \
-d "{\"query\":\"(status = 'doing') ORDER BY metadata.order ASC\",\"page\":1,\"page_size\":20}"
Response
{
"data": [
{
"id": "7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10",
"title": "Retry payment callback on timeout",
"description": "",
"status": "doing",
"path": "",
"tags": ["dev", "urgent"],
"checkInCount": 0,
"metadata": {"ud.sprint": "3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31", "order": 3},
"created_at": "2026-09-22T08:01:44Z",
"created_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502",
"updated_at": "2026-09-25T10:12:03Z",
"updated_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502"
}
],
"total": 9,
"page": 1
}
POST/api/v1/boards/{boardId}/tasksLive
Works today at: POST /api/v1/kanban/boards/{boardId}/tasks
Create a card on a board
Creates a task associated with the board; the server merges the board default_tags in. Today the client decides the initial status/tags itself. The Draft column_id field changes that: name the column, and the server computes the initial values from the column's enter actions (your explicit fields always win).
Request body
| Field | Type | Description |
|---|
titlerequired | string | Card title. |
description | string | Markdown body. |
status | string | Initial status. |
tags | string[] | Merged with the board default_tags on the server. |
metadata | object | Extensible keys (cf.*, ud.sprint, order, ...). |
assignee / derived_from_id / linked_to_ids / resourceIds | various | Optional; see OpenAPI for exact shapes. |
column_idDraft | string | Draft. Target column; the server computes initial values from its enter actions and stamps ud.sprint with the active sprint by default (send "ud.sprint": null in metadata to opt out). |
Request
POST /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/tasks HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Export support tickets",
"description": "CSV and XLSX",
"status": "todo",
"tags": ["dev"]
}
curl -s "$UD_API/api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/tasks" \
-H "Authorization: Bearer $UD_TOKEN" -H "Content-Type: application/json" \
-d '{"title":"Export support tickets","description":"CSV and XLSX","status":"todo","tags":["dev"]}'
Response
{
"id": "b7e02c4d-9a31-4f6e-8c25-d10f4a7b3e92",
"title": "Export support tickets",
"description": "CSV and XLSX",
"status": "todo",
"path": "",
"tags": ["dev", "q4"],
"checkInCount": 0,
"created_at": "2026-09-26T09:14:03Z",
"created_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502",
"updated_at": "2026-09-26T09:14:03Z",
"updated_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502"
}
In this sample "q4" arrived from the board default_tags — the one initial value the server already merges today.
POST/api/v1/boards/{boardId}/tasks/{taskId}/moveLive
Move a card to another column
Single-transaction move. The server computes merge(exit(from), enter(to)) and writes once — replacing today's client-side sequence of one update plus N metadata patches, which can fail halfway. dry_run: true computes without writing, for confirmation UIs.
Request body
| Field | Type | Description |
|---|
from_column_idrequired | string | The column you believe the card is in — the server verifies it. |
to_column_idrequired | string | Target column. |
dry_run | bool | true computes without writing; response shape is the same as 200. |
Request
POST /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10/move HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json
{
"from_column_id": "col-todo",
"to_column_id": "col-doing",
"dry_run": false
}
curl -s "$UD_API/api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10/move" \
-H "Authorization: Bearer $UD_TOKEN" -H "Content-Type: application/json" \
-d '{"from_column_id":"col-todo","to_column_id":"col-doing","dry_run":false}'
Response
{
"task": {
"id": "7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10",
"title": "Retry payment callback on timeout",
"status": "doing",
"tags": ["dev", "urgent"]
},
"applied_actions": [
{"op": "set", "field": "status", "from": "todo", "to": "doing"},
{"op": "add_tags", "value": ["dev"]}
]
}
{
"code": "KANBAN_TASK_NOT_IN_SOURCE_COLUMN",
"message": "task 7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10 no longer matches the query of column col-todo",
"task": {
"id": "7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10",
"status": "doing",
"tags": ["dev", "urgent"]
}
}
Highlighted: what the server actually did — also exactly what dry_run returns for a confirmation dialog.
Errors
409Your view is stale: the card no longer matches the source column query. The response carries the card's current snapshot — refresh and retry. The server never writes fields based on an outdated view.
The whole move is idempotent (set / add / remove are all idempotent), so replays are safe — no idempotency key needed. Not to be confused with PATCH /api/v1/tasks/{id}/move, which moves a task to a different folder path.
GET/api/v1/boards/{boardId}/summaryDraft
Glance at the whole board
Per-column counts plus the first few cards of each column, in one call — no per-column paging just to see how the board stands.
Query parameters
| Field | Type | Description |
|---|
sprint | string | Same semantics as the column read: "active" by default. |
preview | int | Cards to preview per column; default 3, 0 = counts only. |
Request
GET /api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/summary?preview=2 HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
curl -s "$UD_API/api/v1/boards/7d55a212-30f5-4a41-a5da-b2f7f01f3bb9/summary?preview=2" \
-H "Authorization: Bearer $UD_TOKEN"
Response
{
"board_id": "7d55a212-30f5-4a41-a5da-b2f7f01f3bb9",
"name": "Product launch",
"active_sprint_id": "3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31",
"columns": [
{"column_id": "col-todo", "name": "To Do", "total": 24,
"preview": ["Deep links broken on mobile", "Sign-up page A/B test"]},
{"column_id": "col-doing", "name": "Doing", "total": 9,
"preview": ["Retry payment callback on timeout", "Report export encoding bug"]},
{"column_id": "col-done", "name": "Done", "total": 132,
"preview": ["Login page refresh", "Notification grouping"]}
]
}
PATCH/api/v1/tasks/{taskId}/metadataLive
Works today at: PATCH /api/v1/todolist/{taskId}/metadata
Reorder in a column / assign to a sprint
Patches one metadata key per call. Reordering inside a column writes "order"; putting a card into a sprint writes "ud.sprint". Removing a key (taking a card out of a sprint) is the DELETE twin: DELETE /api/v1/tasks/{taskId}/metadata/{key}.
Request body
| Field | Type | Description |
|---|
keyrequired | string | Metadata key, e.g. "order", "ud.sprint", "cf.priority". |
value | any | Any JSON value. To remove the key entirely use the DELETE route, not null. |
Request
PATCH /api/v1/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10/metadata HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json
{
"key": "order",
"value": 12
}
curl -s -X PATCH "$UD_API/api/v1/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10/metadata" \
-H "Authorization: Bearer $UD_TOKEN" -H "Content-Type: application/json" \
-d '{"key":"order","value":12}'
The 200 response is the full task object (same shape as GET /api/v1/tasks/{id}).
GET/api/v1/tasks/{taskId}Live
Works today at: GET | POST | DELETE /api/v1/todolist/{taskId}
Open, edit, delete a card
The generic task detail family: GET reads (with notes and linked items), PATCH /api/v1/tasks/{taskId} updates, DELETE soft-deletes. The update is a partial update — every field is optional and an omitted field stays unchanged; for assignee, kickoff and deadline an explicit empty string means "clear". Note the verb change in the contract: today the update answers to POST at the old path; the target verb is PATCH, which is what the semantics have been all along.
Request
PATCH /api/v1/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10 HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json
{
"status": "done",
"deadline": ""
}
curl -s -X PATCH "$UD_API/api/v1/tasks/7c2f6a1e-4b09-4d2a-9e51-2f8c3d7b9a10" \
-H "Authorization: Bearer $UD_TOKEN" -H "Content-Type: application/json" \
-d '{"status":"done","deadline":""}'
This sample marks the card done and clears its deadline in one call. Full field list (title, description, status, path, tags, assignee, kickoff, deadline, derivedFromId, metadata) is in OpenAPI under updateTodolistItem.
Sprint rhythm
Sprints are tasks (ud.type = sprint); membership is the ud.sprint metadata key. Reads go through the free query gate.
POST/api/v1/tasks/queryLive
Works today at: POST /api/v1/todolist/query
Backlog, sprint members, any free query
The global query gate: SQL-like syntax over built-in fields, tags, ud.* and cf.* metadata. Backlog views, sprint member lists and every ad-hoc slice go through here. Free querying is a product feature and stays — the semantic endpoints above only absorb the most common shape ("give me a column").
Request body
| Field | Type | Description |
|---|
query | string | Query string. |
sort | object | Optional sort override; see OpenAPI (SortDTO). |
page / pageSize | int | Note the camelCase pageSize here (the board query gate uses page_size). |
view | string | "" or "full" returns the historical payload; "lite" drops description, notes and share links for card rendering. |
Request
POST /api/v1/tasks/query HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json
{
"query": "(status = 'todo') AND ('q4' IN tags) ORDER BY metadata.order ASC",
"page": 1,
"pageSize": 20,
"view": "lite"
}
curl -s "$UD_API/api/v1/tasks/query" \
-H "Authorization: Bearer $UD_TOKEN" -H "Content-Type: application/json" \
-d "{\"query\":\"(status = 'todo') AND ('q4' IN tags) ORDER BY metadata.order ASC\",\"page\":1,\"pageSize\":20,\"view\":\"lite\"}"
Response
{
"data": [
{
"id": "4de19b0c-2f6a-47d3-b8a1-9c05e2f7d614",
"title": "Sign-up page A/B test",
"status": "todo",
"path": "",
"tags": ["q4", "growth"],
"checkInCount": 0,
"metadata": {"order": 5},
"created_at": "2026-09-18T02:11:09Z",
"created_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502",
"updated_at": "2026-09-24T11:40:52Z",
"updated_by": "9a7e5c21-1b3f-4e8a-92d6-04c8f1a7d502"
}
],
"total": 14,
"page": 1,
"pageSize": 20,
"totalPages": 1
}
In "lite" view the description field is absent (not empty) — an absent description means "fetch the task to get the body", never "this card has no body".
POST/api/v1/tasks/{sprintId}/close-sprintLive
Works today at: POST /api/v1/todolist/{sprintId}/close-sprint
Close a sprint
The close ceremony in one server-side transaction: rolls unfinished members to the target, settles velocity, writes the retro note, marks the sprint done. Milestone-ordered — the sprint only flips to done after every rollover succeeded, so retries are safe.
Request body
| Field | Type | Description |
|---|
rollover | string | "backlog" or an open sprint id. Omitting the field means "no choice made" — the server refuses that when unfinished members exist, instead of silently sweeping them to the backlog. An empty body is legal when the sprint is already clean. |
Request
POST /api/v1/tasks/3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31/close-sprint HTTP/1.1
Host: api.oatnil.com
Authorization: Bearer <token>
Content-Type: application/json
{
"rollover": "backlog"
}
curl -s "$UD_API/api/v1/tasks/3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31/close-sprint" \
-H "Authorization: Bearer $UD_TOKEN" -H "Content-Type: application/json" \
-d '{"rollover":"backlog"}'
Response
{
"sprintId": "3f6d9a52-88a1-4f0e-b1d4-6c1f2e7a9b31",
"target": "backlog",
"completed": 11,
"rolledOver": 4,
"rotatedBoardIds": ["7d55a212-30f5-4a41-a5da-b2f7f01f3bb9"],
"skippedBoardIds": [],
"velocity": 23.5
}
Highlighted: the boards whose active-sprint pointer this close moved (rotatedBoardIds) and the ones it left for a group admin (skippedBoardIds).
Errors
400Not a sprint task, bad rollover target, or unfinished members with no rollover chosen.
The close also repoints the activeSprintId of every board pointing at this sprint — to the target sprint, or cleared for a backlog close — and reports them: rotatedBoardIds are the boards it moved; skippedBoardIds are shared boards the caller cannot write, whose pointer is left for a group admin to reconcile (a view preference must not fail the close).
Board lifecycle
Board CRUD and sharing; until the rename ships these answer under /api/v1/kanban/boards/*. One thing to know: PUT /api/v1/boards/{id} is the single write path for column definitions — the server materializes column actions and backfills missing column ids on every board write. Field shapes are in OpenAPI.
POST/api/v1/boardsCreate a board (name and board_type "private" | "shared" required; columns optional).
GET/api/v1/boardsList the boards you can see.
GET/api/v1/boards/{id}Get one board with its column definitions and settings.
PUT/api/v1/boards/{id}Update name, columns, default_tags, metadata — the column-definition write path.
DELETE/api/v1/boards/{id}Delete the board; its tasks are preserved.
POST/api/v1/boards/{id}/shareShare with a group (group_id, permission "r" | "rw").
DELETE/api/v1/boards/{id}/shareRemove group sharing.
POST/api/v1/boards/preview-actionsPreview the column actions a query would auto-generate — read-only, used while editing columns.