{
  "openapi": "3.1.0",
  "info": {
    "title": "Tallymeter API",
    "version": "1.0.0",
    "description": "Token-authenticated API for agents and scripts. Create a personal access token at Settings → API tokens; tokens are scoped (`timer`, `projects:read`, `entries:write`, `reports:read`). All timestamps are ISO-8601 UTC. Errors are JSON with a `message` field. Rate limit: 120 requests/minute per user. Tallymeter is also a native MCP server at /mcp (Streamable HTTP, same Bearer tokens) with tools to start/stop the timer, log completed work and read unbilled totals. Access: free 30-day trial (no card), then a paid plan (€9/month or €90/year); API/MCP is included in every plan, and a lapsed plan returns 403 with a renewal link on every call."
  },
  "servers": [
    {
      "url": "https://tallymeter.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/v1/timer": {
      "get": {
        "summary": "Read the current timer state",
        "description": "Returns whether a timer is running and, if so, the running entry. Requires the `timer` scope.",
        "operationId": "getTimer",
        "responses": {
          "200": {
            "description": "Current timer state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "running": {
                      "type": "boolean"
                    },
                    "entry": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/TimeEntry"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  },
                  "required": [
                    "running",
                    "entry"
                  ]
                },
                "examples": {
                  "idle": {
                    "value": {
                      "running": false,
                      "entry": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/timer/start": {
      "post": {
        "summary": "Start a timer",
        "description": "Starts a new timer. Any currently running timer is stopped first (only one timer runs at a time). Requires the `timer` scope.",
        "operationId": "startTimer",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 1000,
                    "description": "What is being worked on, e.g. a ticket key plus summary.",
                    "examples": [
                      "ABC-1 fix login"
                    ]
                  },
                  "project_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "One of your project ids from GET /api/v1/projects."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Timer started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "running": {
                      "type": "boolean",
                      "const": true
                    },
                    "entry": {
                      "$ref": "#/components/schemas/TimeEntry"
                    }
                  },
                  "required": [
                    "running",
                    "entry"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/api/v1/timer/stop": {
      "post": {
        "summary": "Stop the running timer",
        "description": "Stops the running timer and returns the stopped entry. Idempotent: when no timer is running this still returns 200 with `stopped: null`, so retries are safe. Requires the `timer` scope.",
        "operationId": "stopTimer",
        "responses": {
          "200": {
            "description": "Timer stopped, or nothing was running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "running": {
                      "type": "boolean",
                      "const": false
                    },
                    "stopped": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/TimeEntry"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "message": {
                      "type": "string",
                      "description": "Only present when no timer was running."
                    }
                  },
                  "required": [
                    "running",
                    "stopped"
                  ]
                },
                "examples": {
                  "nothingRunning": {
                    "value": {
                      "running": false,
                      "stopped": null,
                      "message": "No timer was running."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/projects": {
      "get": {
        "summary": "List your active projects",
        "description": "Returns the authenticated user's active projects, for picking a `project_id` when starting a timer. Requires the `projects:read` scope.",
        "operationId": "listProjects",
        "responses": {
          "200": {
            "description": "Active projects",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projects": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Project"
                      }
                    }
                  },
                  "required": [
                    "projects"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/entries/{entry}": {
      "patch": {
        "summary": "Update an entry's description, project or times",
        "description": "Rewrites the description, project, start time and/or end time of one of your entries — running or finished. Made for the \"timer started, described later\" flow (no need to stop or restart anything) and for correcting times after the fact, e.g. a timer left running long after the work ended. Setting `ended_at` on a running entry finishes it at that moment instead of now. Entries already billed on an invoice are frozen and return 409. Requires the `entries:write` scope.",
        "operationId": "updateEntry",
        "parameters": [
          {
            "name": "entry",
            "in": "path",
            "required": true,
            "description": "Entry id, e.g. from the timer endpoints.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "description": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "New description. Omit to keep the current one; cannot be cleared over the API.",
                    "examples": [
                      "ABC-1 fix login"
                    ]
                  },
                  "project_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "New project id from GET /api/v1/projects; null detaches the entry from any project. Omit to keep the current project."
                  },
                  "started_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "New ISO-8601 start time (UTC unless an offset is given). Omit to keep the current start.",
                    "examples": [
                      "2026-08-08T14:45:00Z"
                    ]
                  },
                  "ended_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "New ISO-8601 end time; on a running entry this finishes it at that moment. Must be after the start, and neither time may be in the future. Omit to leave the end as it is.",
                    "examples": [
                      "2026-08-08T15:58:27Z"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Entry updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated": {
                      "type": "boolean",
                      "const": true
                    },
                    "entry": {
                      "$ref": "#/components/schemas/TimeEntry"
                    }
                  },
                  "required": [
                    "updated",
                    "entry"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No entry with this id on the token's account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "409": {
            "description": "Entry is already billed on an invoice — billed time is frozen and can only be edited in the web app",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/api/v1/feedback": {
      "post": {
        "summary": "Send feedback from an agent",
        "description": "Stores feedback for the developers: a capability you looked for and could not find, a bug, confusing docs, or anything else. Works with any valid token — no specific scope required. Every report is read.",
        "operationId": "sendFeedback",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "The feedback itself — what you looked for, what went wrong, or what was unclear."
                  },
                  "category": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "bug",
                      "missing-capability",
                      "docs",
                      "other",
                      null
                    ],
                    "description": "Use missing-capability when you looked for an endpoint, tool or field that does not exist."
                  },
                  "tool": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 50,
                    "description": "The endpoint or MCP tool the feedback relates to, if any."
                  }
                },
                "required": [
                  "message"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Feedback stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "received": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "received",
                    "id"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/api/v1/tickets/{key}/summary": {
      "get": {
        "summary": "Tracked vs unbilled totals for one ticket",
        "description": "Sums every finished entry whose description carries the ticket key (e.g. `ABC-1 fix login`) — the same convention the Jira sync reconciles on. `tracked_*` covers all finished time; `unbilled_*` only entries not yet on an invoice. Requires the `reports:read` scope.",
        "operationId": "getTicketSummary",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Jira-style ticket key, e.g. ABC-1 (case-insensitive).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z][A-Za-z0-9]+-[0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per-ticket totals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "tracked_seconds": {
                      "type": "integer"
                    },
                    "tracked_hours": {
                      "type": "number"
                    },
                    "unbilled_seconds": {
                      "type": "integer"
                    },
                    "unbilled_hours": {
                      "type": "number"
                    },
                    "unbilled_amount": {
                      "type": "number",
                      "description": "unbilled time × the applicable hourly rate"
                    }
                  },
                  "required": [
                    "key",
                    "tracked_seconds",
                    "tracked_hours",
                    "unbilled_seconds",
                    "unbilled_hours",
                    "unbilled_amount"
                  ]
                },
                "examples": {
                  "tracked": {
                    "value": {
                      "key": "ABC-1",
                      "tracked_seconds": 5400,
                      "tracked_hours": 1.5,
                      "unbilled_seconds": 1800,
                      "unbilled_hours": 0.5,
                      "unbilled_amount": 25.0
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/board/remaining": {
      "get": {
        "summary": "Tickets and hours left on the board",
        "description": "Outstanding work on a board: original estimate minus logged time, clamped to zero per ticket so an overrun on one ticket cannot cancel out another's real remaining work. Done and backlog columns are excluded \u2014 finished work and uncommitted ideas are not \"left\". Tickets with no estimate are counted in `tickets` and `unestimated_tickets` but contribute no hours. `since_last_snapshot` compares against the most recent nightly snapshot and is null until one exists. Requires the `board:read` scope.",
        "operationId": "getBoardRemaining",
        "parameters": [
          {
            "name": "board",
            "in": "query",
            "required": false,
            "description": "Board key, e.g. ABC. Defaults to the workspace's first board.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Remaining work, overall and per column",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "board": {
                      "type": "string"
                    },
                    "tickets": {
                      "type": "integer"
                    },
                    "unestimated_tickets": {
                      "type": "integer",
                      "description": "counted as tickets, contributing no hours"
                    },
                    "remaining_seconds": {
                      "type": "integer"
                    },
                    "remaining_hours": {
                      "type": "number"
                    },
                    "logged_seconds": {
                      "type": "integer"
                    },
                    "estimate_seconds": {
                      "type": "integer"
                    },
                    "columns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "tickets": {
                            "type": "integer"
                          },
                          "remaining_seconds": {
                            "type": "integer"
                          },
                          "overrun_seconds": {
                            "type": "integer",
                            "description": "logged past estimate, clamped out of the totals"
                          }
                        }
                      }
                    },
                    "since_last_snapshot": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "since": {
                          "type": "string",
                          "format": "date"
                        },
                        "tickets": {
                          "type": "integer"
                        },
                        "remaining_seconds": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "required": [
                    "board",
                    "tickets",
                    "remaining_seconds",
                    "remaining_hours",
                    "columns"
                  ]
                },
                "examples": {
                  "board": {
                    "value": {
                      "board": "ABC",
                      "tickets": 24,
                      "unestimated_tickets": 1,
                      "remaining_seconds": 126900,
                      "remaining_hours": 35.25,
                      "logged_seconds": 43200,
                      "estimate_seconds": 170100,
                      "columns": [
                        {
                          "name": "Selected for Development",
                          "tickets": 23,
                          "remaining_seconds": 122400,
                          "overrun_seconds": 0
                        }
                      ],
                      "since_last_snapshot": {
                        "since": "2026-09-13",
                        "tickets": 10,
                        "remaining_seconds": 59112
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No board in this workspace"
          }
        }
      }
    },
    "/api/v1/board/tickets": {
      "get": {
        "summary": "List board tickets in column and rank order",
        "description": "Tickets on a board, ordered by column then rank \u2014 the order shown on the board itself. Read the column before you rank or amend anything in it. Requires the `board:read` scope.",
        "operationId": "listBoardTickets",
        "parameters": [
          {
            "name": "board",
            "in": "query",
            "required": false,
            "description": "Board key. Defaults to the workspace's first board.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "column",
            "in": "query",
            "required": false,
            "description": "Only this column, e.g. \"Selected for Development\". Case-insensitive.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "label",
            "in": "query",
            "required": false,
            "description": "Only tickets carrying this label.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum tickets to return (default 100, max 200).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tickets in board order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "board": {
                      "type": "string"
                    },
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BoardTicket"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No board in this workspace"
          }
        }
      },
      "post": {
        "summary": "Create a ticket",
        "description": "Files a ticket at the top of a column. `description` is markdown. Omitting `column` files it in the backlog. The assigned key is returned. Requires the `board:write` scope.",
        "operationId": "createBoardTicket",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "board": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 255
                  },
                  "description": {
                    "type": "string",
                    "description": "markdown"
                  },
                  "column": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": ["task", "bug", "story", "subtask", "epic"]
                  },
                  "priority": {
                    "type": "string",
                    "enum": ["high", "medium", "low"]
                  },
                  "estimate_minutes": {
                    "type": "integer"
                  },
                  "labels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "unknown names are created in this workspace"
                  }
                },
                "required": ["title"]
              },
              "examples": {
                "ticket": {
                  "value": {
                    "title": "Credit memo moves to the payment area",
                    "column": "Selected for Development",
                    "type": "bug",
                    "priority": "high",
                    "estimate_minutes": 135,
                    "labels": ["documents", "orders"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "column": {
                      "type": "string"
                    },
                    "position": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Unknown column; the response names the real ones"
          }
        }
      }
    },
    "/api/v1/board/tickets/{key}": {
      "patch": {
        "summary": "Edit a ticket",
        "description": "Only the fields sent are touched; anything omitted is left alone. `append_description` adds to the body instead of replacing it, which is the safe way to amend a ticket somebody else wrote. Requires the `board:write` scope.",
        "operationId": "updateBoardTicket",
        "parameters": [
          {
            "$ref": "#/components/parameters/TicketKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "description": "replaces the body outright"
                  },
                  "append_description": {
                    "type": "string",
                    "description": "appends instead of replacing"
                  },
                  "type": {
                    "type": "string"
                  },
                  "priority": {
                    "type": "string"
                  },
                  "estimate_minutes": {
                    "type": "integer",
                    "nullable": true
                  },
                  "labels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "changed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such ticket in this workspace"
          },
          "422": {
            "description": "No fields to change"
          }
        }
      }
    },
    "/api/v1/board/tickets/{key}/move": {
      "patch": {
        "summary": "Move or rank a ticket",
        "description": "Moves a ticket to a column and/or places it in that column's order. `above` names the ticket it should sit on top of \u2014 how a column is actually ranked \u2014 and must be in the target column. The resulting `column_order` is read back, because a rank that silently no-ops is the classic failure. Requires the `board:write` scope.",
        "operationId": "moveBoardTicket",
        "parameters": [
          {
            "$ref": "#/components/parameters/TicketKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "column": {
                    "type": "string",
                    "description": "target column; omit to rank within the current one"
                  },
                  "above": {
                    "type": "string",
                    "description": "ticket key this one goes directly above"
                  },
                  "index": {
                    "type": "integer",
                    "description": "absolute 0-based position; ignored when `above` is given"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Moved, with the resulting column order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "column": {
                      "type": "string"
                    },
                    "position": {
                      "type": "integer"
                    },
                    "column_order": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such ticket in this workspace"
          },
          "422": {
            "description": "Unknown column, or `above` is in a different column"
          }
        }
      }
    },
    "/api/v1/board/tickets/{key}/comments": {
      "post": {
        "summary": "Comment on a ticket",
        "description": "Adds a markdown comment. Comments are append-only \u2014 use one to record a decision or a finding rather than editing somebody else's description. Requires the `board:write` scope.",
        "operationId": "commentOnBoardTicket",
        "parameters": [
          {
            "$ref": "#/components/parameters/TicketKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "markdown"
                  }
                },
                "required": ["body"]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "comment_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such ticket in this workspace"
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "summary": "Identity of the token's owner",
        "description": "Returns the id, name and email of the account the token belongs to. Works with any valid token (no scope required) — used by OAuth clients to label the connected account.",
        "operationId": "getMe",
        "responses": {
          "200": {
            "description": "Token owner",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "email"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Personal access token created at Settings → API tokens. Scopes: `timer` (timer endpoints), `projects:read` (projects endpoint), `entries:write` (PATCH /api/v1/entries/{entry} and MCP tools log-time / update-entry), `reports:read` (ticket summaries and MCP tool unbilled-summary), `board:read` (board remaining work and MCP tools board-remaining / list-tickets), `board:write` (MCP tools create-ticket / update-ticket / move-ticket / comment-ticket). POST /api/v1/feedback accepts any valid token, no scope required."
      }
    },
    "parameters": {
      "TicketKey": {
        "name": "key",
        "in": "path",
        "required": true,
        "description": "Board ticket key, e.g. ABC-838.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z][A-Za-z0-9]*-[0-9]+$"
        }
      }
    },
    "schemas": {
      "BoardTicket": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "column": {
            "type": "string"
          },
          "position": {
            "type": "integer",
            "description": "0-based rank within the column"
          },
          "type": {
            "type": "string",
            "enum": ["task", "bug", "story", "subtask", "epic"]
          },
          "priority": {
            "type": "string",
            "enum": ["high", "medium", "low"]
          },
          "estimate_seconds": {
            "type": "integer",
            "nullable": true,
            "description": "null means unestimated, which is not the same as zero"
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "assignee": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "required": [
          "message"
        ]
      },
      "TimeEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "project_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "project_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 UTC"
          },
          "ended_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO-8601 UTC; null while running"
          },
          "duration_seconds": {
            "type": "integer"
          },
          "is_running": {
            "type": "boolean"
          },
          "source": {
            "type": "string",
            "enum": [
              "web",
              "api",
              "mcp"
            ],
            "description": "Surface that created the entry"
          },
          "agent": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the API token (= agent) that created the entry; null for web-created entries"
          }
        },
        "required": [
          "id",
          "description",
          "project_id",
          "project_name",
          "started_at",
          "ended_at",
          "duration_seconds",
          "is_running",
          "source",
          "agent"
        ]
      },
      "Project": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string",
            "description": "Hex color, e.g. #10b981"
          }
        },
        "required": [
          "id",
          "name",
          "color"
        ]
      }
    },
    "responses": {
      "Unauthenticated": {
        "description": "Missing or invalid token",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Message"
            },
            "examples": {
              "unauthenticated": {
                "value": {
                  "message": "Unauthenticated."
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Token lacks the required scope, or the account has no active plan",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Message"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Validation failed",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "message": {
                  "type": "string"
                },
                "errors": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "message",
                "errors"
              ]
            }
          }
        }
      }
    }
  }
}