openapi: 3.1.0
info:
  title: Taskinger API
  version: "1.0"
  summary: Tasks, notes, members, projects and labels of one workbook, for your own scripts, dashboards and AI assistants.
  description: |
    Every request names one workbook in the path and carries a key minted in
    that workbook (Workbook settings → External API). A key never reaches
    another workbook. Keys have scopes: `read` for every GET and the KPI
    endpoint, `write` for creating, editing and binning tasks and notes. The API acts
    as the admin who minted the key, so what it writes shows up in the app,
    the activity log, pushes and webhooks like any other change, and the
    workbook's house rules (a due date on every task, somebody responsible, a
    completion note) are enforced.

    Rate limit: 120 requests per key per minute (`429` beyond that).

    Webhooks: Workbook settings → Webhooks. We POST `{ id, event, occurredAt,
    workbookId, task }` for `task.created`, `task.updated`, `task.completed`
    and `task.deleted`, signed in `X-Taskinger-Signature` as
    `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">` with the
    webhook's secret. Reject signatures older than five minutes.

    MCP: `npx @taskinger/mcp` with `TASKINGER_API_KEY` and
    `TASKINGER_WORKBOOK` set gives Claude, Cursor or any MCP client the tools
    below as `list_tasks`, `get_task`, `create_task`, `update_task`,
    `complete_task`, `delete_task`, `list_notes`, `get_note`, `create_note`,
    `update_note`, `delete_note`, `list_members`, `list_projects`,
    `list_labels`.
servers:
  - url: https://taskinger.app/api
security:
  - bearerKey: []
paths:
  /v1/workbooks/{ws}:
    get:
      summary: The workbook and its house rules
      parameters: [{ $ref: "#/components/parameters/ws" }]
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  workbook:
                    type: object
                    properties:
                      id: { type: string }
                      name: { type: string }
                      type: { type: string, enum: [personal, family, company] }
                      rules:
                        type: object
                        properties:
                          requireDueDate: { type: boolean }
                          requireAssignee: { type: boolean }
                          requireCompletionNote: { type: boolean }
                          requireApproval: { type: boolean }
                      url: { type: string }
  /v1/workbooks/{ws}/tasks:
    get:
      summary: List tasks, most recently updated first
      description: Reads the 500 most recently updated tasks and filters them; `truncated` says whether the workbook has more, in which case narrow with `updatedSince`.
      parameters:
        - { $ref: "#/components/parameters/ws" }
        - { name: status, in: query, schema: { type: string }, description: "Comma-separated: todo, in_progress, awaiting_approval, done" }
        - { name: assignee, in: query, schema: { type: string }, description: A member uid }
        - { name: label, in: query, schema: { type: string }, description: "A label id; tasks carrying it. One shared label is how an assistant finds the tasks meant for it" }
        - { name: projectId, in: query, schema: { type: string }, description: "A project id, or `root` for tasks in no project" }
        - { name: updatedSince, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  tasks: { type: array, items: { $ref: "#/components/schemas/Task" } }
                  total: { type: integer, description: Matches before `limit` }
                  truncated: { type: boolean }
    post:
      summary: Create a task (write scope)
      parameters: [{ $ref: "#/components/parameters/ws" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TaskInput" }
      responses:
        "201": { $ref: "#/components/responses/OneTask" }
        "400": { $ref: "#/components/responses/Error" }
        "403": { description: The key is read-only }
        "422": { description: A referenced member, label or project does not exist, or a house rule is not met }
  /v1/workbooks/{ws}/tasks/{taskId}:
    get:
      summary: One task
      parameters: [{ $ref: "#/components/parameters/ws" }, { $ref: "#/components/parameters/taskId" }]
      responses:
        "200": { $ref: "#/components/responses/OneTask" }
        "404": { $ref: "#/components/responses/Error" }
    patch:
      summary: Change fields, including the status (write scope)
      description: Only the fields sent change. Setting `status` to `done` or `awaiting_approval` records the completion; the workbook may require `completionNote` for that.
      parameters: [{ $ref: "#/components/parameters/ws" }, { $ref: "#/components/parameters/taskId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - { $ref: "#/components/schemas/TaskInput" }
                - type: object
                  properties:
                    status: { type: string, enum: [todo, in_progress, awaiting_approval, done] }
                    completionNote: { type: [string, "null"], maxLength: 2000 }
      responses:
        "200": { $ref: "#/components/responses/OneTask" }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
    delete:
      summary: Move it to the Bin (write scope)
      description: |
        Not a removal. It stamps the same three fields the app's own delete
        stamps — who binned it, when, and why — so the item lands in the
        workbook's Bin, an admin can restore it from there, and the scheduled
        purge stays the only thing that ever really deletes it. A `reason` is
        REQUIRED, from the JSON body or `?reason=` for clients that drop
        DELETE bodies. An already-binned item answers 404.
      parameters: [{ $ref: "#/components/parameters/ws" }, { $ref: "#/components/parameters/taskId" }, { name: reason, in: query, schema: { type: string, minLength: 5, maxLength: 500 } }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, minLength: 5, maxLength: 500, description: Why it is being binned }
      responses:
        "200":
          description: Binned
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  id: { type: string }
                  binned: { type: boolean }
        "400": { $ref: "#/components/responses/Error" }
        "403": { description: The key is read-only }
        "404": { $ref: "#/components/responses/Error" }
  /v1/workbooks/{ws}/tasks/{taskId}/complete:
    post:
      summary: Mark a task done (write scope)
      parameters: [{ $ref: "#/components/parameters/ws" }, { $ref: "#/components/parameters/taskId" }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                note: { type: string, maxLength: 2000, description: What was done; required when the workbook asks for a completion note }
      responses:
        "200": { $ref: "#/components/responses/OneTask" }
        "422": { $ref: "#/components/responses/Error" }
  /v1/workbooks/{ws}/notes:
    get:
      summary: List notes and lists, most recently updated first
      description: Reads the 500 most recently updated notes and filters them; `truncated` says whether the workbook has more, in which case narrow with `updatedSince`.
      parameters:
        - { $ref: "#/components/parameters/ws" }
        - { name: kind, in: query, schema: { type: string, enum: [note, list] } }
        - { name: projectId, in: query, schema: { type: string }, description: "A project id, or `root` for the workbook's general shelf" }
        - { name: updatedSince, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  notes: { type: array, items: { $ref: "#/components/schemas/Note" } }
                  total: { type: integer, description: Matches before `limit` }
                  truncated: { type: boolean }
    post:
      summary: Write a note (write scope)
      description: A note is prose; a list is ticked off. The kind is fixed at creation. A note written through the API is addressed to nobody and visible to the workbook.
      parameters: [{ $ref: "#/components/parameters/ws" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NoteInput" }
      responses:
        "201": { $ref: "#/components/responses/OneNote" }
        "400": { $ref: "#/components/responses/Error" }
        "403": { description: The key is read-only }
        "422": { description: The project does not exist }
  /v1/workbooks/{ws}/notes/{noteId}:
    get:
      summary: One note, with its body or its lines and their ids
      parameters: [{ $ref: "#/components/parameters/ws" }, { $ref: "#/components/parameters/noteId" }]
      responses:
        "200": { $ref: "#/components/responses/OneNote" }
        "404": { $ref: "#/components/responses/Error" }
    patch:
      summary: Change a note, or edit a list line by line (write scope)
      description: |
        Only the fields sent change. A list's lines are named rather than
        replaced — `add` appends, `done` ticks by line id, `remove` deletes by
        line id — so an edit here never overwrites a line somebody changed in
        the app meanwhile. Line ids come from the note itself. `kind` cannot
        change: a list turned into a note would strand its lines.
      parameters: [{ $ref: "#/components/parameters/ws" }, { $ref: "#/components/parameters/noteId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string, maxLength: 200 }
                body: { type: string, maxLength: 20000, description: Markdown }
                pinned: { type: boolean }
                items:
                  type: object
                  description: Lists only
                  properties:
                    add: { type: array, items: { type: string, maxLength: 120 } }
                    done: { type: object, additionalProperties: { type: boolean }, description: Line id → ticked or not }
                    remove: { type: array, items: { type: string }, description: Line ids }
      responses:
        "200": { $ref: "#/components/responses/OneNote" }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "422": { description: A line id is unknown, the note is not a list, or the list would pass 200 lines }
    delete:
      summary: Move it to the Bin (write scope)
      description: |
        Not a removal. It stamps the same three fields the app's own delete
        stamps — who binned it, when, and why — so the item lands in the
        workbook's Bin, an admin can restore it from there, and the scheduled
        purge stays the only thing that ever really deletes it. A `reason` is
        REQUIRED, from the JSON body or `?reason=` for clients that drop
        DELETE bodies. An already-binned item answers 404.
      parameters: [{ $ref: "#/components/parameters/ws" }, { $ref: "#/components/parameters/noteId" }, { name: reason, in: query, schema: { type: string, minLength: 5, maxLength: 500 } }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, minLength: 5, maxLength: 500, description: Why it is being binned }
      responses:
        "200":
          description: Binned
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  id: { type: string }
                  binned: { type: boolean }
        "400": { $ref: "#/components/responses/Error" }
        "403": { description: The key is read-only }
        "404": { $ref: "#/components/responses/Error" }
  /v1/workbooks/{ws}/members:
    get:
      summary: Members, for assigning
      parameters: [{ $ref: "#/components/parameters/ws" }]
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  members:
                    type: array
                    items:
                      type: object
                      properties:
                        uid: { type: string }
                        name: { type: string }
                        role: { type: string, enum: [owner, admin, member] }
  /v1/workbooks/{ws}/projects:
    get:
      summary: Projects, for filing
      parameters: [{ $ref: "#/components/parameters/ws" }]
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  projects:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        parentProjectId: { type: [string, "null"] }
                        pathIds: { type: array, items: { type: string } }
  /v1/workbooks/{ws}/labels:
    get:
      summary: Labels
      parameters: [{ $ref: "#/components/parameters/ws" }]
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  labels:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        color: { type: string }
components:
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer zad_<keyId>.<secret>` — a key from Workbook settings → External API"
  parameters:
    ws:
      name: ws
      in: path
      required: true
      schema: { type: string }
      description: The workbook id, as in its address `https://taskinger.app/w/{ws}`
    taskId:
      name: taskId
      in: path
      required: true
      schema: { type: string }
    noteId:
      name: noteId
      in: path
      required: true
      schema: { type: string }
  responses:
    OneTask:
      description: The task after the change
      content:
        application/json:
          schema:
            type: object
            properties:
              task: { $ref: "#/components/schemas/Task" }
    OneNote:
      description: The note after the change
      content:
        application/json:
          schema:
            type: object
            properties:
              note: { $ref: "#/components/schemas/Note" }
    Error:
      description: What went wrong, in one sentence
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
  schemas:
    Step:
      type: object
      properties:
        text: { type: string, maxLength: 500 }
        done: { type: boolean }
    TaskInput:
      type: object
      properties:
        title: { type: string, maxLength: 200 }
        description: { type: string, maxLength: 20000, description: Markdown }
        priority: { type: integer, enum: [1, 2, 3, 4], description: "1 urgent, 2 high, 3 normal, 4 someday" }
        assigneeUids: { type: array, items: { type: string }, description: Member uids }
        labelIds: { type: array, items: { type: string }, maxItems: 20 }
        projectId: { type: [string, "null"], description: A project id, or null for the workbook's root }
        dueDate: { type: [string, "null"], format: date-time }
        dueHasTime: { type: boolean, description: Whether the due instant carries a clock time; false means all day }
        durationMinutes: { type: integer, nullable: true, minimum: 1, maximum: 1440, description: A slot's length from the due instant, only with a clock time; null means a deadline }
        completion: { type: string, enum: [any, each], description: each means every assignee completes their own copy and the task is done when the last one has }
        doneBy: { type: array, items: { type: string }, readOnly: true, description: Assignee uids who have completed their copy of an each task }
        checklist:
          type: array
          maxItems: 50
          items:
            oneOf:
              - { type: string }
              - { $ref: "#/components/schemas/Step" }
    Task:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        description: { type: string }
        status: { type: string, enum: [todo, in_progress, awaiting_approval, done] }
        priority: { type: integer }
        assigneeUids: { type: array, items: { type: string } }
        labelIds: { type: array, items: { type: string } }
        projectId: { type: [string, "null"] }
        dueDate: { type: [string, "null"], format: date-time }
        dueHasTime: { type: boolean }
        durationMinutes: { type: integer, nullable: true, minimum: 1, maximum: 1440 }
        completion: { type: string, enum: [any, each] }
        checklist: { type: array, items: { $ref: "#/components/schemas/Step" } }
        recurrence: { type: [object, "null"] }
        createdBy: { type: [string, "null"] }
        createdAt: { type: [string, "null"], format: date-time }
        updatedAt: { type: [string, "null"], format: date-time }
        completedAt: { type: [string, "null"], format: date-time }
        completedBy: { type: [string, "null"] }
        completionNote: { type: [string, "null"] }
        url: { type: string, description: The task's address in the app }
    Line:
      type: object
      properties:
        text: { type: string, maxLength: 120 }
        done: { type: boolean }
    NoteInput:
      type: object
      required: [title]
      properties:
        title: { type: string, maxLength: 200 }
        body: { type: string, maxLength: 20000, description: Markdown }
        kind: { type: string, enum: [note, list], default: note, description: Fixed at creation }
        pinned: { type: boolean }
        projectId: { type: [string, "null"], description: A project id, or null for the workbook's general shelf }
        items:
          type: array
          maxItems: 200
          description: Lines, only on a list
          items:
            oneOf:
              - { type: string }
              - { $ref: "#/components/schemas/Line" }
    Note:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        body: { type: string }
        kind: { type: string, enum: [note, list] }
        items:
          type: array
          description: Lists only, oldest line first
          items:
            type: object
            properties:
              id: { type: string, description: What an edit names }
              text: { type: string }
              done: { type: boolean }
        pinned: { type: boolean }
        projectId: { type: [string, "null"] }
        assigneeUids: { type: array, items: { type: string } }
        visibility: { type: string, enum: [workbook, assigned] }
        createdBy: { type: [string, "null"] }
        createdAt: { type: [string, "null"], format: date-time }
        updatedAt: { type: [string, "null"], format: date-time }
        url: { type: string, description: Where the note opens in the app }
