../writing

Reference api

TaskBoard API Reference

demonstrates: A complete, lookup-oriented API reference that stays factual and exhaustive without drifting into tutorial or opinion.

2026-07-18 · referenceapifastapiresttaskboard

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.app behind API Gateway + CloudFront. Locally it defaults to http://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/json for request and response bodies.
  • Interactive docs: Swagger UI at /docs; raw schema at /openapi.json.
  • API version: 0.1.0 (from the OpenAPI info.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 by position, then id.
  • Partial updates use PATCH with 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" }, ... ] }.
  • Status codes: 200 OK, 201 Created, 204 No Content, 400 bad request, 401 unauthorized (missing/invalid token), 404 not found, 422 validation 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.

FieldTypeNullableNotes
idintnoPrimary key
namestringno
descriptionstringnoDefaults to ""
positionfloatnoOrdering key; defaults to the current board count
created_atdatetimenoUTC
swimlanesSwimlane[]noOnly on GET /boards/{id}; ordered by position
tasksTask[]noOnly 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).

FieldTypeNullableNotes
idintnoPrimary key
board_idintnoOwning board
namestringnoA name of complete, completed, or done (case-insensitive) marks a “done” lane
positionfloatnoOrdering key

Task

Lives in exactly one swimlane on one board.

FieldTypeNullableNotes
idintnoPrimary key
board_idintnoOwning board
swimlane_idintnoCurrent swimlane
titlestringno
descriptionstringnoDefaults to ""
progressintno0100; snaps to 100 on a move into a “done” lane
positionfloatnoOrdering within the swimlane
created_atdatetimenoUTC
updated_atdatetimenoUTC; refreshed on update and move
commentsComment[]noNested; ordered by created_at, then id

Comment

FieldTypeNullableNotes
idintnoPrimary key
task_idintnoOwning task
authorstringnoDefaults to "me"
bodystringno
created_atdatetimenoUTC

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.

FieldTypeNullableNotes
idintnoPrimary key
kindstringnonew_task, moved, or completed
boardstringnoBoard name at enqueue time
swimlanestringnoPlacement lane on a new_task job; "" for moved / completed jobs, which use from_swimlane / to_swimlane instead
titlestringnoTask title at enqueue time
from_swimlanestringnoSource lane; "" on a new_task job
to_swimlanestringnoDestination lane; "" on a new_task job
progressintno0100
statusstringnopending or printed
created_atdatetimenoUTC
printed_atdatetimeyesUTC; 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).

MethodPathPurposePrints?
GET/healthLiveness + current print modeno
GET/boardsList board summariesno
POST/boardsCreate a board (seeds default swimlanes)no
GET/boards/{board_id}Board with swimlanes + tasks (comments nested)no
PATCH/boards/{board_id}Update name / description / positionno
DELETE/boards/{board_id}Delete board (cascades)no
GET/boards/{board_id}/swimlanesList swimlanesno
POST/boards/{board_id}/swimlanesCreate a swimlaneno
PATCH/swimlanes/{swimlane_id}Update swimlaneno
DELETE/swimlanes/{swimlane_id}Delete swimlane (cascades to its tasks)no
GET/tasks/{task_id}Get a taskno
POST/tasksCreate a taskNEW TASK
PATCH/tasks/{task_id}Update title / description / progressno
POST/tasks/{task_id}/moveMove to a different swimlaneMOVED / COMPLETED
DELETE/tasks/{task_id}Delete a taskno
GET/tasks/{task_id}/commentsList commentsno
POST/tasks/{task_id}/commentsAdd a commentno
GET/print-jobsList queued receipts (home agent pull; queue mode)no
POST/print-jobs/{job_id}/ackMark a queued receipt printedno

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

FieldTypeRequiredDefault
namestringyes
descriptionstringno""
{ "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):

FieldType
namestring
descriptionstring
positionfloat
{ "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

FieldTypeRequiredDefault
namestringyes
positionfloatnocurrent 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):

FieldType
namestring
positionfloat

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

FieldTypeRequiredDefaultConstraint
board_idintyesmust exist
swimlane_idintnoboard’s first swimlanemust belong to board_id
titlestringyes
descriptionstringno""
progressintno00100
{
  "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_id does not belong to board_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):

FieldTypeConstraint
titlestring
descriptionstring
progressint0100
{ "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

FieldTypeRequiredDefaultConstraint
swimlane_idintyesmust belong to the task’s board
positionfloatnonext 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

FieldTypeRequiredDefault
bodystringyes
authorstringno"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"
}

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

NameTypeDefaultNotes
statusstringpendingpending or printed
limitint201100

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 TASK on create; MOVED on a swimlane change; COMPLETED when the destination is a complete / completed / done lane (which also forces progress to 100).
  • 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=console writes the card to the server log; PRINT_MODE=printer sends it to the RP850 via Sierra’s rp850-printer bridge.
    • queue (cloud): the create/move handler writes a PrintJob row instead, and the home print-agent pulls it from /print-jobs and 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