TaskBoard API Reference
demonstrates: A complete, lookup-oriented API reference that stays factual and exhaustive without drifting into tutorial or opinion.
TaskBoard is a Kanban API for personal projects. Each board is a subject with a row of status columns (swimlanes); a task lives in exactly one swimlane and carries comments. Moving a task to a new swimlane is the “status change” that prints a receipt on a Rongta RP850 thermal printer.
This page is a lookup reference for the HTTP contract. For how the printing pipeline works, see the printer-integration explanation linked under See also.
Overview
- Base URL: in the cloud, the API is served at
https://printchit.appbehind API Gateway + CloudFront. Locally it defaults tohttp://127.0.0.1:8000, and when the API also serves the built web board the same address answers over Tailscale/LAN. - Media type:
application/jsonfor request and response bodies. - Interactive docs: Swagger UI at
/docs; raw schema at/openapi.json. - API version:
0.1.0(from the OpenAPIinfo.version).
Authentication
Every business endpoint requires a Cognito-issued JWT, sent as a bearer token:
Authorization: Bearer <access-token>
In the cloud, API Gateway’s JWT authorizer validates the token before the request reaches the app, and the app reads the forwarded claims. On a box without the gateway, the app validates the bearer token itself against Cognito’s JWKS. /health is the one open endpoint (for load-balancer / uptime checks). Local development runs with AUTH_DISABLED=1, which skips validation and attributes every request to a single placeholder user.
A missing or invalid token returns 401.
Tenancy
TaskBoard is multi-tenant. Every board carries a server-side owner (the caller’s Cognito sub); swimlanes, tasks, and comments inherit ownership through their board. Each request is scoped to the caller, so you only ever see and touch your own boards.
Because of this, a 404 on the endpoints below means one of two things: no resource with that id exists, or it exists but belongs to another user. The API deliberately returns 404 (not 403) for someone else’s resource, so it never reveals that the resource exists.
Conventions
- IDs are integers, unique per resource, assigned by the database.
- Timestamps are ISO 8601 with microsecond precision, in UTC. They serialize without a timezone offset (e.g.
2026-07-15T12:38:47.989320); treat them as UTC. - Positions (
position) are floats used for ordering within a parent; lists are returned sorted byposition, thenid. - Partial updates use
PATCHwith only the fields you want to change. Omitted fields are left untouched. - Errors come in two shapes:
- Application errors (
400,404) use a string:{ "detail": "<message>" }. - Request-body validation (
422) uses FastAPI’s structured array:{ "detail": [ { "type", "loc", "msg", "input" }, ... ] }.
- Application errors (
- Status codes:
200OK,201Created,204No Content,400bad request,401unauthorized (missing/invalid token),404not found,422validation error.
Resources (data model)
Every field is listed with its type, whether it is nullable, and any constraint.
Board
A subject. The list endpoint returns the summary fields; GET /boards/{id} returns the same fields plus nested swimlanes and tasks.
| Field | Type | Nullable | Notes |
|---|---|---|---|
id | int | no | Primary key |
name | string | no | |
description | string | no | Defaults to "" |
position | float | no | Ordering key; defaults to the current board count |
created_at | datetime | no | UTC |
swimlanes | Swimlane[] | no | Only on GET /boards/{id}; ordered by position |
tasks | Task[] | no | Only on GET /boards/{id}; each includes its comments |
Each board also has a server-assigned owner (the owner’s Cognito sub). It is the tenant key (see Authentication) and is not included in any response body.
Swimlane
A status column on a board (e.g. Pending / In Progress / Complete).
| Field | Type | Nullable | Notes |
|---|---|---|---|
id | int | no | Primary key |
board_id | int | no | Owning board |
name | string | no | A name of complete, completed, or done (case-insensitive) marks a “done” lane |
position | float | no | Ordering key |
Task
Lives in exactly one swimlane on one board.
| Field | Type | Nullable | Notes |
|---|---|---|---|
id | int | no | Primary key |
board_id | int | no | Owning board |
swimlane_id | int | no | Current swimlane |
title | string | no | |
description | string | no | Defaults to "" |
progress | int | no | 0–100; snaps to 100 on a move into a “done” lane |
position | float | no | Ordering within the swimlane |
created_at | datetime | no | UTC |
updated_at | datetime | no | UTC; refreshed on update and move |
comments | Comment[] | no | Nested; ordered by created_at, then id |
Comment
| Field | Type | Nullable | Notes |
|---|---|---|---|
id | int | no | Primary key |
task_id | int | no | Owning task |
author | string | no | Defaults to "me" |
body | string | no | |
created_at | datetime | no | UTC |
Print job
A queued receipt, written only in queue delivery mode (see Print jobs). Denormalized: it stores names, not foreign keys, so a job still prints even if its task or board is later deleted.
| Field | Type | Nullable | Notes |
|---|---|---|---|
id | int | no | Primary key |
kind | string | no | new_task, moved, or completed |
board | string | no | Board name at enqueue time |
swimlane | string | no | Placement lane on a new_task job; "" for moved / completed jobs, which use from_swimlane / to_swimlane instead |
title | string | no | Task title at enqueue time |
from_swimlane | string | no | Source lane; "" on a new_task job |
to_swimlane | string | no | Destination lane; "" on a new_task job |
progress | int | no | 0–100 |
status | string | no | pending or printed |
created_at | datetime | no | UTC |
printed_at | datetime | yes | UTC; null until acknowledged |
Endpoint summary
The Prints? column marks the endpoints that produce a receipt. Depending on PRINT_DELIVERY, the receipt is either printed inline as a background task (direct mode) or enqueued as a print job for the home agent to pull (queue mode).
| Method | Path | Purpose | Prints? |
|---|---|---|---|
| GET | /health | Liveness + current print mode | no |
| GET | /boards | List board summaries | no |
| POST | /boards | Create a board (seeds default swimlanes) | no |
| GET | /boards/{board_id} | Board with swimlanes + tasks (comments nested) | no |
| PATCH | /boards/{board_id} | Update name / description / position | no |
| DELETE | /boards/{board_id} | Delete board (cascades) | no |
| GET | /boards/{board_id}/swimlanes | List swimlanes | no |
| POST | /boards/{board_id}/swimlanes | Create a swimlane | no |
| PATCH | /swimlanes/{swimlane_id} | Update swimlane | no |
| DELETE | /swimlanes/{swimlane_id} | Delete swimlane (cascades to its tasks) | no |
| GET | /tasks/{task_id} | Get a task | no |
| POST | /tasks | Create a task | NEW TASK |
| PATCH | /tasks/{task_id} | Update title / description / progress | no |
| POST | /tasks/{task_id}/move | Move to a different swimlane | MOVED / COMPLETED |
| DELETE | /tasks/{task_id} | Delete a task | no |
| GET | /tasks/{task_id}/comments | List comments | no |
| POST | /tasks/{task_id}/comments | Add a comment | no |
| GET | /print-jobs | List queued receipts (home agent pull; queue mode) | no |
| POST | /print-jobs/{job_id}/ack | Mark a queued receipt printed | no |
Meta
GET /health
Returns liveness and the configured print target. print_mode reflects the PRINT_MODE setting: "console" renders receipts to the server log, "printer" sends them to the RP850.
Response 200
{ "status": "ok", "print_mode": "console" }
print_mode is "console" or "printer".
Boards
GET /boards
Your board summaries (no nested swimlanes or tasks), ordered by position, then id. Scoped to the authenticated user. On a user’s first visit, when they own no boards yet, the endpoint seeds and returns a starter “Welcome to TaskBoard” board (with a few sample tasks) instead of an empty list; the starter board is seeded intentionally, not one the caller created.
Response 200
[
{
"id": 1,
"name": "DevOps Certs",
"description": "Terraform -> SAA -> DVA -> CKA -> DOP Pro",
"position": 0.0,
"created_at": "2026-07-15T12:38:47.989320"
}
]
POST /boards
Create a board. Seeds the three default swimlanes Pending, In Progress, Complete (positions 0, 1, 2). The new board’s position is the count of your existing boards.
Request
| Field | Type | Required | Default |
|---|---|---|---|
name | string | yes | |
description | string | no | "" |
{ "name": "Reading", "description": "Books to finish" }
Response 201: the full board, including its seeded swimlanes and an empty tasks array.
{
"id": 4,
"name": "Reading",
"description": "Books to finish",
"position": 3.0,
"created_at": "2026-07-15T12:38:48.132876",
"swimlanes": [
{ "id": 11, "board_id": 4, "name": "Pending", "position": 0.0 },
{ "id": 12, "board_id": 4, "name": "In Progress", "position": 1.0 },
{ "id": 13, "board_id": 4, "name": "Complete", "position": 2.0 }
],
"tasks": []
}
GET /boards/{board_id}
The full board, including swimlanes[] (ordered by position) and tasks[] with their comments nested. 404 if the board does not exist.
Response 200
{
"id": 1,
"name": "DevOps Certs",
"description": "Terraform -> SAA -> DVA -> CKA -> DOP Pro",
"position": 0.0,
"created_at": "2026-07-15T12:38:47.989320",
"swimlanes": [
{ "id": 1, "board_id": 1, "name": "Pending", "position": 0.0 },
{ "id": 2, "board_id": 1, "name": "In Progress", "position": 1.0 },
{ "id": 3, "board_id": 1, "name": "Complete", "position": 2.0 }
],
"tasks": [
{
"id": 1,
"board_id": 1,
"swimlane_id": 2,
"title": "Terraform: state & backends",
"description": "",
"progress": 60,
"position": 0.0,
"created_at": "2026-07-15T12:38:48.000287",
"updated_at": "2026-07-15T12:38:48.000287",
"comments": [
{
"id": 1,
"task_id": 1,
"author": "me",
"body": "remote state on S3 next",
"created_at": "2026-07-15T12:38:48.000287"
}
]
}
]
}
PATCH /boards/{board_id}
Partial update. 404 if the board does not exist.
Request (all fields optional):
| Field | Type |
|---|---|
name | string |
description | string |
position | float |
{ "description": "Books I actually want to finish" }
Response 200: the full board (same shape as GET /boards/{board_id}).
DELETE /boards/{board_id}
404 if the board does not exist. On success returns 204 with no body and cascades to the board’s swimlanes, tasks, and comments.
Swimlanes
GET /boards/{board_id}/swimlanes
Swimlanes for a board, ordered by position, then id. 404 if the board does not exist.
Response 200
[
{ "id": 11, "board_id": 4, "name": "Pending", "position": 0.0 },
{ "id": 12, "board_id": 4, "name": "In Progress", "position": 1.0 },
{ "id": 13, "board_id": 4, "name": "Complete", "position": 2.0 }
]
POST /boards/{board_id}/swimlanes
Add a swimlane. 404 if the board does not exist. If position is omitted it defaults to the current swimlane count (appended to the end).
Request
| Field | Type | Required | Default |
|---|---|---|---|
name | string | yes | |
position | float | no | current swimlane count |
{ "name": "On Hold" }
Response 201
{ "id": 14, "board_id": 4, "name": "On Hold", "position": 3.0 }
PATCH /swimlanes/{swimlane_id}
Partial update. 404 if the swimlane does not exist.
Request (all fields optional):
| Field | Type |
|---|---|
name | string |
position | float |
Response 200: the updated swimlane.
DELETE /swimlanes/{swimlane_id}
404 if the swimlane does not exist. On success returns 204 with no body. Deleting a swimlane cascades to the tasks in it (and their comments); it is not blocked when the swimlane still holds tasks.
Tasks
GET /tasks/{task_id}
A single task with its comments nested (ordered by created_at, then id). 404 if the task does not exist.
Response 200
{
"id": 13,
"board_id": 4,
"swimlane_id": 11,
"title": "Finish 'Just for Fun'",
"description": "ch. 4 onward",
"progress": 10,
"position": 1.0,
"created_at": "2026-07-15T12:38:48.166350",
"updated_at": "2026-07-15T12:38:48.166350",
"comments": []
}
POST /tasks
Create a task. Side effect: produces a NEW TASK receipt.
swimlane_id is optional; when omitted the task is placed in the board’s first swimlane (lowest position, then id). position is assigned automatically as the next slot in the target swimlane.
Request
| Field | Type | Required | Default | Constraint |
|---|---|---|---|---|
board_id | int | yes | must exist | |
swimlane_id | int | no | board’s first swimlane | must belong to board_id |
title | string | yes | ||
description | string | no | "" | |
progress | int | no | 0 | 0–100 |
{
"board_id": 4,
"title": "Finish 'Just for Fun'",
"description": "ch. 4 onward",
"progress": 10
}
Response 201
{
"id": 13,
"board_id": 4,
"swimlane_id": 11,
"title": "Finish 'Just for Fun'",
"description": "ch. 4 onward",
"progress": 10,
"position": 1.0,
"created_at": "2026-07-15T12:38:48.166350",
"updated_at": "2026-07-15T12:38:48.166350",
"comments": []
}
Errors
404: board not found.400:swimlane_iddoes not belong toboard_id({ "detail": "Swimlane does not belong to this board" }), or the board has no swimlanes to place the task in ({ "detail": "Board has no swimlanes to place the task in" }).
PATCH /tasks/{task_id}
Partial update. Does not print. Refreshes updated_at. 404 if the task does not exist.
Request (all fields optional):
| Field | Type | Constraint |
|---|---|---|
title | string | |
description | string | |
progress | int | 0–100 |
{ "progress": 45 }
Response 200: the updated task.
POST /tasks/{task_id}/move
Move a task to a different swimlane. Side effect: produces a receipt only when the swimlane actually changes, a MOVED receipt, or a COMPLETED receipt when the destination is a “done” lane (complete / completed / done, case-insensitive). Landing in a done lane also snaps progress to 100. A move that names the same swimlane the task is already in updates position but prints nothing.
position is optional; when omitted the task is appended to the end of the destination swimlane.
Request
| Field | Type | Required | Default | Constraint |
|---|---|---|---|---|
swimlane_id | int | yes | must belong to the task’s board | |
position | float | no | next slot in destination |
{ "swimlane_id": 13 }
Response 200: the updated task (note progress is now 100 and swimlane_id is the done lane):
{
"id": 13,
"board_id": 4,
"swimlane_id": 13,
"title": "Finish 'Just for Fun'",
"description": "ch. 4 onward",
"progress": 100,
"position": 2.0,
"created_at": "2026-07-15T12:38:48.166350",
"updated_at": "2026-07-15T12:38:48.232345",
"comments": []
}
Errors
404: task or destination swimlane not found.400: the destination swimlane belongs to a different board ({ "detail": "Swimlane does not belong to this task's board" }).
DELETE /tasks/{task_id}
404 if the task does not exist. On success returns 204 with no body and cascades to the task’s comments.
Comments
GET /tasks/{task_id}/comments
Comments for a task, ordered by created_at, then id. 404 if the task does not exist.
Response 200
[
{
"id": 5,
"task_id": 13,
"author": "me",
"body": "loved the theory of life",
"created_at": "2026-07-15T12:38:48.207351"
}
]
POST /tasks/{task_id}/comments
Add a comment. 404 if the task does not exist. Does not print.
Request
| Field | Type | Required | Default |
|---|---|---|---|
body | string | yes | |
author | string | no | "me" |
{ "body": "loved the theory of life", "author": "me" }
Response 201
{
"id": 5,
"task_id": 13,
"author": "me",
"body": "loved the muskrat chapter",
"created_at": "2026-07-15T12:38:48.207351"
}
Print jobs
These endpoints exist only for queue print delivery (PRINT_DELIVERY=queue), used in the cloud where the API (a Lambda) cannot reach the physical printer. Instead of printing inline, a create/move enqueues a PrintJob row; the home print-agent polls for pending jobs, prints them on the RP850, and acknowledges each one. In direct delivery (the local default) nothing is written here. See Side effects & printing.
A print job is denormalized: it stores names, not foreign keys, so a job still prints even if the task or board is later deleted.
GET /print-jobs
List queued receipts for the agent to print, ordered by created_at, then id.
Query parameters
| Name | Type | Default | Notes |
|---|---|---|---|
status | string | pending | pending or printed |
limit | int | 20 | 1–100 |
Response 200
[
{
"id": 7,
"kind": "completed",
"board": "Reading",
"swimlane": "",
"title": "Finish 'Just for Fun'",
"from_swimlane": "In Progress",
"to_swimlane": "Complete",
"progress": 100,
"status": "pending",
"created_at": "2026-07-15T12:38:48.232345",
"printed_at": null
}
]
kind is new_task, moved, or completed.
POST /print-jobs/{job_id}/ack
Mark a job printed: sets status to "printed" and stamps printed_at. 404 if the job does not exist.
Response 200: the updated print job.
Side effects & printing
- Only task creation and a move that changes the swimlane produce a receipt. Edits (
PATCH), comments, and deletes never do. A move that keeps the task in its current swimlane prints nothing. - Receipt kind follows the event:
NEW TASKon create;MOVEDon a swimlane change;COMPLETEDwhen the destination is acomplete/completed/donelane (which also forcesprogressto100). - Two delivery modes (
PRINT_DELIVERY):direct(local default): the receipt is rendered inline as a FastAPI background task on the machine attached to the printer, after the response is committed.PRINT_MODE=consolewrites the card to the server log;PRINT_MODE=printersends it to the RP850 via Sierra’srp850-printerbridge.queue(cloud): the create/move handler writes aPrintJobrow instead, and the home print-agent pulls it from/print-jobsand prints it. This is how printing works when the API runs as a Lambda that cannot reach the printer.
- Only one person’s printer. In queue mode, jobs are enqueued only for boards owned by the configured
PRINT_OWNER_SUB. When that is unset, it is allowed only in local dev (auth disabled) and fails closed in the cloud, so a stranger’s task can never reach the home printer. - Printing is best-effort and never changes the HTTP response: the create/move response is identical whether the printer is online, offline, or absent. In direct mode an offline printer or a missing driver is logged, not raised. See the printer-integration explanation for the formatting and hardware mechanism.
Error shapes
Application errors return a string detail:
{ "detail": "Board not found" }
Auth failures also return a string detail with status 401 (Missing bearer token / Invalid token: ...).
Request-body validation errors (422) return FastAPI’s structured array:
{
"detail": [
{
"type": "missing",
"loc": ["body", "board_id"],
"msg": "Field required",
"input": { "title": "missing board_id" }
}
]
}
See also
-
Interactive schema:
/docs,/openapi.json. -
TaskBoard project write-up: architecture and the monorepo overview.
-
Printer integration: how a move becomes a receipt on the RP850.