{"components":{"parameters":{"BoardId":{"in":"path","name":"boardId","required":true,"schema":{"format":"uuid","type":"string"}},"ProjectId":{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}},"TicketId":{"in":"path","name":"ticketId","required":true,"schema":{"format":"uuid","type":"string"}}},"responses":{"BadRequest":{"content":{"text/plain":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid request body or field values."},"InternalError":{"content":{"text/plain":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unexpected server error."},"NotFound":{"content":{"text/plain":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Resource not found or not owned by this API key."},"Unauthorized":{"content":{"text/plain":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing, invalid, or revoked API key."}},"schemas":{"ApiKey":{"properties":{"client":{"description":"AI keys only. The client the agent runs in.","enum":["claude-code","claude-desktop","codex","cursor","vscode","gemini","other",null],"type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"display":{"description":"What it is called: \"Claude\", an other agent's label (or \"AI agent\"), or a service key's name. Others see AI keys with their owner: \"Claude (keloran)\".","type":"string"},"expires_at":{"description":"After this the key is refused (401 \"api key expired\").","format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"kind":{"description":"AI keys can be assigned tickets; service keys are for servers and integrations; personal keys act exactly as their owner (their own scripts and displays).","enum":["ai","service","personal"],"type":"string"},"label":{"description":"AI keys with client \"other\": what the agent is called.","type":["string","null"]},"last_used_at":{"format":"date-time","type":["string","null"]},"name":{"description":"The id its work is recorded under. Generated for AI keys (e.g. claude-482193); chosen for service keys. Only a label for personal keys, whose work is recorded as their owner.","type":"string"},"prefix":{"type":"string"},"read_only":{"description":"Only GET, HEAD and OPTIONS; anything else is 403 \"this api key is read-only\".","type":"boolean"}},"type":"object"},"Attachment":{"properties":{"content_type":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"created_by":{"type":"string"},"delete_on":{"description":"Set once the file is a year old: the day it is deleted unless kept.","format":"date","type":"string"},"id":{"format":"uuid","type":"string"},"key":{"type":"string"},"name":{"type":"string"},"size":{"type":"integer"},"ticket_id":{"format":"uuid","type":"string"},"url":{"format":"uri","type":"string"}},"type":"object"},"AttachmentInput":{"properties":{"content_type":{"type":"string"},"key":{"type":"string"},"name":{"minLength":1,"type":"string"},"size":{"minimum":0,"type":"integer"},"url":{"description":"Must be an https UploadThing URL on utfs.io or a *.ufs.sh host.","format":"uri","type":"string"}},"required":["key","url","name"],"type":"object"},"Board":{"properties":{"agent_review":{"description":"Agents' moves into Done wait for a person's review.","type":"boolean"},"archived_at":{"description":"Archived boards are off the project page and take no new work.","format":"date-time","type":["string","null"]},"columns":{"items":{"$ref":"#/components/schemas/Column"},"type":"array"},"created_at":{"format":"date-time","type":"string"},"deletable":{"description":"Never used (no tickets, no sprints): DELETE works; otherwise archive it.","type":"boolean"},"description":{"type":"string"},"estimate_scale":{"$ref":"#/components/schemas/EstimateScale"},"hidden_done":{"description":"Kanban only. Done tickets finished more than 14 days ago, left off the board.","type":"integer"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"owner_clerk_id":{"type":"string"},"project_id":{"format":"uuid","type":"string"},"sprint":{"$ref":"#/components/schemas/Sprint"},"style":{"enum":["sprints","kanban"],"type":"string"},"team_id":{"description":"The team whose leaders run this board.","format":"uuid","type":["string","null"]},"updated_at":{"format":"date-time","type":"string"}},"required":["id","project_id","name","columns"],"type":"object"},"BoardFlow":{"properties":{"median_cycle_days":{"description":"Median elapsed days for tickets with a known start.","type":["number","null"]},"p85_cycle_days":{"description":"85th percentile elapsed days for tickets with a known start.","type":["number","null"]},"throughput":{"description":"Counts by UTC Monday week start, including weeks with no completed tickets.","items":{"properties":{"tickets":{"type":"integer"},"week_start":{"format":"date","type":"string"}},"required":["week_start","tickets"],"type":"object"},"type":"array"},"tickets":{"description":"Finished tickets in the date window, oldest completion first.","items":{"properties":{"cycle_days":{"description":"Elapsed days from started_at to done_at; null when start is unknown.","type":["number","null"]},"done_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"key":{"example":"TL-123","type":"string"},"started_at":{"format":"date-time","type":["string","null"]},"title":{"type":"string"}},"required":["id","key","title","started_at","done_at","cycle_days"],"type":"object"},"type":"array"},"weeks":{"description":"Number of UTC calendar weeks included","including the current week.":null,"type":"integer"}},"required":["weeks","tickets","median_cycle_days","p85_cycle_days","throughput"],"type":"object"},"BoardInput":{"properties":{"description":{"type":"string"},"name":{"minLength":1,"type":"string"},"template_id":{"description":"A template from GET /board-templates (built-in ids start with builtin:). Default builtin:simple (To do, In progress, Done). Unknown id: 400.","type":"string"}},"required":["name"],"type":"object"},"BoardSummary":{"properties":{"created_at":{"format":"date-time","type":"string"},"description":{"type":"string"},"estimate_scale":{"$ref":"#/components/schemas/EstimateScale"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"owner_clerk_id":{"type":"string"},"project_id":{"format":"uuid","type":"string"},"stats":{"description":"Only on a project's boards. Counts the tickets the board shows (the open sprint's, or all if it has never run one). in_progress = neither the first column nor done; urgent = open urgent tickets.","properties":{"active":{"description":"Latest ticket change, or the board's own update","format":"date-time","type":"string"},"done":{"type":"integer"},"in_progress":{"type":"integer"},"open":{"type":"integer"},"sprint_ends_at":{"format":"date-time","type":["string","null"]},"sprint_number":{"type":["integer","null"]},"urgent":{"type":"integer"}},"type":"object"},"team_id":{"format":"uuid","type":["string","null"]},"updated_at":{"format":"date-time","type":"string"}},"type":"object"},"BoardTemplate":{"properties":{"builtin":{"type":"boolean"},"columns":{"description":"In board order; the last column is Done.","items":{"properties":{"name":{"maxLength":80,"minLength":1,"type":"string"},"wip_limit":{"maximum":999,"minimum":1,"type":["integer","null"]}},"required":["name"],"type":"object"},"maxItems":20,"minItems":1,"type":"array"},"estimate_scale":{"$ref":"#/components/schemas/EstimateScale"},"id":{"description":"builtin:simple, builtin:scrum, builtin:kanban, or an org template's UUID","type":"string"},"name":{"type":"string"},"style":{"enum":["sprints","kanban"],"type":"string"}},"required":["id","name","builtin","style","estimate_scale","columns"],"type":"object"},"BugfixTicket":{"properties":{"board_id":{"format":"uuid","type":"string"},"body":{"type":"string"},"column_id":{"format":"uuid","type":"string"},"created_at":{"format":"date-time","type":"string"},"created_by":{"type":"string"},"id":{"format":"uuid","type":"string"},"priority":{"type":"string"},"title":{"type":"string"}},"required":["id","board_id","column_id","title","body","priority","created_by","created_at"],"type":"object"},"Column":{"properties":{"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"position":{"type":"integer"},"tickets":{"items":{"$ref":"#/components/schemas/Ticket"},"type":"array"},"wip_limit":{"type":["integer","null"]}},"type":"object"},"Comment":{"properties":{"author":{"description":"BugFixes API key name.","type":"string"},"body":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"parent_comment_id":{"format":"uuid","type":["string","null"]},"ticket_id":{"format":"uuid","type":"string"}},"required":["id","ticket_id","body","author","created_at"],"type":"object"},"CreateApiKeyInput":{"properties":{"client":{"description":"Required for AI keys. Their name is generated from it.","enum":["claude-code","claude-desktop","codex","cursor","vscode","gemini","other"],"type":"string"},"expires_in_days":{"default":90,"description":"How long the key works.","maximum":366,"minimum":1,"type":"integer"},"kind":{"enum":["ai","service","personal"],"type":"string"},"label":{"description":"AI keys with client \"other\": what the agent is called (optional).","maxLength":40,"type":"string"},"name":{"description":"Required for service and personal keys; ignored for AI keys.","type":"string"},"read_only":{"default":false,"description":"The key can only read.","type":"boolean"}},"required":["kind"],"type":"object"},"CreateBacklogTicket":{"properties":{"description":{"type":"string"},"labels":{"$ref":"#/components/schemas/Labels"},"priority":{"default":"medium","enum":["low","medium","high","urgent"],"type":"string"},"title":{"minLength":1,"type":"string"},"type":{"default":"task","enum":["bug","feature","task","epic","spike"],"type":"string"}},"required":["title"],"type":"object"},"CreateBugfixTicket":{"properties":{"board_id":{"format":"uuid","type":"string"},"body":{"description":"Initial ticket description.","type":"string"},"column_id":{"format":"uuid","type":"string"},"labels":{"$ref":"#/components/schemas/Labels"},"priority":{"description":"Defaults to medium.","enum":["low","medium","high","urgent"],"type":"string"},"title":{"minLength":1,"type":"string"}},"required":["board_id","column_id","title"],"type":"object"},"CreateComment":{"properties":{"body":{"maxLength":10000,"minLength":1,"type":"string"},"parent_comment_id":{"description":"Omit or set null for a top-level appended update; set to a comment ID to reply in a thread.","format":"uuid","type":["string","null"]}},"required":["body"],"type":"object"},"CreatedApiKey":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"properties":{"key":{"description":"Plaintext secret returned only once.","type":"string"}},"required":["key"],"type":"object"}]},"EpicProgress":{"description":"An epic's progress and size, from its direct sub-tickets (duplicates left out).","properties":{"done":{"type":"integer"},"points":{"description":"Sum of their estimates as points (T-shirt sizes converted).","type":"number"},"total":{"type":"integer"},"unestimated":{"type":"integer"}},"type":"object"},"Error":{"description":"Plain-text error message.","type":"string"},"EstimateScale":{"description":"How a board estimates tickets. fibonacci 1,2,3,5,8,13,21; tshirt XS,S,M,L,XL; powers 1,2,4,8,16; linear 1-5; none turns estimates off.","enum":["none","fibonacci","tshirt","powers","linear"],"type":"string"},"ExternalRef":{"properties":{"id":{"description":"e.g. the Bugfixes report id.","type":"string"},"project":{"description":"The source's project id.","type":"string"},"source":{"enum":["bugfixes"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["source","project","id"],"type":["object","null"]},"HostedFocus":{"description":"What the host has open, shown to followers.","properties":{"filters":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"assignee.":null,"description":"e.g. labels","priorities":null,"type":"object"},"ticketId":{"description":"The ticket open; null when none.","nullable":true,"type":"string"}},"type":"object"},"HostedSession":{"properties":{"focus":{"$ref":"#/components/schemas/HostedFocus"},"host":{"description":"The host's user id.","type":"string"},"people":{"description":"User ids in the session now (seen in the last minute)","host included.":null,"items":{"type":"string"},"type":"array"},"startedAt":{"description":"Unix milliseconds.","format":"int64","type":"integer"}},"type":"object"},"IntakeSettings":{"properties":{"can_manage":{"description":"The caller may open, close or reset it: a signed-in org admin.","type":"boolean"},"open":{"type":"boolean"},"token":{"description":"The form is at /request/{token}.","type":["string","null"]}},"type":"object"},"IntakeSubmission":{"properties":{"contact":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"decided_at":{"format":"date-time","type":["string","null"]},"decided_by":{"type":["string","null"]},"details":{"type":"string"},"id":{"format":"uuid","type":"string"},"project_id":{"format":"uuid","type":"string"},"status":{"enum":["pending","promoted","dismissed"],"type":"string"},"ticket_id":{"format":"uuid","type":["string","null"]},"ticket_key":{"type":["string","null"]},"title":{"type":"string"},"type":{"enum":["bug","feature"],"type":"string"}},"type":"object"},"Invitation":{"properties":{"accept_url":{"description":"Clerk-hosted invitation URL, returned by create and list/get.","format":"uri","type":"string"},"accepted_at":{"format":"date-time","type":"string"},"accepted_by":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"email_address":{"format":"email","type":"string"},"expires_at":{"format":"date-time","type":"string"},"id":{"description":"TrackLin.es invitation id used by inspect, revoke, and accept routes.","format":"uuid","type":"string"},"invited_by":{"type":"string"},"org_id":{"type":"string"},"scope":{"enum":["organization","team","project"],"type":"string"},"status":{"enum":["pending","accepted","revoked","expired"],"type":"string"},"target_id":{"format":"uuid","type":"string"}},"required":["id","org_id","email_address","scope","invited_by","status","expires_at","created_at"],"type":"object"},"InvitationInput":{"properties":{"email_address":{"format":"email","type":"string"},"scope":{"enum":["organization","team","project"],"type":"string"},"target_id":{"description":"Required for team and project scopes; omit for organization scope.","format":"uuid","type":"string"}},"required":["email_address","scope"],"type":"object"},"Labels":{"description":"Free-form labels. Trimmed; blanks and case-insensitive duplicates dropped (first spelling wins). Omit to leave unchanged; [] clears.","items":{"maxLength":50,"type":"string"},"maxItems":20,"type":"array"},"LiveStandup":{"properties":{"index":{"description":"Current participant; people.length is the clean-board review.","type":"integer"},"people":{"items":{"properties":{"id":{"type":"string"},"label":{"type":"string"}},"required":["id","label"],"type":"object"},"type":"array"},"runningSince":{"description":"Unix milliseconds when the clock resumed; null while paused.","format":"int64","type":["integer","null"]},"spentMs":{"description":"Elapsed time before the current run.","format":"int64","type":"integer"},"startedBy":{"type":"string"}},"required":["people","index","spentMs","runningSince","startedBy"],"type":"object"},"MoveTicket":{"properties":{"column_id":{"format":"uuid","type":"string"},"estimate":{"description":"Size the ticket on the way in, on the target board's estimate_scale. Required when the ticket enters a board with an estimate scale (from the backlog or another board) and has no estimate on that same scale; 400 otherwise.","type":"string"},"position":{"default":0,"type":"integer"}},"required":["column_id"],"type":"object"},"NameInput":{"properties":{"name":{"minLength":1,"type":"string"}},"required":["name"],"type":"object"},"Notification":{"properties":{"actor":{"description":"User id or agent name.","type":"string"},"board_id":{"format":"uuid","type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"kind":{"enum":["assigned","mention","finished","wont_do","review","sent_back","approval"],"type":"string"},"read_at":{"format":"date-time","type":["string","null"]},"text":{"description":"Reads after the actor, e.g. \"assigned you TL-12: Fix login\".","type":"string"},"ticket_id":{"format":"uuid","type":["string","null"]},"ticket_key":{"type":["string","null"]}},"type":"object"},"OrgAdmin":{"properties":{"created_at":{"format":"date-time","type":"string"},"granted_by":{"description":"clerk:creator, clerk:admin-role, clerk:earliest-member, or the user id of the admin who granted it","type":"string"},"user_id":{"type":"string"}},"type":"object"},"OrganizationMember":{"properties":{"first_name":{"type":"string"},"identifier":{"description":"Public identifier provided by Clerk, usually an email address.","type":"string"},"image_url":{"format":"uri","type":"string"},"last_name":{"type":"string"},"user_id":{"type":"string"}},"required":["user_id","identifier"],"type":"object"},"OrganizationMemberPage":{"properties":{"limit":{"type":"integer"},"members":{"items":{"$ref":"#/components/schemas/OrganizationMember"},"type":"array"},"offset":{"type":"integer"},"total_count":{"type":"integer"}},"required":["members","total_count","limit","offset"],"type":"object"},"PlannedSprint":{"properties":{"board_id":{"format":"uuid","type":"string"},"capacity":{"properties":{"approved_at":{"format":"date-time","type":["string","null"]},"approved_by":{"type":["string","null"]},"approved_total":{"type":["number","null"]},"availability":{"description":"The board team's time away over the sprint, from Google Calendar (weekends and company days off aren't working days). A team figure, never per person. Null without Google Calendar or a board team.","properties":{"percent":{"0–100.":null,"description":"Share of the team's working days not away","type":"number"},"person_days_away":{"type":"number"}},"type":["object","null"]},"cap":{"description":"velocity + 10%, times availability.percent when that's set; null = no limit yet.","type":["number","null"]},"needs_approval":{"description":"Over capacity and not covered by an approval; it won't start until approved or trimmed.","type":"boolean"},"over":{"type":"boolean"},"unestimated":{"type":"integer"},"unit":{"enum":["points","tickets"],"type":"string"},"used":{"type":"number"},"velocity":{"description":"Average completed over the last 3 closed sprints; null without history.","type":["number","null"]}},"type":"object"},"goal":{"description":"Carried into the sprint when it starts.","maxLength":200,"type":"string"},"id":{"format":"uuid","type":"string"},"length_days":{"type":"integer"},"number":{"description":"The sprint number it will get","type":"integer"},"position":{"description":"1 = the next sprint","type":"integer"},"tickets":{"items":{"$ref":"#/components/schemas/Ticket"},"type":"array"}},"type":"object"},"Progress":{"properties":{"done":{"type":"integer"},"total":{"type":"integer"}},"type":"object"},"Project":{"properties":{"boards":{"items":{"$ref":"#/components/schemas/BoardSummary"},"type":"array"},"created_at":{"format":"date-time","type":"string"},"description":{"type":"string"},"id":{"format":"uuid","type":"string"},"key_prefix":{"description":"Ticket keys are \u003ckey_prefix\u003e-\u003cnumber\u003e. Worked out from the name on create (its capitals, e.g. TrackLin.es → TL), unique in the org.","example":"TL","type":"string"},"name":{"type":"string"},"roadmaps":{"items":{"$ref":"#/components/schemas/Roadmap"},"type":"array"},"stats":{"description":"Only on the project list. Open and done count board tickets (done = in the board's last column); urgent counts open urgent tickets on boards or in the backlog.","properties":{"active":{"description":"Latest ticket change, or the project's own update","format":"date-time","type":"string"},"backlog":{"type":"integer"},"boards":{"type":"integer"},"done":{"type":"integer"},"open":{"type":"integer"},"roadmaps":{"type":"integer"},"urgent":{"type":"integer"}},"type":"object"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","description","key_prefix","created_at","updated_at"],"type":"object"},"ProjectInput":{"properties":{"description":{"type":"string"},"key_prefix":{"description":"Update only; lower case is upper-cased. Omitted = unchanged.","pattern":"^[A-Z][A-Z0-9]{1,5}$","type":"string"},"name":{"minLength":1,"type":"string"}},"required":["name"],"type":"object"},"ProjectRepo":{"properties":{"created_at":{"format":"date-time","type":"string"},"default_branch":{"type":"string"},"full_name":{"example":"acme/shop","type":"string"},"provider":{"enum":["github","bitbucket"],"type":"string"}},"type":"object"},"ReportMix":{"properties":{"count":{"type":"integer"},"type":{"enum":["bug","feature","task","epic","spike"],"type":"string"},"value":{"type":"number"}},"type":"object"},"ReportOverall":{"properties":{"goals_met":{"type":"integer"},"goals_set":{"type":"integer"},"predictability":{"type":["number","null"]},"sprints":{"type":"integer"}},"type":"object"},"ReportRollup":{"properties":{"boards":{"description":"A project's sprint boards (not kanban or archived): each a board report.","items":{"type":"object"},"type":"array"},"from":{"format":"date-time","type":"string"},"id":{"description":"The project; absent for the organization.","type":"string"},"months":{"items":{"properties":{"month":{"example":"2026-08","type":"string"},"points_done":{"type":"number"},"predictability":{"type":["number","null"]},"sprints":{"type":"integer"},"tickets_done":{"type":"number"}},"type":"object"},"type":"array"},"name":{"type":"string"},"overall":{"$ref":"#/components/schemas/ReportOverall"},"points":{"description":"Boards that estimate.","oneOf":[{"$ref":"#/components/schemas/ReportTotals"},{"type":"null"}]},"previous":{"$ref":"#/components/schemas/ReportOverall"},"projects":{"description":"The organization's projects, each a roll-up without its boards.","items":{"$ref":"#/components/schemas/ReportRollup"},"type":"array"},"summary":{"type":"string"},"tickets":{"description":"Boards that count tickets.","oneOf":[{"$ref":"#/components/schemas/ReportTotals"},{"type":"null"}]},"to":{"format":"date-time","type":"string"}},"type":"object"},"ReportTotals":{"properties":{"added":{"type":["number","null"]},"carried":{"type":["number","null"]},"done":{"type":"number"},"mix":{"items":{"$ref":"#/components/schemas/ReportMix"},"type":"array"},"planned":{"type":"number"},"predictability":{"type":["number","null"]},"sprints":{"description":"Closed sprints counted.","type":"integer"},"velocity":{"type":["number","null"]}},"type":"object"},"Roadmap":{"properties":{"can_manage":{"description":"The caller can change this roadmap (an org admin, or a leader of a team with a board in its project). Always false for readers outside its organization.","type":"boolean"},"created_at":{"format":"date-time","type":"string"},"description":{"type":"string"},"id":{"format":"uuid","type":"string"},"items":{"items":{"$ref":"#/components/schemas/RoadmapItem"},"type":"array"},"progress":{"$ref":"#/components/schemas/Progress"},"project_id":{"format":"uuid","type":"string"},"title":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"visibility":{"enum":["public","login_only","team"],"type":"string"}},"type":"object"},"RoadmapInput":{"properties":{"description":{"type":"string"},"title":{"minLength":1,"type":"string"},"visibility":{"default":"public","enum":["public","login_only","team"],"type":"string"}},"required":["title"],"type":"object"},"RoadmapItem":{"allOf":[{"$ref":"#/components/schemas/RoadmapItemInput"},{"properties":{"position":{"type":"integer"},"progress":{"$ref":"#/components/schemas/Progress"},"status":{"enum":["not_started","in_progress","done"],"type":"string"},"tickets":{"description":"Included for the owner; hidden for other viewers.","items":{"properties":{"done":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"title":{"type":"string"},"type":{"description":"An epic counts its sub-tickets towards progress.","enum":["bug","feature","task","epic","spike"],"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}]},"RoadmapItemInput":{"properties":{"description":{"type":"string"},"id":{"description":"Omit to create a new item.","format":"uuid","type":"string"},"manual_status":{"enum":["not_started","in_progress","done"],"type":"string"},"start_date":{"description":"Gantt bar start date.","format":"date","type":["string","null"]},"target_date":{"format":"date","type":["string","null"]},"title":{"minLength":1,"type":"string"}},"required":["title"],"type":"object"},"RoadmapUpdate":{"properties":{"author":{"description":"Who posted it, by name (an email-only name without its domain; an agent by its own name). Missing on updates from before TL-413.","type":"string"},"health":{"enum":["on_track","at_risk","off_track"],"type":"string"},"id":{"format":"uuid","type":"string"},"note":{"maxLength":2000,"type":"string"},"published_at":{"format":"date-time","type":"string"}},"required":["id","health","note","published_at"],"type":"object"},"RoadmapUpdateInput":{"properties":{"health":{"enum":["on_track","at_risk","off_track"],"type":"string"},"note":{"description":"Plain text or Markdown; rendered as safe text by clients.","maxLength":2000,"minLength":1,"type":"string"}},"required":["health","note"],"type":"object"},"Sprint":{"properties":{"board_id":{"format":"uuid","type":"string"},"closed_at":{"format":"date-time","type":["string","null"]},"ends_at":{"format":"date-time","type":"string"},"goal":{"description":"One line the sprint is for; empty = none.","maxLength":200,"type":"string"},"id":{"format":"uuid","type":"string"},"length_days":{"type":"integer"},"number":{"type":"integer"},"starts_at":{"format":"date-time","type":"string"}},"type":"object"},"Team":{"properties":{"created_at":{"format":"date-time","type":"string"},"description":{"type":"string"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"org_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","org_id","name","description","created_at","updated_at"],"type":"object"},"TeamInput":{"properties":{"description":{"type":"string"},"name":{"minLength":1,"type":"string"}},"required":["name"],"type":"object"},"TeamMember":{"properties":{"leader":{"type":"boolean"},"user_id":{"type":"string"}},"type":"object"},"Ticket":{"description":"Agent origin is recorded in created_by and rendered in the ticket UI; assignment is shown in assigned_to.","properties":{"assigned_to":{"type":["string","null"]},"blocked":{"type":"boolean"},"column_id":{"description":"Null while in the backlog.","format":"uuid","type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"created_by":{"type":"string"},"description":{"type":"string"},"done_at":{"description":"When it reached its board's Done column or was resolved; null while open.","format":"date-time","type":["string","null"]},"duplicate_of_id":{"description":"The original ticket when this ticket is marked as a duplicate.","format":"uuid","type":["string","null"]},"duplicate_of_key":{"description":"Key of the original ticket.","type":["string","null"]},"duplicate_of_title":{"description":"Title of the original ticket.","type":["string","null"]},"estimate":{"description":"On the board's estimate_scale. Cleared when the ticket moves to the backlog or to a board with another scale.","type":["string","null"]},"external_source":{"description":"Where an integration made it (bugfixes); null for our own tickets.","type":["string","null"]},"external_url":{"description":"Link to it there (e.g. the Bugfixes report).","type":["string","null"]},"id":{"format":"uuid","type":"string"},"key":{"description":"The project's key prefix and the ticket's number in the project, assigned on create. For branch names, PR titles and search.","example":"TL-123","type":"string"},"labels":{"description":"Free-form labels, sorted case-insensitively.","items":{"type":"string"},"type":"array"},"planned_sprint_id":{"description":"A backlog ticket planned into a board's upcoming sprint (refinement).","format":"uuid","type":["string","null"]},"position":{"type":"integer"},"priority":{"enum":["low","medium","high","urgent"],"type":"string"},"project_id":{"format":"uuid","type":"string"},"resolution":{"description":"How a resolved backlog ticket was closed.","enum":["done","wont_do","duplicate",null],"type":["string","null"]},"resolved_at":{"description":"Set when the ticket was resolved in the backlog (POST /tickets/{id}/resolve); it then counts as done.","format":"date-time","type":["string","null"]},"review_requested_at":{"description":"Set while an agent's finished ticket waits for a person's review (in the column before Done).","format":"date-time","type":["string","null"]},"sprint_id":{"format":"uuid","type":["string","null"]},"started_at":{"description":"First move out of the board's first column; null for historical tickets without a known start.","format":"date-time","type":["string","null"]},"title":{"type":"string"},"type":{"enum":["bug","feature","task","epic","spike"],"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","project_id","column_id","title","description","created_by","type","priority","position","blocked"],"type":"object"},"TicketChild":{"properties":{"assigned_to":{"type":["string","null"]},"board_id":{"description":"Null when the sub-ticket is in the backlog.","type":["string","null"]},"board_name":{"type":["string","null"]},"done":{"type":"boolean"},"estimate":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"key":{"type":["string","null"]},"title":{"type":"string"},"type":{"enum":["bug","feature","task","epic","spike"],"type":"string"}},"type":"object"},"TicketDependency":{"properties":{"done":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"title":{"type":"string"}},"type":"object"},"TicketDetail":{"description":"Ticket properties plus board, column, sprint, and completion context.","properties":{"assigned_to":{"type":["string","null"]},"attachment_count":{"description":"Files on the ticket (list them with GET /tickets/{ticketId}/attachments): designs, screenshots, logs.","type":"integer"},"blocked":{"type":"boolean"},"blocked_by":{"items":{"$ref":"#/components/schemas/TicketDependency"},"type":"array"},"blocks":{"items":{"$ref":"#/components/schemas/TicketDependency"},"type":"array"},"board_id":{"format":"uuid","type":["string","null"]},"board_name":{"type":["string","null"]},"children":{"items":{"$ref":"#/components/schemas/TicketChild"},"type":"array"},"column_id":{"format":"uuid","type":["string","null"]},"column_name":{"type":["string","null"]},"created_by":{"type":"string"},"description":{"type":"string"},"done":{"type":"boolean"},"duplicate_of_id":{"format":"uuid","type":["string","null"]},"duplicate_of_key":{"type":["string","null"]},"duplicate_of_title":{"type":["string","null"]},"duplicates":{"items":{"properties":{"id":{"format":"uuid","type":"string"},"key":{"type":"string"},"title":{"type":"string"}},"required":["id","key","title"],"type":"object"},"type":"array"},"epic":{"$ref":"#/components/schemas/EpicProgress"},"epic_id":{"description":"The epic this ticket is part of, at any depth (epics don't nest).","type":["string","null"]},"estimate":{"type":["string","null"]},"estimate_scale":{"$ref":"#/components/schemas/EstimateScale"},"id":{"format":"uuid","type":"string"},"parent_id":{"format":"uuid","type":["string","null"]},"position":{"type":"integer"},"priority":{"enum":["low","medium","high","urgent"],"type":"string"},"project_id":{"format":"uuid","type":"string"},"resolution":{"enum":["done","wont_do","duplicate",null],"type":["string","null"]},"sprint_id":{"format":"uuid","type":["string","null"]},"sprint_number":{"type":["integer","null"]},"timebox_days":{"description":"A spike's timebox, in days (with timebox_hours). Null when not a spike or not yet timed.","type":["integer","null"]},"timebox_hours":{"description":"A spike's timebox hours (0–23).","type":["integer","null"]},"title":{"type":"string"},"type":{"enum":["bug","feature","task","epic","spike"],"type":"string"}},"type":"object"},"TicketInput":{"properties":{"description":{"type":"string"},"estimate":{"description":"A value on the board's estimate_scale (400 otherwise). Required when creating on a board with an estimate scale; omitted = unchanged on update, and it can't be cleared there. Not allowed on backlog tickets.","type":"string"},"labels":{"$ref":"#/components/schemas/Labels"},"priority":{"default":"medium","enum":["low","medium","high","urgent"],"type":"string"},"timebox_days":{"description":"A spike's timebox days (TL-502). Send with timebox_hours; both 0 clears the timebox, both omitted = unchanged. Only meaningful on spikes (set via update).","maximum":60,"minimum":0,"type":"integer"},"timebox_hours":{"description":"A spike's timebox hours (0–23). Send with timebox_days.","maximum":23,"minimum":0,"type":"integer"},"title":{"minLength":1,"type":"string"},"type":{"default":"task","enum":["bug","feature","task","epic","spike"],"type":"string"}},"required":["title"],"type":"object"},"URLResponse":{"properties":{"url":{"format":"uri","type":"string"}},"required":["url"],"type":"object"},"Velocity":{"properties":{"cap":{"description":"A sprint's capacity: velocity (last 3 closed sprints) + 10%. Null without closed sprints.","nullable":true,"type":"number"},"current":{"description":"The open sprint, or null. total is the sprint's current scope (scope changes aren't tracked over time).","properties":{"done":{"description":"When each done ticket reached Done, oldest first.","items":{"properties":{"at":{"format":"date-time","type":"string"},"value":{"type":"number"}},"type":"object"},"type":"array"},"ends_at":{"format":"date-time","type":"string"},"number":{"type":"integer"},"starts_at":{"format":"date-time","type":"string"},"total":{"type":"number"},"unestimated":{"description":"Tickets with no estimate on a points board.","type":"integer"}},"type":["object","null"]},"sprints":{"description":"Closed sprints, oldest first.","items":{"properties":{"closed_at":{"format":"date-time","type":"string"},"completed":{"type":"number"},"number":{"type":"integer"},"planned":{"description":"What the sprint held when it closed (its burn chart scope), in the same unit; null for sprints closed before that was recorded.","type":["number","null"]},"starts_at":{"format":"date-time","type":"string"}},"type":"object"},"type":"array"},"unit":{"enum":["points","tickets"],"type":"string"}},"required":["unit","sprints","current"],"type":"object"}},"securitySchemes":{"bearerAuth":{"bearerFormat":"JWT or TrackLin.es API key","description":"Clerk session token, TrackLin.es key (`tl_p_…`, `tl_ai_…`, `tl_s_…`, or an older `tl_…`), or BugFixes `bf_` key, depending on the route.","scheme":"bearer","type":"http"},"bugfixesBearer":{"bearerFormat":"API key","description":"TrackLin.es API key created for a BugFixes agent; key starts with `bf_`.","scheme":"bearer","type":"http"},"clerkSession":{"bearerFormat":"JWT","description":"A signed-in Clerk session token. API keys are rejected on API-key management routes.","scheme":"bearer","type":"http"}}},"info":{"description":"Complete HTTP API for projects, boards, tickets, roadmaps, billing, and\nintegrations. The API also supports the BugFixes workflow: create in a\nboard column or project backlog, append conversation updates, and close a\nboard ticket by moving it to the board's last (Done) column.\n\nAPI-key requests are rate limited per key (default 300 a minute, set with\nAPI_KEY_RATE_LIMIT): over the limit answers 429 with Retry-After, and every\nresponse carries X-RateLimit-Limit / X-RateLimit-Remaining. MCP tool calls\ncount like the REST calls they make. Browser sessions aren't limited.\n\nMost private endpoints accept a Clerk session token or a TrackLin.es (`tl_ai_…` for AI agents, `tl_s_…` for services, `tl_p_…` for personal keys; older keys are `tl_…`)\nor BugFixes (`bf_…`) agent API key as a Bearer token. Projects, boards and\nroadmaps belong to a Clerk organization: a session sees its active\norganization's data, and a request without an active organization gets\n403. API keys act inside the organization they were created in. Personal and\nagent keys belong to the person who made them: a personal key is them, so its\nwrites are theirs; an agent key is their AI agent, so its writes are the agent's\n(shown with its owner's name to everyone else). Service keys belong to the\norganization, are made by its admins, and attribute writes to the key's name. A read-only key gets 403 for anything but GET. API-key management\nrequires a Clerk session. Stripe webhooks use Stripe-Signature.\n\nTimestamps are stored and returned in UTC. Day-based rules (a sprint's last\nday) use the caller's timezone from the optional `Tracklines-Timezone`\nheader (an IANA name such as Europe/London); without it, or for an unknown\nzone, UTC.\n\nOn a sprint's last day (in that timezone, or once ends_at has passed) its\nscope is locked: moving a ticket into it answers 409, and a ticket created\nin one of its board's columns goes to the bottom of the project backlog\ninstead (the 201 body has `redirected_to_backlog: true`).\nIDs are UUIDs. Error responses are plain-text messages.\n","title":"TrackLin.es API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/assignees":{"get":{"description":"Only members of the active organization and its active AI keys are listed. Service/API keys are excluded.","operationId":"listAssignees","responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"away":{"description":"People only, with Google Calendar connected (TL-407): on holiday now.","properties":{"company":{"description":"A company day off rather than their own leave.","type":"boolean"},"until":{"description":"When they're back (UTC); back-to-back leave is joined up.","format":"date-time","type":"string"}},"type":"object"},"away_next":{"description":"People only: leave starting within the next 7 days.","properties":{"from":{"format":"date-time","type":"string"},"until":{"format":"date-time","type":"string"}},"type":"object"},"id":{"type":"string"},"image_url":{"description":"People only: their Clerk avatar image, when they have one.","type":"string"},"kind":{"enum":["person","ai"],"type":"string"},"label":{"type":"string"},"name":{"description":"People only: real name (also when label is \"You\"), for @ mentions.","type":"string"},"username":{"description":"People only: Clerk username, when set. @username mentions them.","type":"string"}},"type":"object"},"type":"array"}}},"description":"Assignable identities."},"401":{"$ref":"#/components/responses/Unauthorized"},"502":{"description":"Could not load organization members from Clerk."}},"summary":"List organization members and AI keys available for assignment","tags":["Tickets"]}},"/attachments/retention":{"get":{"description":"Files due to be deleted (warned, soonest first) and files the clean-up has deleted (newest first, the latest 500). Org admins only; API keys can't.","operationId":"attachmentRetention","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"deleted":{"items":{"properties":{"created_by":{"type":"string"},"deleted_at":{"format":"date-time","type":"string"},"name":{"type":"string"},"size":{"type":"integer"},"ticket_id":{"format":"uuid","type":"string"},"ticket_key":{"type":"string"},"ticket_title":{"type":"string"}},"type":"object"},"type":"array"},"due":{"items":{"properties":{"created_by":{"type":"string"},"delete_on":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"size":{"type":"integer"},"ticket_id":{"format":"uuid","type":"string"},"ticket_key":{"type":"string"},"ticket_title":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Due and deleted files."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Not an org admin","or an API key.":null}},"summary":"The org's file clean-up","tags":["Attachments"]}},"/attachments/{attachmentId}":{"delete":{"operationId":"deleteAttachment","parameters":[{"in":"path","name":"attachmentId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Attachment deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete an attachment and its stored file","tags":["Attachments"]}},"/attachments/{attachmentId}/keep":{"post":{"description":"Files are warned about a year after they're added (or last kept) and deleted at 13 months unless kept. Org admins and the file's uploader can keep it; API keys can't.","operationId":"keepAttachment","parameters":[{"in":"path","name":"attachmentId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Kept for another year."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Not an org admin or the file's uploader","or an API key.":null},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Keep a year-old file for another year","tags":["Attachments"]}},"/board-templates":{"get":{"operationId":"listBoardTemplates","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/BoardTemplate"},"type":"array"}}},"description":"Templates"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Board templates (built-ins, then the organization's own)","tags":["Boards"]},"post":{"description":"Org admins only, signed in with a Clerk session. Send a full template, or {name, from_board_id} to copy an existing board's columns, WIP limits, style and estimate scale.","operationId":"createBoardTemplate","requestBody":{"content":{"application/json":{"schema":{"properties":{"columns":{"$ref":"#/components/schemas/BoardTemplate/properties/columns"},"estimate_scale":{"$ref":"#/components/schemas/EstimateScale"},"from_board_id":{"format":"uuid","type":"string"},"name":{"maxLength":80,"minLength":1,"type":"string"},"style":{"enum":["sprints","kanban"],"type":"string"}},"required":["name"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoardTemplate"}}},"description":"Template saved."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Save an organization board template","tags":["Boards"]}},"/board-templates/{templateId}":{"delete":{"operationId":"deleteBoardTemplate","responses":{"204":{"description":"Deleted."},"400":{"description":"Built-in templates can't be deleted."},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete an organization template (built-ins can't be deleted)","tags":["Boards"]},"parameters":[{"in":"path","name":"templateId","required":true,"schema":{"type":"string"}}]},"/boards/{boardId}":{"delete":{"operationId":"deleteBoard","responses":{"204":{"description":"Board deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete a board","tags":["Boards"]},"get":{"description":"Use the column with the greatest `position` as the Done column when closing a ticket.","operationId":"getBoard","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Board"}}},"description":"Board, with ordered columns and their current tickets."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Get board columns and tickets","tags":["Boards"]},"parameters":[{"$ref":"#/components/parameters/BoardId"}],"patch":{"description":"Provide any of name, estimate_scale and style. Changing estimate_scale clears the estimates of tickets on the board, except those in closed sprints (kept for velocity history). Switching to kanban ends the open sprint, if any: it closes with no successor, keeps its done tickets, and its unfinished tickets stay on the board outside any sprint. Kanban boards can't start sprints and show only Done tickets finished in the last 14 days.","operationId":"updateBoard","requestBody":{"content":{"application/json":{"schema":{"minProperties":1,"properties":{"agent_review":{"description":"Agents' moves into Done wait for a person's review.","type":"boolean"},"estimate_scale":{"$ref":"#/components/schemas/EstimateScale"},"name":{"minLength":1,"type":"string"},"style":{"enum":["sprints","kanban"],"type":"string"}},"type":"object"}}},"required":true},"responses":{"204":{"description":"Board updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin or the linked team's leader."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Rename a board or change its settings","tags":["Boards"]}},"/boards/{boardId}/archive":{"parameters":[{"in":"path","name":"boardId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"description":"Takes it off the project's boards; its history stays. Refused (409) while it has unfinished tickets. Its open sprint closes with no next sprint (finished tickets stay with it for velocity); its planned sprints are dropped and their tickets go back to the backlog unplanned. No tickets can be added to or moved onto an archived board (409).","operationId":"archiveBoard","responses":{"204":{"description":"Archived."},"403":{"description":"Not an org admin or this board's team leader."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"It still has unfinished tickets."}},"summary":"Archive a used board (board managers)","tags":["Boards"]}},"/boards/{boardId}/columns":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"description":"The new column goes just before the last column (Done), which stays last. A ticket is done when it's in the board's last column.","operationId":"createColumn","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NameInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Column"}}},"description":"Column created."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin or the linked team's leader."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Add a column to a board","tags":["Columns"]},"put":{"description":"The edit board page's save. Columns in order, each an existing column (id) or a new one (no id), with its name and WIP limit. The first (To do) and last (Done) stay first and last, and take no WIP limit. Columns left out are deleted, refused (409) while they hold tickets in the current sprint. All or nothing. Board managers only.","operationId":"setColumnLayout","requestBody":{"content":{"application/json":{"schema":{"properties":{"columns":{"items":{"properties":{"id":{"description":"Left out for a new column.","format":"uuid","type":"string"},"name":{"maxLength":80,"type":"string"},"wip_limit":{"description":"0 or null for none.","maximum":999,"minimum":0,"nullable":true,"type":"integer"}},"required":["name"],"type":"object"},"minItems":2,"type":"array"}},"required":["columns"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Saved."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Only organization admins and the board's team leaders can change its columns."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"A column left out still has tickets in the current sprint."}},"summary":"Set a board's whole column layout at once","tags":["Columns"]}},"/boards/{boardId}/columns/order":{"put":{"description":"Every column exactly once, with the last column (Done) still last; moving it is a 400, since a ticket is done when it's in the last column.","operationId":"reorderColumns","parameters":[{"$ref":"#/components/parameters/BoardId"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"column_ids":{"items":{"format":"uuid","type":"string"},"type":"array"}},"required":["column_ids"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Column order updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin or the linked team's leader."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Replace a board's column order","tags":["Columns"]}},"/boards/{boardId}/flow":{"get":{"description":"Returns tickets completed during the requested rolling window of UTC calendar weeks, including empty throughput weeks. Cycle-time percentiles include only tickets with a recorded started_at; older tickets without an exact start time remain in throughput but have a null cycle_days.","operationId":"getBoardFlow","parameters":[{"description":"Number of UTC calendar weeks including the current week.","in":"query","name":"weeks","schema":{"default":12,"maximum":52,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoardFlow"}}},"description":"Finished tickets, cycle-time percentiles, and throughput by week."},"400":{"description":"weeks must be between 1 and 52."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Cycle time and weekly throughput for finished tickets","tags":["Boards"]},"parameters":[{"$ref":"#/components/parameters/BoardId"}]},"/boards/{boardId}/live":{"get":{"description":"Server-sent events fan out across backend instances through Valkey. Connects with the latest retained event, then streams updates; sends a heartbeat comment every 20 seconds. Events are named `standup`, `standup-ended`, or `refinement`.","operationId":"streamBoardLiveEvents","responses":{"200":{"content":{"text/event-stream":{"schema":{"type":"string"}}},"description":"Server-sent event stream."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"No active organization in the caller's session or API key."},"404":{"$ref":"#/components/responses/NotFound"},"503":{"description":"Valkey is unavailable."}},"summary":"Stream standup and refinement changes for a board","tags":["Boards"]},"parameters":[{"$ref":"#/components/parameters/BoardId"}]},"/boards/{boardId}/live/standup":{"get":{"operationId":"getLiveStandup","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveStandup"}}},"description":"The active standup session"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"503":{"description":"Valkey is unavailable."}},"summary":"Get the current board standup session","tags":["Boards"]},"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"operationId":"startLiveStandup","requestBody":{"content":{"application/json":{"schema":{"properties":{"people":{"items":{"properties":{"id":{"type":"string"},"label":{"type":"string"}},"required":["id","label"],"type":"object"},"type":"array"}},"required":["people"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveStandup"}}},"description":"Started session"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"503":{"description":"Valkey is unavailable."}},"summary":"Start or replace the board standup session","tags":["Boards"]}},"/boards/{boardId}/live/standup/action":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"operationId":"actOnLiveStandup","requestBody":{"content":{"application/json":{"schema":{"properties":{"action":{"enum":["next","previous","jump","pause","resume","end"],"type":"string"},"index":{"description":"Required for jump; clamped to the participant list and review turn.","type":"integer"}},"required":["action"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveStandup"}}},"description":"Updated session"},"204":{"description":"Session ended."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"503":{"description":"Valkey is unavailable."}},"summary":"Advance, jump, pause, resume, or end the current standup","tags":["Boards"]}},"/boards/{boardId}/live/standup/focus":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"put":{"operationId":"setHostedStandupFocus","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedFocus"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"The session."},"403":{"description":"Only the host can set it."},"404":{"description":"No standup running."}},"summary":"Say what the host has open and how the board is filtered (host only)","tags":["Boards"]}},"/boards/{boardId}/live/standup/host":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"put":{"operationId":"handOverHostedStandup","requestBody":{"content":{"application/json":{"schema":{"properties":{"user_id":{"type":"string"}},"required":["user_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"The session with its new host."},"400":{"description":"That person isn't in the standup."},"403":{"description":"Only the host can hand over (or take over from a host who has gone)."},"404":{"description":"No standup running."}},"summary":"Hand over hosting (e.g. to the next speaker), or take over from a host who has gone","tags":["Boards"]}},"/boards/{boardId}/live/standup/join":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"operationId":"joinHostedStandup","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"The session","with the caller in it.":null},"404":{"description":"No standup running."}},"summary":"Join the standup, and stay in it (call every 30 seconds)","tags":["Boards"]}},"/boards/{boardId}/live/standup/leave":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"operationId":"leaveHostedStandup","responses":{"204":{"description":"Left."}},"summary":"Leave the standup","tags":["Boards"]}},"/boards/{boardId}/live/standup/session":{"get":{"description":"Starting a standup makes the starter its host; ending it ends the session. Events (`standup-session`, `standup-session-ended`) go out on the board's live stream.","operationId":"getHostedStandup","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"The session."},"404":{"description":"No standup running","or the board isn't in the caller's organization.":null},"503":{"description":"Valkey is unavailable."}},"summary":"Who's in the standup and what the host has open","tags":["Boards"]},"parameters":[{"$ref":"#/components/parameters/BoardId"}]},"/boards/{boardId}/planned-sprints":{"get":{"operationId":"listPlannedSprints","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PlannedSprint"},"type":"array"}}},"description":"Planned sprints, next first (at most 2)."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"A board's planned sprints with their tickets and capacity","tags":["Sprints"]},"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"operationId":"planSprint","requestBody":{"content":{"application/json":{"schema":{"properties":{"goal":{"maxLength":200,"type":"string"},"length_days":{"maximum":365,"minimum":1,"type":"integer"}},"required":["length_days"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PlannedSprint"},"type":"array"}}},"description":"The board's planned sprints."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Not an org admin or the board team's leader."},"409":{"description":"Already 2 planned sprints","or a kanban board.":null}},"summary":"Plan a sprint after the current one (org admins and the board team's leaders)","tags":["Sprints"]}},"/boards/{boardId}/reports/sprints":{"get":{"operationId":"boardSprintReport","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"board_id":{"type":"string"},"board_name":{"type":"string"},"from":{"format":"date-time","type":"string"},"previous":{"$ref":"#/components/schemas/ReportTotals"},"spikes":{"description":"Timeboxed spikes started and finished on this board in the period (TL-508), compared with their timebox in business hours (Mon–Fri 09:00–17:00, 8h a day) in the caller's Tracklines-Timezone. Spikes without a timebox, or never started, are left out.","properties":{"done":{"type":"integer"},"over":{"type":"integer"},"over_hours":{"description":"Business hours over","summed across the spikes that went over.":null,"type":"number"},"within":{"type":"integer"}},"type":"object"},"sprints":{"items":{"properties":{"added":{"type":["number","null"]},"approved_over":{"type":["number","null"]},"carried":{"type":["number","null"]},"closed_at":{"type":["string","null"]},"done":{"type":"number"},"ends_at":{"format":"date-time","type":"string"},"estimated":{"type":"boolean"},"goal":{"type":"string"},"goal_met":{"type":["boolean","null"]},"mix":{"items":{"$ref":"#/components/schemas/ReportMix"},"type":"array"},"number":{"type":"integer"},"planned":{"type":"number"},"predictability":{"type":["number","null"]},"removed":{"type":["number","null"]},"starts_at":{"format":"date-time","type":"string"},"velocity":{"type":["number","null"]}},"type":"object"},"type":"array"},"summary":{"example":"Delivered 84% of planned work across 6 sprints","type":"string","up from 71%.":null},"to":{"format":"date-time","type":"string"},"totals":{"$ref":"#/components/schemas/ReportTotals"},"unit":{"enum":["points","tickets"],"type":"string"}},"type":"object"}}},"description":"The report. Sprints from before reports were recorded have estimated (planned = what they held at close) and no added/removed."},"400":{"description":"Bad dates."},"404":{"description":"Not your organization's board."}},"summary":"How a board's sprints went over a period (TL-397): sprints that closed in it plus the open one, totals, the period before for trends, and a one-line summary. Never per person.","tags":["Sprints"]},"parameters":[{"in":"path","name":"boardId","required":true,"schema":{"format":"uuid","type":"string"}},{"description":"First day (in the caller's Tracklines-Timezone); default 90 days ago.","in":"query","name":"from","schema":{"format":"date","type":"string"}},{"description":"Last day","in":"query","inclusive; default today. At most two years after from.":null,"name":"to","schema":{"format":"date","type":"string"}},{"description":"md (or markdown): the report as a Markdown download (text/markdown, attachment) with Mermaid charts, for wikis and docs (TL-399).","in":"query","name":"format","schema":{"enum":["json","md","markdown"],"type":"string"}}]},"/boards/{boardId}/restore":{"parameters":[{"in":"path","name":"boardId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"operationId":"restoreBoard","responses":{"204":{"description":"Restored."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Put an archived board back on its project (board managers)","tags":["Boards"]}},"/boards/{boardId}/sprints":{"get":{"operationId":"listSprints","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Sprint"},"type":"array"}}},"description":"Sprints, newest first."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"List a board's sprints","tags":["Sprints"]},"parameters":[{"$ref":"#/components/parameters/BoardId"}],"post":{"operationId":"startSprint","requestBody":{"content":{"application/json":{"schema":{"properties":{"goal":{"description":"Optional one-line goal.","maxLength":200,"type":"string"},"length_days":{"maximum":365,"minimum":1,"type":"integer"}},"required":["length_days"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sprint"}}},"description":"Sprint started."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin or the linked team's leader."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Board already has an open sprint, or is a kanban board."}},"summary":"Start a sprint and pull in unassigned board tickets","tags":["Sprints"]}},"/boards/{boardId}/team":{"parameters":[{"$ref":"#/components/parameters/BoardId"}],"put":{"description":"Org admins only, signed in with a Clerk session. The team's leaders' authority is scoped to the boards it runs. The team must belong to the organization and already be linked to the board's project; removing the team from the project unlinks it from that project's boards. null unlinks.","operationId":"setBoardTeam","requestBody":{"content":{"application/json":{"schema":{"properties":{"team_id":{"format":"uuid","type":["string","null"]}},"required":["team_id"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Board updated."},"403":{"description":"Not a signed-in org admin."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The team isn't linked to the board's project."}},"summary":"Set the team that runs a board","tags":["Teams"]}},"/boards/{boardId}/velocity":{"get":{"description":"A closed sprint's completed value is its tickets that were done when it closed (closing carries the rest into the next sprint). Unit is points on a board with an estimate scale (t-shirt sizes count XS=1, S=2, M=3, L=5, XL=8; unestimated tickets count 0), otherwise tickets.","operationId":"getBoardVelocity","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Velocity"}}},"description":"Velocity history and burn data."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Velocity per closed sprint, plus the open sprint's burn data","tags":["Sprints"]},"parameters":[{"$ref":"#/components/parameters/BoardId"}]},"/columns/{columnId}":{"delete":{"operationId":"deleteColumn","responses":{"204":{"description":"Column deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin or the linked team's leader."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Column still contains tickets."}},"summary":"Delete an empty column","tags":["Columns"]},"parameters":[{"in":"path","name":"columnId","required":true,"schema":{"format":"uuid","type":"string"}}],"patch":{"operationId":"updateColumn","requestBody":{"content":{"application/json":{"schema":{"minProperties":1,"properties":{"name":{"minLength":1,"type":"string"},"wip_limit":{"description":"Advisory; moves over the limit still go through. 0 removes it.","maximum":999,"minimum":0,"type":"integer"}},"type":"object"}}},"required":true},"responses":{"204":{"description":"Column updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin or the linked team's leader."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Rename a column and/or set its work-in-progress limit","tags":["Columns"]}},"/columns/{columnId}/tickets":{"post":{"description":"The ticket joins the board's open sprint. On that sprint's last day it goes to the bottom of the project backlog instead: still 201, with `column_id` null, `redirected_to_backlog: true`, and any estimate dropped.","operationId":"createBoardTicket","parameters":[{"in":"path","name":"columnId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Ticket"},{"properties":{"redirected_to_backlog":{"description":"Present (true) when the sprint was locked and the ticket went to the backlog.","type":"boolean"}},"type":"object"}]}}},"description":"Ticket created."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}},"summary":"Create a ticket in a board column","tags":["Tickets"]}},"/enquiries":{"post":{"description":"Public, no account needed, and rate limited per address. Filed in the ChewedFeed HQ enquiry inbox.","operationId":"sendEnquiry","requestBody":{"content":{"application/json":{"schema":{"properties":{"company":{"maxLength":160,"type":"string"},"email":{"format":"email","maxLength":320,"type":"string"},"message":{"maxLength":5000,"type":"string"},"name":{"maxLength":160,"type":"string"},"source_path":{"description":"The page the form was sent from.","maxLength":500,"type":"string"}},"required":["name","email","message"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Sent."},"400":{"description":"A required field is missing","or the email isn't valid.":null,"too long":null},"429":{"description":"Too many messages from this address."},"502":{"description":"The enquiry system couldn't be reached; try again."},"503":{"description":"Enquiries aren't configured."}},"security":[],"summary":"Send a contact-form message to the TrackLin.es team","tags":["Contact"]}},"/google-chat":{"delete":{"operationId":"deleteGoogleChat","responses":{"204":{"description":"Removed."}},"summary":"Stop posting to a Google Chat space (org admins)","tags":["Organizations"]},"get":{"operationId":"getGoogleChat","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"connected":{"type":"boolean"},"kinds":{"items":{"enum":["created","review","approval","over_capacity","finished","wont_do","assigned","sent_back","retention"],"type":"string"},"type":"array"},"updated_at":{"format":"date-time","type":"string"},"updated_by":{"type":"string"}},"type":"object"}}},"description":"The space's settings."}},"summary":"Whether a Google Chat space is set up (any member); never returns its URL","tags":["Organizations"]},"parameters":[{"description":"The project whose space this is; leave out for the whole organization's space.","in":"query","name":"project_id","schema":{"format":"uuid","type":"string"}}],"put":{"description":"url is the space's incoming webhook (https://chat.googleapis.com/v1/spaces/…); leave it out to change only the kinds. Defaults: created, review, approval, over_capacity, finished, wont_do. A project's space gets its events; the org's gets every project's.","operationId":"setGoogleChat","requestBody":{"content":{"application/json":{"schema":{"properties":{"kinds":{"items":{"type":"string"},"type":"array"},"project_id":{"format":"uuid","type":"string"},"url":{"type":"string"}},"type":"object"}}},"required":true},"responses":{"204":{"description":"Saved."},"400":{"an unknown kind":null,"description":"Not a Google Chat webhook URL","or no URL yet.":null},"403":{"description":"Not a signed-in org admin."}},"summary":"Post notifications into a Google Chat space (org admins)","tags":["Organizations"]}},"/google-chat/test":{"post":{"operationId":"testGoogleChat","requestBody":{"content":{"application/json":{"schema":{"properties":{"project_id":{"format":"uuid","type":"string"}},"type":"object"}}}},"responses":{"204":{"description":"Sent."},"409":{"description":"No space set up."},"502":{"description":"Google Chat refused it (the reason is in the body)."}},"summary":"Post a test message to a Google Chat space (org admins)","tags":["Organizations"]}},"/health":{"get":{"operationId":"healthCheck","responses":{"200":{"content":{"text/plain":{"schema":{"example":"ok","type":"string"}}},"description":"API, PostgreSQL, and Valkey are healthy."},"503":{"description":"Database or Valkey is unavailable."}},"security":[],"servers":[{"description":"Use the deployed backend origin directly in production; the frontend API proxy does not expose /health.","url":"http://localhost:8080"}],"summary":"Check API and backing services health","tags":["System"]}},"/intake-submissions/{submissionId}/dismiss":{"parameters":[{"in":"path","name":"submissionId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"operationId":"dismissIntakeSubmission","responses":{"204":{"description":"Dismissed."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Already triaged."}},"summary":"Close a pending request without making a ticket","tags":["Intake"]}},"/intake-submissions/{submissionId}/promote":{"parameters":[{"in":"path","name":"submissionId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"operationId":"promoteIntakeSubmission","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeSubmission"}}},"description":"Promoted","with its ticket.":null},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Already triaged."}},"summary":"Turn a pending request into an unassigned backlog ticket","tags":["Intake"]}},"/intake/{token}":{"get":{"description":"Says nothing else about the project.","operationId":"intakeForm","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"open":{"type":"boolean"}},"type":"object"}}},"description":"Open."},"404":{"description":"Closed or unknown link."}},"security":[],"summary":"Whether a request form link is open (public, no account)","tags":["Intake"]},"parameters":[{"description":"The project's unlisted form link token.","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"post":{"description":"Held for the team to triage; never put on a board. Limits: 5 per sender per hour, 200 per project per day. A filled-in website field (hidden from people) marks a bot: accepted and dropped.","operationId":"intakeSubmit","requestBody":{"content":{"application/json":{"schema":{"properties":{"contact":{"description":"Optional","e.g. an email; seen by the team only.":null,"maxLength":200,"type":"string"},"details":{"maxLength":5000,"type":"string"},"title":{"maxLength":200,"type":"string"},"type":{"enum":["bug","feature"],"type":"string"},"website":{"description":"Honeypot; leave empty.","type":"string"}},"required":["type","title"],"type":"object"}}},"required":true},"responses":{"202":{"description":"Received."},"400":{"description":"Invalid request."},"404":{"description":"Closed or unknown link."},"429":{"description":"Too many requests from this sender."}},"security":[],"summary":"Send a bug report or feature request through a project's form (public, no account)","tags":["Intake"]}},"/integrations/google-calendar":{"delete":{"operationId":"disconnectGoogleCalendar","responses":{"204":{"description":"Disconnected."}},"summary":"Disconnect Google Calendar (org admins); the time away it found is deleted with it.","tags":["Organizations"]},"get":{"operationId":"getGoogleCalendar","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"available":{"description":"The server has a Google service account.","type":"boolean"},"client_id":{"description":"The service account's client ID","for admin.google.com → Security → API controls → Domain-wide delegation.":null,"type":"string"},"connected":{"type":"boolean"},"connection":{"properties":{"admin_email":{"description":"Org admins only.","type":"string"},"connected_at":{"format":"date-time","type":"string"},"domain":{"type":"string"},"holidays_calendar":{"type":"string"},"last_sync_at":{"format":"date-time","nullable":true,"type":"string"},"last_sync_error":{"type":"string"}},"type":"object"},"provider":{"enum":["google-calendar"],"type":"string"},"scope":{"description":"The one scope to authorize (read-only calendars).","type":"string"}},"type":"object"}}},"description":"Whether the server offers it and the org is connected, plus what a Workspace admin authorizes for domain-wide delegation."}},"summary":"Google Calendar (TL-404) for the org; any member. The admin email is for org admins only.","tags":["Organizations"]},"put":{"operationId":"connectGoogleCalendar","requestBody":{"content":{"application/json":{"schema":{"properties":{"admin_email":{"description":"A user in the domain.","type":"string"},"domain":{"example":"example.com","type":"string"},"holidays_calendar":{"description":"Optional shared calendar of company days off","e.g. en.uk#holiday@group.v.calendar.google.com":null,"type":"string"}},"required":["domain","admin_email"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Connected."},"400":{"description":"Bad input","e.g. delegation not granted).":null,"or Google refused (the message says what to fix":null},"403":{"description":"Not an org admin."},"502":{"description":"Google couldn't be reached."},"503":{"description":"The server has no Google service account."}},"summary":"Connect Google Calendar (org admins). Google is asked first, so only a working connection is saved.","tags":["Organizations"]}},"/integrations/google-calendar/sync":{"post":{"operationId":"syncGoogleCalendar","responses":{"200":{"description":"The connection","with last_sync_at and last_sync_error (empty when it worked).":null},"404":{"description":"Not connected."},"503":{"description":"The server has no Google service account."}},"summary":"Sync time away from Google Calendar now (org admins); it also runs hourly and right after connecting.","tags":["Organizations"]}},"/integrations/google-calendar/test":{"post":{"operationId":"testGoogleCalendar","responses":{"204":{"description":"Access works."},"400":{"description":"Google refused; the message says why."},"404":{"description":"Not connected."}},"summary":"Check Google still lets us read the org's calendars (org admins).","tags":["Organizations"]}},"/integrations/{provider}":{"delete":{"description":"For GitHub, ?installation=\u003cid\u003e disconnects just that account; without it, every GitHub account is disconnected.","operationId":"disconnectIntegration","parameters":[{"description":"GitHub only: the installation (account) to disconnect.","in":"query","name":"installation","schema":{"type":"string"}}],"responses":{"204":{"description":"Disconnected."},"403":{"description":"Not a signed-in org admin."},"404":{"description":"That GitHub account isn't connected to this org."}},"summary":"Disconnect the org from a provider (org admins)","tags":["Organizations"]},"get":{"operationId":"getIntegration","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"account":{"description":"GitHub: the first connected account (kept for older clients); see accounts.","type":"string"},"accounts":{"description":"GitHub only: every GitHub account (org or user) the org connected. Each project links repos from whichever account owns them.","items":{"properties":{"account":{"type":"string"},"installation_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"updated_by":{"type":"string"}},"type":"object"},"type":"array"},"base_url":{"type":"string"},"connected":{"type":"boolean"},"provider":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"updated_by":{"type":"string"}},"type":"object"}}},"description":"Connection status."},"403":{"description":"Not a signed-in org admin."}},"summary":"Whether the org is connected to a provider (any org member); never returns the secret","tags":["Organizations"]},"parameters":[{"in":"path","name":"provider","required":true,"schema":{"enum":["bugfixes"],"type":"string"}}],"put":{"description":"The token is sealed (AES-GCM) with the server's TRACKLINES_SECRET_KEY before it's stored; 503 when the server has no key.","operationId":"connectIntegration","requestBody":{"content":{"application/json":{"schema":{"properties":{"base_url":{"default":"https://api.bugfix.es","type":"string"},"token":{"type":"string"}},"required":["token"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Connected."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Not a signed-in org admin."},"503":{"description":"Integrations aren't configured on this server."}},"summary":"Connect the org to a provider (org admins). Bugfixes takes an access token from its settings.","tags":["Organizations"]}},"/integrations/{provider}/complete":{"parameters":[{"in":"path","name":"provider","required":true,"schema":{"enum":["github","bitbucket"],"type":"string"}}],"post":{"operationId":"integrationComplete","requestBody":{"content":{"application/json":{"schema":{"properties":{"code":{"description":"Bitbucket OAuth code.","type":"string"},"installation_id":{"description":"GitHub. Checked with the app before it's stored.","type":"string"},"state":{"type":"string"}},"required":["state"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Connected."},"400":{"description":"The state expired or is for another org."},"403":{"description":"Not a signed-in org admin."},"502":{"bad code).":null,"description":"The provider refused (e.g. not this app's installation"}},"summary":"Finish connecting after the provider sends the admin back","tags":["Organizations"]}},"/integrations/{provider}/connect":{"get":{"description":"The link carries a sealed state tied to the org (15 minutes). 503 when the provider isn't configured on the server.","operationId":"integrationConnectURL","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"url":{"type":"string"}},"type":"object"}}},"description":"The link."},"403":{"description":"Not a signed-in org admin."},"503":{"description":"Provider not configured."}},"summary":"Where an org admin goes to install the GitHub App or authorize Bitbucket","tags":["Organizations"]},"parameters":[{"in":"path","name":"provider","required":true,"schema":{"enum":["github","bitbucket"],"type":"string"}}]},"/integrations/{provider}/repos":{"get":{"operationId":"integrationRepos","responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"default_branch":{"type":"string"},"full_name":{"type":"string"},"private":{"type":"boolean"}},"type":"object"},"type":"array"}}},"description":"Repositories."},"409":{"description":"Not connected."}},"summary":"Repositories the org's connection can see (org admins), first 100","tags":["Organizations"]},"parameters":[{"in":"path","name":"provider","required":true,"schema":{"enum":["github","bitbucket"],"type":"string"}}]},"/invitations":{"get":{"description":"Uses the caller's verified Clerk email addresses and does not require an active organization.","operationId":"listMyInvitations","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Invitation"},"type":"array"}}},"description":"Pending invitations addressed to the caller."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"API keys cannot inspect or accept invitations."},"502":{"description":"Clerk could not load the caller's verified email addresses."}},"summary":"List pending invitations sent to the caller","tags":["Organizations"]}},"/keys":{"get":{"description":"Service keys belong to the organization, so every member sees them and when they expire, whoever made them.","operationId":"listApiKeys","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ApiKey"},"type":"array"}}},"description":"API key metadata. Secret values are never returned after creation."},"401":{"$ref":"#/components/responses/Unauthorized"}},"security":[{"clerkSession":[]}],"summary":"List the caller's active API keys, and the organization's service keys","tags":["API keys"]},"post":{"description":"The plaintext key is returned only once. This route requires a Clerk session. Only organization admins can create service keys.","operationId":"createApiKey","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApiKey"}}},"description":"Key created; save the returned `key` securely now."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Only organization admins can create service keys."},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"clerkSession":[]}],"summary":"Create a TrackLin.es API key","tags":["API keys"]}},"/keys/{keyId}":{"delete":{"description":"Your own keys; organization admins can also revoke any of the organization's service keys.","operationId":"revokeApiKey","responses":{"204":{"description":"API key revoked."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"clerkSession":[]}],"summary":"Revoke an API key","tags":["API keys"]},"parameters":[{"in":"path","name":"keyId","required":true,"schema":{"format":"uuid","type":"string"}}]},"/live/sessions":{"get":{"description":"Every hosted standup (on a board) and refinement (on a project) running in the caller's organization, in one request. Archived boards are left out. Empty when live sessions are off.","operationId":"listRunningSessions","responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"host":{"type":"string"},"id":{"description":"The board's (standup) or project's (refinement).","format":"uuid","type":"string"},"kind":{"enum":["standup","refinement"],"type":"string"},"name":{"type":"string"},"people":{"items":{"type":"string"},"type":"array"},"project_id":{"format":"uuid","type":"string"},"project_name":{"type":"string"}},"type":"object"},"type":"array"}}},"description":"Running sessions, by project."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"The organization's running standups and refinements","tags":["Live"]}},"/mcp":{"post":{"description":"MCP over Streamable HTTP (stateless; responses are application/json). Send JSON-RPC MCP messages (initialize, tools/list, tools/call). Authenticate like the REST API (tl_ API key or Clerk session). Each tool calls the matching REST route with the caller's credentials, so results, permissions and errors are the same as REST; a failed call is a tool result with isError and the REST status in its text. GET and DELETE return 405 (no sessions or server-initiated streams).","operationId":"mcp","requestBody":{"content":{"application/json":{"schema":{"description":"A JSON-RPC 2.0 MCP message.","type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"JSON-RPC response."},"202":{"description":"Notification accepted."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Model Context Protocol endpoint for bots","tags":["MCP"]}},"/me/google-chat":{"delete":{"operationId":"unlinkGoogleChat","responses":{"204":{"description":"Unlinked."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Stop personal messages in Google Chat","tags":["Notifications"]},"get":{"description":"The TrackLin.es Google Chat app sends your own notifications (assigned, mentioned, review…) to your chat with it, once you've linked your account from the link it sends you.","operationId":"getGoogleChatLink","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"available":{"type":"boolean"},"linked":{"type":"boolean"},"linked_at":{"format":"date-time","type":"string"}},"type":"object"}}},"description":"available is false when the server has no Chat app."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Whether your Google Chat is linked for personal messages","tags":["Notifications"]}},"/me/google-chat/link":{"post":{"description":"Signed-in sessions only (not API keys). Linking again replaces the earlier link.","operationId":"linkGoogleChat","requestBody":{"content":{"application/json":{"schema":{"properties":{"code":{"type":"string"}},"required":["code"],"type":"object"}}},"required":true},"responses":{"200":{"description":"Linked; the app confirms in Google Chat."},"400":{"description":"The code expired (30 minutes) or was already used."},"403":{"description":"Not a signed-in session."},"503":{"description":"The Chat app isn't set up on this server."}},"summary":"Link your Google Chat with the one-time code the Chat app sent you","tags":["Notifications"]}},"/mentions":{"get":{"description":"One row per ticket, newest mention first. Mentions are recorded when a comment names someone, and stay when the notification is cleared.","operationId":"listMentions","parameters":[{"in":"query","name":"limit","schema":{"default":10,"maximum":100,"minimum":1,"type":"integer"}},{"description":"true leaves out done and closed tickets.","in":"query","name":"hide_done","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"actor":{"description":"Who mentioned the caller last (user id or agent name).","type":"string"},"comment_id":{"description":"The comment, or \"\" for mentions from before they were recorded.","type":"string"},"done":{"type":"boolean"},"key":{"example":"TL-123","type":"string"},"mentioned_at":{"format":"date-time","type":"string"},"project_name":{"type":"string"},"ticket_id":{"format":"uuid","type":"string"},"title":{"type":"string"}},"type":"object"},"type":"array"}}},"description":"Mentioned tickets."},"400":{"description":"limit is not 1–100."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Tickets that @mention the caller","tags":["Tickets"]}},"/mentions/page":{"get":{"description":"The same list as /mentions, with how many there are in all.","operationId":"pageMentions","parameters":[{"in":"query","name":"page","schema":{"default":1,"minimum":1,"type":"integer"}},{"in":"query","name":"per_page","schema":{"default":25,"maximum":100,"minimum":1,"type":"integer"}},{"description":"true leaves out done and closed tickets.","in":"query","name":"hide_done","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"mentions":{"description":"As in /mentions.","items":{"type":"object"},"type":"array"},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}},"type":"object"}}},"description":"One page of mentioned tickets."},"400":{"description":"page or per_page is out of range."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Tickets that @mention the caller, a page at a time","tags":["Tickets"]}},"/notifications":{"get":{"description":"Events: assigned (someone assigned you a ticket), mention (@you in a comment), finished (a ticket you raised was finished or resolved), wont_do (a ticket you raised was closed as won't do, with the reason), review (an agent finished a ticket on a board you manage), sent_back (your ticket was sent back from review), approval (a planned sprint on a board you manage went over capacity). Nobody is told about their own actions; only people (not agents) get notifications. MCP tool list_notifications.","operationId":"listNotifications","parameters":[{"in":"query","name":"page","schema":{"default":1,"minimum":1,"type":"integer"}},{"in":"query","name":"per_page","schema":{"default":20,"maximum":100,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Notification"},"type":"array"}}},"description":"Notifications."},"403":{"description":"Not signed in to an organization."}},"summary":"The signed-in user's inbox in the active organization, unread first, then newest","tags":["Notifications"]}},"/notifications/read":{"delete":{"operationId":"clearReadNotifications","responses":{"204":{"description":"Cleared."}},"summary":"Remove all your notifications that are already read","tags":["Notifications"]}},"/notifications/read-all":{"post":{"operationId":"readAllNotifications","responses":{"204":{"description":"Read."}},"summary":"Mark all your notifications read","tags":["Notifications"]}},"/notifications/unread-count":{"get":{"operationId":"unreadNotificationCount","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"count":{"type":"integer"}},"type":"object"}}},"description":"The count."}},"summary":"How many notifications are unread","tags":["Notifications"]}},"/notifications/{notificationId}":{"delete":{"operationId":"dismissNotification","parameters":[{"in":"path","name":"notificationId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Removed."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Remove one of your notifications","tags":["Notifications"]}},"/notifications/{notificationId}/read":{"post":{"operationId":"readNotification","parameters":[{"in":"path","name":"notificationId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Read."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Mark one of your notifications read","tags":["Notifications"]}},"/openapi.json":{"get":{"description":"Public; no key needed. The same document as /openapi.yaml.","operationId":"getOpenAPIJSON","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"The OpenAPI document."}},"security":[],"summary":"This API description, as JSON","tags":["System"]}},"/openapi.yaml":{"get":{"description":"Public; no key needed. Built into the API, so it always matches what's running.","operationId":"getOpenAPIYAML","responses":{"200":{"content":{"application/yaml":{"schema":{"type":"string"}}},"description":"The OpenAPI document."}},"security":[],"summary":"This API description, as YAML","tags":["System"]}},"/org/reports/sprints":{"get":{"operationId":"orgSprintReport","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportRollup"}}},"description":"The roll-up."},"403":{"description":"Not an organization admin."}},"summary":"Every project in the organization together (TL-398), by project. Organization admins only (people, not API keys).","tags":["Sprints"]},"parameters":[{"description":"First day (in the caller's Tracklines-Timezone); default 90 days ago.","in":"query","name":"from","schema":{"format":"date","type":"string"}},{"description":"Last day","in":"query","inclusive; default today.":null,"name":"to","schema":{"format":"date","type":"string"}},{"description":"md (or markdown): the report as a Markdown download (text/markdown, attachment) with Mermaid charts, for wikis and docs (TL-399).","in":"query","name":"format","schema":{"enum":["json","md","markdown"],"type":"string"}}]},"/organizations/admins":{"get":{"description":"Organizations and their members live in Clerk; TrackLin.es only records which members are admins. The first time an org's admins are needed, one is chosen from Clerk and stored: the user who created the organization if they are still a member, otherwise the longest-standing member with the Clerk org:admin role, otherwise the longest-standing member. granted_by records which rule applied (clerk:creator, clerk:admin-role, clerk:earliest-member) or the admin who granted it. If Clerk can't be reached nothing is stored and the next request tries again (502); an organization with no members gets no admin (409).","operationId":"listOrganizationAdmins","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/OrgAdmin"},"type":"array"}}},"description":"Admins oldest first"},"403":{"description":"No active organization."},"409":{"description":"The organization has no members to make admin."},"502":{"description":"Clerk could not be reached."}},"summary":"List the organization's TrackLin.es admins","tags":["Organizations"]}},"/organizations/admins/{userId}":{"delete":{"description":"Org admins only, signed in with a Clerk session. The last admin can't be removed.","operationId":"revokeOrganizationAdmin","responses":{"204":{"description":"Removed."},"403":{"description":"Not a signed-in org admin."},"409":{"description":"That is the organization's last admin."}},"summary":"Remove an admin","tags":["Organizations"]},"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"put":{"description":"Org admins only, signed in with a Clerk session (API keys are refused). The user must be a Clerk member of the organization.","operationId":"grantOrganizationAdmin","responses":{"204":{"description":"Granted (or already an admin)."},"403":{"description":"Not a signed-in org admin."},"422":{"description":"The user is not a member of this organization."}},"summary":"Make an organization member an admin","tags":["Organizations"]}},"/organizations/invitations":{"get":{"description":"Org admins only. Pending, accepted, and revoked TrackLin.es invitations are returned newest first; expired pending invitations are reported as expired.","operationId":"listOrganizationInvitations","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Invitation"},"type":"array"}}},"description":"Organization invitations."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin."}},"summary":"List invitations created for the active organization","tags":["Organizations"]},"post":{"description":"Org admins only. Clerk sends an organization invitation with the org:member role. For team or project scope, the recipient must accept through Clerk and then call the accept operation below with a signed-in session using the invited, verified email address. Team invites create ordinary team membership; project invites create project membership. Organization members retain read access to all organization projects.","operationId":"createOrganizationInvitation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invitation"}}},"description":"Invitation created. accept_url is the Clerk-hosted invitation link."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The plan's member limit is reached (Free and Pro: 20, counting invitations not yet accepted); upgrade to Enterprise."},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The email address is already an organization member."},"502":{"description":"Clerk could not create or validate the invitation."}},"summary":"Invite an email address to an organization, team, or project","tags":["Organizations"]}},"/organizations/invitations/{id}":{"delete":{"operationId":"revokeOrganizationInvitation","responses":{"204":{"description":"Invitation revoked in Clerk and TrackLin.es."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Only pending invitations can be revoked."},"502":{"description":"Clerk could not revoke the invitation."}},"summary":"Revoke a pending invitation","tags":["Organizations"]},"get":{"operationId":"getOrganizationInvitation","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invitation"}}},"description":"Invitation details."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Inspect an organization invitation","tags":["Organizations"]},"parameters":[{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}]},"/organizations/invitations/{id}/accept":{"post":{"description":"The invitee must first accept the Clerk invitation. This endpoint then verifies the Clerk invitation, active organization membership, and a verified matching email. Team and project invitations create the corresponding local membership; team invitations never assign team leader authority. API keys are refused.","operationId":"acceptOrganizationInvitation","parameters":[{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Invitation accepted and local membership applied."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The caller is an API key or does not own the invited verified email address."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Clerk invitation or organization membership is not accepted yet."},"410":{"description":"Invitation expired, revoked, or already accepted by another user."},"502":{"description":"Clerk could not verify the invitation or recipient."}},"summary":"Complete an accepted Clerk invitation in TrackLin.es","tags":["Organizations"]}},"/organizations/me":{"get":{"operationId":"getMyOrganizationRoles","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"admin":{"type":"boolean"},"leads":{"items":{"format":"uuid","type":"string"},"type":"array"},"org_id":{"type":"string"},"user_id":{"type":"string"}},"type":"object"}}},"description":"Whether the caller is an org admin, and the teams they lead."},"403":{"description":"No active organization."},"502":{"description":"Clerk could not be reached to resolve the first admin."}},"summary":"The caller's roles in their active organization","tags":["Organizations"]}},"/organizations/members":{"get":{"description":"Returns role-free member details for the caller's active organization. Results are paginated.","operationId":"listOrganizationMembers","parameters":[{"in":"query","name":"limit","schema":{"default":100,"maximum":500,"minimum":1,"type":"integer"}},{"in":"query","name":"offset","schema":{"default":0,"minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationMemberPage"}}},"description":"A page of active organization members."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"No active organization in the caller's session or API key."},"502":{"description":"The identity provider could not return organization membership."}},"summary":"List active organization members","tags":["Organizations"]}},"/planned-sprints/{plannedSprintId}":{"delete":{"operationId":"unplanSprint","responses":{"204":{"description":"Removed; a later planned sprint moves up."},"403":{"description":"Not an org admin or the board team's leader."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Remove a planned sprint; its tickets stay in the backlog, unplanned and unestimated","tags":["Sprints"]},"parameters":[{"in":"path","name":"plannedSprintId","required":true,"schema":{"format":"uuid","type":"string"}}],"patch":{"operationId":"updatePlannedSprint","requestBody":{"content":{"application/json":{"schema":{"properties":{"goal":{"maxLength":200,"type":"string"},"length_days":{"maximum":365,"minimum":1,"type":"integer"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PlannedSprint"},"type":"array"}}},"description":"The board's planned sprints."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Not an org admin or the board team's leader."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Change a planned sprint's length and/or goal (org admins and the board team's leaders)","tags":["Sprints"]}},"/planned-sprints/{plannedSprintId}/approve":{"parameters":[{"in":"path","name":"plannedSprintId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"description":"Records the total, who and when. If the planned total later goes above it, the sprint needs approval again.","operationId":"approvePlannedSprint","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlannedSprint"}}},"description":"The planned sprint."},"403":{"description":"Not an org admin or the board team's leader","or an API key.":null},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Accept a planned sprint's current total over capacity (org admins and the board team's leaders, signed in)","tags":["Sprints"]}},"/plans":{"get":{"operationId":"listPlans","responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"id":{"enum":["free","pro","enterprise"],"type":"string"},"members":{"description":"Member limit; null is unlimited (TL-418).","type":["integer","null"]},"price":{"properties":{"amount":{"type":"integer"},"currency":{"type":"string"},"interval":{"type":"string"}},"type":["object","null"]},"projects":{"type":["integer","null"]}},"type":"object"},"type":"array"}}},"description":"Free, Pro and Enterprise. projects null means unlimited; price is in the currency's smallest unit, from Stripe, and missing when it can't be read."}},"security":[],"summary":"The plans, their project limits and prices (public)","tags":["Billing"]}},"/projects":{"get":{"description":"Use a project's ID when creating a ticket directly in its backlog.","operationId":"listProjects","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Project"},"type":"array"}}},"description":"Projects owned by the API key's owner."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"List projects accessible to this agent","tags":["Projects"]},"post":{"operationId":"createProject","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}},"description":"Project created."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Free plan project limit reached."},"500":{"$ref":"#/components/responses/InternalError"}},"summary":"Create a project","tags":["Projects"]}},"/projects/{projectId}":{"delete":{"description":"This action cannot be undone and permanently deletes all work under the project. Organization admin session required.","operationId":"deleteProject","responses":{"204":{"description":"Project deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Organization admin session required."},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"bearerAuth":[]}],"summary":"Permanently delete a project and its boards, tickets, roadmaps, repos, and intake","tags":["Projects"]},"get":{"operationId":"getProject","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}},"description":"Project details."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Get a project with its boards and roadmaps","tags":["Projects"]},"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"patch":{"description":"key_prefix (optional) renames every ticket key in the project (TL-123 → TRK-123); only organization admins can change it. Use 2–6 capital letters or digits starting with a letter; 409 if another project in the org uses it.","operationId":"updateProject","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectInput"}}},"required":true},"responses":{"204":{"description":"Project updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Changing key_prefix requires an organization admin session."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"key_prefix is used by another project in the organization."}},"summary":"Update a project","tags":["Projects"]}},"/projects/{projectId}/archived-boards":{"get":{"operationId":"listArchivedBoards","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Board"},"type":"array"}}},"description":"Boards."}},"summary":"A project's archived boards, most recently archived first","tags":["Boards"]},"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}}]},"/projects/{projectId}/backlog":{"get":{"operationId":"listBacklog","parameters":[{"in":"query","name":"type","schema":{"enum":["bug","feature","task","epic","spike"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Ticket"},"type":"array"}}},"description":"Backlog tickets, ordered by position."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"List a project's backlog","tags":["Tickets"]},"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"description":"Creates a ticket without requiring a board or column. The ticket is attributed to the API key's agent name and defaults to type `task` if `type` is omitted.","operationId":"createBacklogTicket","parameters":[{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"description":"Creating a project from the dashboard returns HTTP 403.","title":"Project creation returns an error","type":"bug"},"schema":{"$ref":"#/components/schemas/CreateBacklogTicket"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}},"description":"Backlog ticket created."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}},"summary":"Create a ticket directly in a project's backlog","tags":["Tickets"]}},"/projects/{projectId}/backlog/page":{"get":{"description":"By default ready tickets come first, then blocked ones, ordered by backlog position. `sort=newest` or `sort=oldest` orders the full filtered backlog by creation date before pagination. The `counts` field is per type and respects label and priority filters, but not the type filter, for type tabs. Label counts cover the whole backlog. The unpaged list stays at /projects/{projectId}/backlog.","operationId":"pageBacklog","parameters":[{"description":"Only this epic's sub-tickets, at any depth.","in":"query","name":"epic","schema":{"format":"uuid","type":"string"}},{"description":"Any of these priorities; counts follow it.","explode":true,"in":"query","name":"priority","schema":{"items":{"enum":["low","medium","high","urgent"],"type":"string"},"type":"array"},"style":"form"},{"$ref":"#/components/parameters/ProjectId"},{"in":"query","name":"type","schema":{"enum":["bug","feature","task","epic","spike"],"type":"string"}},{"description":"Order tickets globally before pagination. Defaults to the manual backlog order.","in":"query","name":"sort","schema":{"default":"manual","enum":["manual","newest","oldest"],"type":"string"}},{"description":"Repeatable; a ticket matches if it has any of them (case-insensitive).","explode":true,"in":"query","name":"label","schema":{"items":{"type":"string"},"type":"array"},"style":"form"},{"in":"query","name":"page","schema":{"default":1,"minimum":1,"type":"integer"}},{"in":"query","name":"per_page","schema":{"default":25,"maximum":100,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"counts":{"properties":{"all":{"type":"integer"},"bug":{"type":"integer"},"feature":{"type":"integer"},"task":{"type":"integer"}},"type":"object"},"labels":{"description":"Labels in the whole backlog (no filters applied), most used first, for a label filter.","items":{"properties":{"count":{"type":"integer"},"label":{"type":"string"}},"type":"object"},"type":"array"},"tickets":{"items":{"$ref":"#/components/schemas/Ticket"},"type":"array"}},"required":["tickets","counts","labels"],"type":"object"}}},"description":"The page and per-type counts."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"One page of the backlog, filtered and ordered by the server","tags":["Tickets"]}},"/projects/{projectId}/boards":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"createBoard","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoardInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Board"}}},"description":"Board created."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Create a team board, optionally from a template","tags":["Boards"]}},"/projects/{projectId}/epics":{"get":{"description":"Epics are large pieces of work: they stay in the backlog (never on a board or in a sprint), take no estimate, and can't sit anywhere inside another epic. Work joins one by becoming its sub-ticket. Progress counts the epic's direct sub-tickets (duplicates left out); size sums their estimates as points. Open epics come first, by backlog position.","operationId":"listEpics","parameters":[{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"done":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"key":{"type":["string","null"]},"priority":{"enum":["low","medium","high","urgent"],"type":"string"},"progress":{"$ref":"#/components/schemas/EpicProgress"},"resolution":{"type":["string","null"]},"title":{"type":"string"}},"type":"object"},"type":"array"}}},"description":"Epics."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"A project's epics, with progress and size","tags":["Tickets"]}},"/projects/{projectId}/intake":{"get":{"operationId":"intakeSettings","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeSettings"}}},"description":"Settings."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"The project's request form, open (with its link token) or closed","tags":["Intake"]},"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}}],"put":{"description":"Org admins only, in a signed-in session. API keys get 403, even an admin's.","operationId":"setIntake","requestBody":{"content":{"application/json":{"schema":{"properties":{"open":{"type":"boolean"},"rotate":{"type":"boolean"}},"required":["open"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntakeSettings"}}},"description":"Settings."},"403":{"description":"Not a signed-in org admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Open or close the request form; rotate replaces the link (the old one stops working)","tags":["Intake"]}},"/projects/{projectId}/intake/submissions":{"get":{"operationId":"listIntakeSubmissions","parameters":[{"in":"query","name":"status","schema":{"default":"pending","enum":["pending","promoted","dismissed"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/IntakeSubmission"},"type":"array"}}},"description":"Submissions."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Requests sent through the form, newest first (pending by default)","tags":["Intake"]},"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}}]},"/projects/{projectId}/labels":{"get":{"operationId":"listProjectLabels","parameters":[{"$ref":"#/components/parameters/ProjectId"}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"count":{"type":"integer"},"label":{"type":"string"}},"type":"object"},"type":"array"}}},"description":"Labels with ticket counts."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Labels in use in a project, most used first","tags":["Projects"]}},"/projects/{projectId}/live":{"get":{"description":"Server-sent events, like the board stream: the latest retained event on connect, then updates, with a heartbeat every 20 seconds. Events are `refinement-session` (the whole session) and `refinement-session-ended`.","operationId":"streamProjectLiveEvents","responses":{"200":{"content":{"text/event-stream":{"schema":{"type":"string"}}},"description":"Server-sent event stream."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"503":{"description":"Valkey is unavailable."}},"summary":"Stream the project's hosted refinement session","tags":["Projects"]},"parameters":[{"$ref":"#/components/parameters/ProjectId"}]},"/projects/{projectId}/live/refinement":{"delete":{"operationId":"endHostedRefinement","responses":{"204":{"description":"Ended."},"403":{"description":"Only the host can end it."},"404":{"description":"No session running."}},"summary":"End the session for everyone (host only)","tags":["Projects"]},"get":{"operationId":"getHostedRefinement","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"The session."},"404":{"description":"No session running","or the project isn't in the caller's organization.":null},"503":{"description":"Valkey is unavailable."}},"summary":"The project's running refinement session, with who's here","tags":["Projects"]},"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"startHostedRefinement","responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"Started; the caller is the host."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"A session is already running; join it instead."},"503":{"description":"Valkey is unavailable."}},"summary":"Start a refinement session and host it","tags":["Projects"]}},"/projects/{projectId}/live/refinement/focus":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"put":{"operationId":"setHostedRefinementFocus","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedFocus"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"The session."},"403":{"description":"Only the host can set it."},"404":{"description":"No session running."}},"summary":"Say what the host has open, so followers see it (host only)","tags":["Projects"]}},"/projects/{projectId}/live/refinement/host":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"put":{"description":"The host can hand over to anyone in the session. Anyone in the session can make themselves host once the host hasn't been seen for a minute.","operationId":"handOverHostedRefinement","requestBody":{"content":{"application/json":{"schema":{"properties":{"user_id":{"type":"string"}},"required":["user_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"The session with its new host."},"400":{"description":"That person isn't in the session."},"403":{"description":"Only the host can hand over (or take over from a host who has gone)."},"404":{"description":"No session running."}},"summary":"Hand over hosting, or take over from a host who has gone","tags":["Projects"]}},"/projects/{projectId}/live/refinement/join":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"description":"Call every 30 seconds while taking part; anyone not seen for a minute drops off the list. A host gone for two minutes ends the session.","operationId":"joinHostedRefinement","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HostedSession"}}},"description":"The session","with the caller in it.":null},"404":{"description":"No session running."}},"summary":"Join the session, and stay in it","tags":["Projects"]}},"/projects/{projectId}/live/refinement/leave":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"leaveHostedRefinement","responses":{"204":{"description":"Left."}},"summary":"Leave the session","tags":["Projects"]}},"/projects/{projectId}/open-tickets":{"get":{"description":"Every ticket in the project that isn't in its board's Done column, plus the backlog, without descriptions. Ordered the way work is picked: tickets in their board's open sprint first, then priority (urgent first), then on a board before in the backlog, then oldest. The MCP tool list_open_tickets maps to this.","operationId":"listOpenTickets","parameters":[{"description":"unassigned, or me (the caller's agent name or user id). Omit for everyone.","in":"query","name":"assignee","schema":{"enum":["unassigned","me"],"type":"string"}},{"description":"exclude leaves out tickets waiting on unfinished work.","in":"query","name":"blocked","schema":{"enum":["exclude"],"type":"string"}},{"description":"Any of these priorities.","explode":true,"in":"query","name":"priority","schema":{"items":{"enum":["low","medium","high","urgent"],"type":"string"},"type":"array"},"style":"form"}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"assigned_to":{"type":["string","null"]},"blocked":{"type":"boolean"},"board_id":{"description":"Null in the backlog.","format":"uuid","type":["string","null"]},"board_name":{"type":["string","null"]},"column_id":{"format":"uuid","type":["string","null"]},"column_name":{"type":["string","null"]},"estimate":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"in_sprint":{"description":"In its board's open sprint.","type":"boolean"},"labels":{"items":{"type":"string"},"type":"array"},"priority":{"enum":["low","medium","high","urgent"],"type":"string"},"title":{"type":"string"},"type":{"enum":["bug","feature","task","epic","spike"],"type":"string"}},"type":"object"},"type":"array"}}},"description":"Open tickets."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Tickets that aren't done, across boards and backlog, in pick order","tags":["Tickets"]},"parameters":[{"$ref":"#/components/parameters/ProjectId"}]},"/projects/{projectId}/reports/sprints":{"get":{"operationId":"projectSprintReport","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportRollup"}}},"description":"The roll-up."},"400":{"description":"Bad dates."},"404":{"description":"Not your organization's project."}},"summary":"A project's sprint boards together (TL-398): each board's report, points and tickets totalled apart, predictability across both, and calendar months. Anyone in the organization.","tags":["Sprints"]},"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}},{"description":"First day (in the caller's Tracklines-Timezone); default 90 days ago.","in":"query","name":"from","schema":{"format":"date","type":"string"}},{"description":"Last day","in":"query","inclusive; default today.":null,"name":"to","schema":{"format":"date","type":"string"}},{"description":"md (or markdown): the report as a Markdown download (text/markdown, attachment) with Mermaid charts, for wikis and docs (TL-399).","in":"query","name":"format","schema":{"enum":["json","md","markdown"],"type":"string"}}]},"/projects/{projectId}/repos":{"delete":{"operationId":"unlinkProjectRepo","parameters":[{"in":"query","name":"provider","required":true,"schema":{"enum":["github","bitbucket"],"type":"string"}},{"in":"query","name":"full_name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Unlinked."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Unlink a repository (org admins)","tags":["Projects"]},"get":{"operationId":"projectRepos","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ProjectRepo"},"type":"array"}}},"description":"Linked repositories."}},"summary":"Repositories linked to the project","tags":["Projects"]},"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"linkProjectRepo","requestBody":{"content":{"application/json":{"schema":{"properties":{"full_name":{"type":"string"},"provider":{"enum":["github","bitbucket"],"type":"string"}},"required":["provider","full_name"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectRepo"}}},"description":"Linked."},"400":{"description":"The connection can't see that repository."},"409":{"description":"Already linked to a project in this org","or not connected.":null}},"summary":"Link a repository the org's connection can see (org admins); one project per repo in an org","tags":["Projects"]}},"/projects/{projectId}/roadmaps":{"post":{"description":"Org admins and the project's team leaders. Visibility defaults to team; public needs a signed-in org admin (API keys get 403).","operationId":"createRoadmap","parameters":[{"$ref":"#/components/parameters/ProjectId"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoadmapInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Roadmap"}}},"description":"Roadmap created."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Create a roadmap in a project","tags":["Roadmaps"]}},"/projects/{projectId}/teams":{"get":{"operationId":"listProjectTeams","parameters":[{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Team"},"type":"array"}}},"description":"Project teams"}},"summary":"List teams linked to a project","tags":["Teams"]}},"/projects/{projectId}/teams/{teamId}":{"delete":{"operationId":"removeProjectTeam","responses":{"204":{"description":"Team unlinked."},"403":{"description":"Requires a signed-in organization admin."}},"summary":"Unlink a team from a project","tags":["Teams"]},"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"format":"uuid","type":"string"}}],"put":{"operationId":"addProjectTeam","responses":{"204":{"description":"Team linked."},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Link a same-organization team to a project","tags":["Teams"]}},"/push":{"get":{"operationId":"pushKey","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"enabled":{"type":"boolean"},"public_key":{"type":"string"}},"type":"object"}}},"description":"Push settings."}},"summary":"Whether this server sends Web Push, and the VAPID public key browsers subscribe with","tags":["Notifications"]}},"/push/subscriptions":{"delete":{"operationId":"unsubscribePush","requestBody":{"content":{"application/json":{"schema":{"properties":{"endpoint":{"type":"string"}},"required":["endpoint"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Unsubscribed (your own devices only)."}},"summary":"Turn off notifications for this device","tags":["Notifications"]},"post":{"description":"The browser's PushSubscription. The endpoint must be https on a known push service (Google, Mozilla, Apple, Microsoft): the server sends to it. The same browser subscribing again takes the row over. The device then gets every notification from the inbox, even with TrackLin.es closed.","operationId":"subscribePush","requestBody":{"content":{"application/json":{"schema":{"properties":{"endpoint":{"format":"uri","type":"string"},"keys":{"properties":{"auth":{"type":"string"},"p256dh":{"type":"string"}},"required":["p256dh","auth"],"type":"object"}},"required":["endpoint","keys"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Subscribed."},"400":{"description":"Not a browser push subscription."},"403":{"description":"API keys can't subscribe devices."},"503":{"description":"Push isn't set up on this server."}},"summary":"Turn on notifications for this device (signed-in people only)","tags":["Notifications"]}},"/roadmaps":{"get":{"operationId":"listRoadmaps","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Roadmap"},"type":"array"}}},"description":"Roadmaps owned by the caller."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"List the caller's roadmaps","tags":["Roadmaps"]}},"/roadmaps/{roadmapId}":{"delete":{"description":"Signed-in org admins only; API keys get 403.","operationId":"deleteRoadmap","responses":{"204":{"description":"Roadmap deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete a roadmap","tags":["Roadmaps"]},"get":{"description":"Public roadmaps are readable without a token; login_only and team roadmaps require a signed-in member of the owning organization. Non-owner readers see progress counts but not linked-ticket IDs or titles.","operationId":"getRoadmap","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Roadmap"}}},"description":"Roadmap and items."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Roadmap is restricted to its owner."},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{},{"bearerAuth":[]}],"summary":"Get a roadmap","tags":["Roadmaps"]},"parameters":[{"in":"path","name":"roadmapId","required":true,"schema":{"format":"uuid","type":"string"}}],"put":{"description":"Org admins and the project's team leaders. Leaving out visibility keeps it. Changing visibility to or from public needs a signed-in org admin (API keys get 403).","operationId":"updateRoadmap","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoadmapInput"}}},"required":true},"responses":{"204":{"description":"Roadmap updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Update a roadmap","tags":["Roadmaps"]}},"/roadmaps/{roadmapId}/items":{"put":{"description":"Existing item IDs are updated in place; IDs omitted from the submitted list are removed. Unknown or omitted IDs create items.","operationId":"replaceRoadmapItems","parameters":[{"in":"path","name":"roadmapId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/RoadmapItemInput"},"type":"array"}}},"required":true},"responses":{"204":{"description":"Roadmap items replaced."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Replace a roadmap's item list","tags":["Roadmaps"]}},"/roadmaps/{roadmapId}/items/{itemId}/tickets":{"put":{"description":"Linking an epic counts its sub-tickets towards the item's progress (not the epic itself), worked out on every read, so ones added to the epic later count too.","operationId":"setRoadmapItemTickets","parameters":[{"in":"path","name":"roadmapId","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"path","name":"itemId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"ticket_ids":{"items":{"format":"uuid","type":"string"},"type":"array"}},"required":["ticket_ids"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Linked tickets replaced."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Replace tickets linked to a roadmap item","tags":["Roadmaps"]}},"/roadmaps/{roadmapId}/updates":{"get":{"description":"Uses the roadmap's public, login-only, or team visibility rules. Responses contain only update status, note, and date; linked ticket and organization details are never included.","operationId":"listRoadmapUpdates","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/RoadmapUpdate"},"type":"array"}}},"description":"Updates oldest first."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Roadmap is restricted to its owner."},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{},{"bearerAuth":[]}],"summary":"List dated progress updates","tags":["Roadmaps"]},"parameters":[{"in":"path","name":"roadmapId","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"description":"Appends an immutable update to a roadmap owned by the caller's active organization.","operationId":"publishRoadmapUpdate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoadmapUpdateInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoadmapUpdate"}}},"description":"Update published."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"bearerAuth":[]}],"summary":"Publish a dated progress update","tags":["Roadmaps"]}},"/search":{"get":{"description":"Full-text over titles (weighted highest), descriptions and comments. Every word must match, as a prefix, so it works while typing; punctuation is ignored. A ticket key in q (TL-123, case-insensitive) returns just that ticket. Ranked by relevance, then most recently changed. Without q it lists by recent change, filtered. The MCP tool search_tickets maps to this.","operationId":"searchTickets","parameters":[{"description":"Only this epic's sub-tickets, at any depth.","in":"query","name":"epic","schema":{"format":"uuid","type":"string"}},{"in":"query","name":"q","schema":{"type":"string"}},{"in":"query","name":"project","schema":{"format":"uuid","type":"string"}},{"in":"query","name":"board","schema":{"format":"uuid","type":"string"}},{"description":"Any of.","explode":true,"in":"query","name":"type","schema":{"items":{"enum":["bug","feature","task","epic","spike"],"type":"string"},"type":"array"},"style":"form"},{"description":"Any of.","explode":true,"in":"query","name":"priority","schema":{"items":{"enum":["low","medium","high","urgent"],"type":"string"},"type":"array"},"style":"form"},{"description":"Any of (case-insensitive).","explode":true,"in":"query","name":"label","schema":{"items":{"type":"string"},"type":"array"},"style":"form"},{"description":"me (the caller), unassigned, or a user id / agent name.","in":"query","name":"assignee","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"enum":["open","done"],"type":"string"}},{"description":"any: a ticket matching any word (of three letters or more) will do, most matches first, for spotting duplicates despite typos. Default: every word must match.","in":"query","name":"match","schema":{"enum":["any"],"type":"string"}},{"in":"query","name":"limit","schema":{"default":20,"maximum":50,"minimum":1,"type":"integer"}},{"description":"1-based, with limit. \"My work\" is assignee=me\u0026status=open across every board and project.","in":"query","name":"page","schema":{"default":1,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"allOf":[{"$ref":"#/components/schemas/Ticket"},{"properties":{"board_id":{"description":"Null in the backlog.","format":"uuid","type":["string","null"]},"board_name":{"type":["string","null"]},"column_name":{"type":["string","null"]},"done":{"type":"boolean"},"project_name":{"type":"string"},"snippet":{"description":"Where the words matched in the description or a comment; empty when only the title matched.","type":"string"}},"type":"object"}]},"type":"array"}}},"description":"Matching tickets, best first."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Search the organization's tickets","tags":["Tickets"]}},"/short-links":{"post":{"description":"For \"Copy link\": shortens a link to this site (tickets, boards…) through 1tn. The same link always gives the same short link. Links to any other site are refused.","operationId":"createShortLink","requestBody":{"content":{"application/json":{"schema":{"properties":{"title":{"description":"Shown for the short link (the shortener can't read pages behind our sign-in). Anyone can look a short link's title up, so keep it free of anything private. Default: TrackLin.es.","example":"TL-204 on TrackLin.es","maxLength":120,"type":"string"},"url":{"example":"https://tracklin.es/tickets/9205518f-7140-44cf-b155-4e9b8d6a2d48","type":"string"}},"required":["url"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"url":{"example":"https://1tn.pw/yJjWqM","type":"string"}},"type":"object"}}},"description":"The short link."},"400":{"description":"Not a link to this site."},"401":{"$ref":"#/components/responses/Unauthorized"},"502":{"description":"The shortener couldn't be reached or refused it."}},"security":[{"clerkSession":[]}],"summary":"A short link for a TrackLin.es link","tags":["Tickets"]}},"/sprints/{sprintId}":{"get":{"description":"For a closed sprint, tickets are the ones it finished (closing moves the rest to the next sprint) and burn.total is what it held when it closed; sprints closed before scope was recorded fall back to what they finished and carried_over is null. burn.ends_at is the close time. For an open sprint, everything reflects now.","operationId":"getSprint","parameters":[{"in":"path","name":"sprintId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Sprint"},{"properties":{"burn":{"$ref":"#/components/schemas/Velocity/properties/current"},"carried_over":{"description":"Tickets moved on to the next sprint when it closed.","type":["integer","null"]},"tickets":{"items":{"$ref":"#/components/schemas/Ticket"},"type":"array"},"unit":{"enum":["points","tickets"],"type":"string"}},"type":"object"}]}}},"description":"The sprint."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"One sprint, read-only, with its burn data and tickets","tags":["Sprints"]},"patch":{"operationId":"updateSprintLength","parameters":[{"in":"path","name":"sprintId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"description":"Send length_days and/or goal.","properties":{"goal":{"description":"\"\" clears it.","maxLength":200,"type":"string"},"length_days":{"maximum":365,"minimum":1,"type":"integer"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sprint"}}},"description":"Updated sprint."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin or the linked team's leader."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Change the length of an open or scheduled sprint","tags":["Sprints"]}},"/sprints/{sprintId}/close":{"post":{"description":"With no body, the sprint just closes: unfinished tickets stay on the board, out of any sprint, until the next POST /boards/{id}/sprints, which takes them in along with a waiting planned sprint (and its goal). Give next_length_days and/or next_starts_at to open the next sprint at once instead, as auto-close does when a sprint runs out: unfinished tickets carry over, and a planned sprint (refinement) within capacity or approved becomes the next sprint (its length unless next_length_days is given, and its tickets, which join the first column). A planned sprint over capacity without approval isn't promoted and stays planned.","operationId":"closeSprint","parameters":[{"in":"path","name":"sprintId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"next_length_days":{"description":"Defaults to the current sprint's length.","maximum":365,"minimum":1,"type":"integer"},"next_starts_at":{"description":"Defaults to now; a future value schedules the next sprint.","format":"date-time","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sprint"}}},"description":"The next sprint, started or scheduled (when asked for)."},"204":{"description":"Closed; no next sprint started."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Requires a signed-in organization admin or the linked team's leader."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Close a sprint; optionally start or schedule the next one","tags":["Sprints"]}},"/subscription":{"get":{"description":"The plan belongs to the organization; everyone in it sees it, and its admins buy and manage it.","operationId":"getSubscription","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"billed":{"description":"Paid through a Stripe subscription, so the billing portal can manage it. false for free plans and for plans granted by hand; the portal answers 409 for those.","type":"boolean"},"can_manage":{"description":"The caller is an org admin","so may upgrade or manage the plan.":null,"type":"boolean"},"member_limit":{"description":"People allowed in the organization (TL-418): 20 on Free and Pro, -1 (unlimited) on Enterprise. AI agents do not count.","type":"integer"},"paid":{"type":"boolean"},"plan":{"description":"Free: 1 project. Pro: 5. Enterprise: unlimited.","enum":["free","pro","enterprise"],"type":"string"},"project_limit":{"description":"-1 means unlimited.","type":"integer"}},"required":["plan","paid","project_limit"],"type":"object"}}},"description":"Subscription status."},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"description":"Billing status unavailable."}},"summary":"Get the active organization's plan and project limit","tags":["Billing"]}},"/subscription/checkout":{"post":{"description":"For an organization that isn't paying yet. One already on a paid plan changes plan instead (POST /subscription/plan).","operationId":"createCheckoutSession","requestBody":{"content":{"application/json":{"schema":{"properties":{"plan":{"default":"pro","enum":["pro","enterprise"],"type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/URLResponse"}}},"description":"Stripe Checkout URL."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Not a signed-in admin of the active organization."},"409":{"description":"The organization is already subscribed."},"502":{"description":"Stripe is unavailable."},"503":{"description":"Billing is not configured or unavailable."}},"summary":"Create a Stripe checkout session for the active organization's plan","tags":["Billing"]}},"/subscription/plan":{"post":{"description":"Moves the existing Stripe subscription to the other plan's price; Stripe prorates the difference. Org admins only.","operationId":"changePlan","requestBody":{"content":{"application/json":{"schema":{"properties":{"plan":{"enum":["pro","enterprise"],"type":"string"}},"required":["plan"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Changed; the new project limit applies straight away."},"400":{"description":"Unknown plan."},"403":{"description":"Not a signed-in admin of the active organization."},"409":{"description":"No subscription to change (check out instead)","or the plan was granted by hand.":null},"502":{"description":"Stripe is unavailable."},"503":{"description":"Billing (or that plan) isn't configured."}},"summary":"Change the organization's paid plan (Pro ↔ Enterprise), prorated","tags":["Billing"]}},"/subscription/portal":{"post":{"operationId":"createBillingPortalSession","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/URLResponse"}}},"description":"Stripe Billing Portal URL."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Not a signed-in admin of the active organization."},"409":{"description":"The organization has no Stripe subscription (free","or granted by hand).":null},"502":{"description":"Stripe is unavailable."},"503":{"description":"Billing is not configured or unavailable."}},"summary":"Create a Stripe billing portal session for the active organization","tags":["Billing"]}},"/teams":{"get":{"operationId":"listTeams","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Team"},"type":"array"}}},"description":"Teams"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"No active organization."}},"summary":"List teams in the active organization","tags":["Teams"]},"post":{"operationId":"createTeam","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}},"description":"Team created"},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Requires a signed-in organization admin."}},"summary":"Create a team","tags":["Teams"]}},"/teams/{teamId}":{"delete":{"operationId":"deleteTeam","responses":{"204":{"description":"Team and its memberships deleted."},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete a team","tags":["Teams"]},"parameters":[{"in":"path","name":"teamId","required":true,"schema":{"format":"uuid","type":"string"}}],"patch":{"operationId":"updateTeam","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamInput"}}},"required":true},"responses":{"204":{"description":"Team updated."},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Update a team","tags":["Teams"]}},"/teams/{teamId}/leaders/{userId}":{"delete":{"operationId":"removeTeamLeader","responses":{"204":{"description":"No longer a leader."},"403":{"description":"Not a signed-in org admin."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The user is not on the team."}},"summary":"Stop a member leading the team (they stay on it)","tags":["Teams"]},"parameters":[{"in":"path","name":"teamId","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"put":{"description":"Org admins only, signed in with a Clerk session. A team can have several leaders; leaders must be on the team, and removing them from the team ends their leadership.","operationId":"addTeamLeader","responses":{"204":{"description":"They now lead the team."},"403":{"description":"Not a signed-in org admin."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The user is not on the team."}},"summary":"Make a team member a leader","tags":["Teams"]}},"/teams/{teamId}/members":{"get":{"operationId":"listTeamMembers","parameters":[{"in":"path","name":"teamId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TeamMember"},"type":"array"}}},"description":"Team members"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"List team members and which of them lead the team","tags":["Teams"]}},"/teams/{teamId}/members/{userId}":{"delete":{"operationId":"removeTeamMember","responses":{"204":{"description":"Membership removed."},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Remove a member from a team","tags":["Teams"]},"parameters":[{"in":"path","name":"teamId","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"put":{"operationId":"addTeamMember","responses":{"204":{"description":"Membership recorded."},"403":{"description":"Requires a signed-in organization admin."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"User is not an active member of the organization."},"502":{"description":"Could not verify membership with the identity provider."}},"summary":"Add an active organization member to a team","tags":["Teams"]}},"/ticket-keys/{key}":{"get":{"description":"MCP tools also accept keys wherever they take a ticket_id, parent_id or ticket_ids.","operationId":"getTicketByKey","parameters":[{"in":"path","name":"key","required":true,"schema":{"example":"TL-123","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketDetail"}}},"description":"The ticket with where it lives."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"A ticket by its key (e.g. TL-123, case-insensitive), same as GET /tickets/{ticketId}","tags":["Tickets"]}},"/tickets/{ticketId}":{"delete":{"operationId":"deleteTicket","responses":{"204":{"description":"Ticket deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete a ticket","tags":["Tickets"]},"get":{"description":"Use `board_id` to look up the Done column before closing a ticket.","operationId":"getTicket","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketDetail"}}},"description":"Ticket details."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Get a ticket and its board context","tags":["Tickets"]},"parameters":[{"$ref":"#/components/parameters/TicketId"}],"patch":{"description":"`title` is required; empty `type` and `priority` retain their current values.","operationId":"updateTicket","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketInput"}}},"required":true},"responses":{"204":{"description":"Ticket updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Update a ticket","tags":["Tickets"]}},"/tickets/{ticketId}/assignee":{"put":{"description":"Set `assignee` to an active member of the caller's organization or an active AI key name; set it to null to unassign. Server/service keys cannot be assignees.","operationId":"assignTicket","parameters":[{"$ref":"#/components/parameters/TicketId"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"assignee":{"type":["string","null"]}},"required":["assignee"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}},"description":"Updated ticket."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"description":"Could not verify organization membership with Clerk."}},"summary":"Assign or unassign a ticket","tags":["Tickets"]}},"/tickets/{ticketId}/attachments":{"get":{"operationId":"listTicketAttachments","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Attachment"},"type":"array"}}},"description":"Attachment records."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"List ticket attachments","tags":["Attachments"]},"parameters":[{"$ref":"#/components/parameters/TicketId"}],"post":{"description":"The file must already be uploaded; this endpoint stores its metadata and does not upload bytes.","operationId":"recordTicketAttachment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Attachment"}}},"description":"Attachment recorded."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Ticket is in its Done column."}},"summary":"Record an uploaded UploadThing file on a ticket","tags":["Attachments"]}},"/tickets/{ticketId}/backlog":{"post":{"description":"Agents cannot move tickets awaiting human review into the backlog.","operationId":"sendTicketToBacklog","parameters":[{"$ref":"#/components/parameters/TicketId"}],"responses":{"200":{"description":"Moved, with its open sub-tickets on the board: {also_moved: [{id, key, title}]}. If one can't go, nothing moves and the error names it."},"204":{"description":"Ticket moved to backlog."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"An agent attempted to bypass a pending human review."}},"summary":"Move a ticket into its project's backlog","tags":["Tickets"]}},"/tickets/{ticketId}/blocked-by":{"put":{"description":"Use an empty `ticket_ids` array to clear dependencies. Dependencies must be in the same project and cannot form a cycle. A done ticket can keep or drop its blockers but not gain new ones (409).","operationId":"setTicketDependencies","parameters":[{"$ref":"#/components/parameters/TicketId"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"ticket_ids":{"items":{"format":"uuid","type":"string"},"type":"array"}},"required":["ticket_ids"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Dependencies replaced."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The ticket is done and the list adds a blocker."}},"summary":"Replace the tickets blocking this ticket","tags":["Tickets"]}},"/tickets/{ticketId}/bugfixes":{"get":{"description":"The linked crash report's live details, fetched server-side with the org's Bugfixes connection and cached for a minute. regressed is true when the report is open and was seen after the ticket was done. When the org hasn't connected Bugfixes (reason not_connected) or Bugfixes can't be reached (unavailable), available is false but the link is still returned.","operationId":"getTicketBugfixes","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"available":{"type":"boolean"},"project":{"type":"string"},"reason":{"enum":["not_connected","unavailable"],"type":"string"},"regressed":{"type":"boolean"},"report":{"properties":{"file":{"type":"string"},"last_seen":{"format":"date-time","type":"string"},"levels":{"items":{"type":"string"},"type":"array"},"line":{"type":"integer"},"occurrences":{"type":"integer"},"status":{"enum":["open","resolved","ignored"],"type":"string"},"title":{"type":"string"}},"type":"object"},"report_id":{"type":"string"},"source":{"enum":["bugfixes"],"type":"string"},"url":{"type":"string"}},"type":"object"}}},"description":"The panel."},"404":{"description":"The ticket didn't come from Bugfixes."}},"summary":"The Bugfixes panel for a ticket Bugfixes created","tags":["Tickets"]},"parameters":[{"$ref":"#/components/parameters/TicketId"}]},"/tickets/{ticketId}/claim":{"post":{"description":"AI keys and signed-in people may claim tickets. Server/service keys cannot claim work.","operationId":"claimTicket","parameters":[{"$ref":"#/components/parameters/TicketId"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}},"description":"Claimed ticket."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Server/service API keys cannot claim tickets."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Ticket is already assigned or blocked by another ticket."}},"summary":"Assign an unassigned ticket to the caller","tags":["Tickets"]}},"/tickets/{ticketId}/comments":{"get":{"operationId":"listTicketComments","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Comment"},"type":"array"}}},"description":"Comments in chronological order."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"List a ticket's conversation","tags":["Comments"]},"parameters":[{"$ref":"#/components/parameters/TicketId"}],"post":{"description":"Adds a top-level comment to the ticket conversation. Use this for progress reports or follow-up findings; it does not replace the ticket description.","operationId":"appendTicketComment","requestBody":{"content":{"application/json":{"example":{"body":"Fixed in the backend. The request now succeeds with the production origin.\n"},"schema":{"$ref":"#/components/schemas/CreateComment"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comment"}}},"description":"Comment appended."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}},"summary":"Append an update to a ticket","tags":["Comments"]}},"/tickets/{ticketId}/duplicate":{"post":{"description":"Marks ticketId as a duplicate of another ticket in the same project, closes it, and links it from the original. Optionally copies labels and comments to the original. Duplicate tickets do not count toward sprint velocity or carry-over. MCP tool mark_duplicate accepts ticket keys or ids.","operationId":"markTicketDuplicate","parameters":[{"$ref":"#/components/parameters/TicketId"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"copy_comments":{"default":false,"type":"boolean"},"copy_labels":{"default":false,"type":"boolean"},"duplicate_of":{"description":"Original ticket UUID (MCP also accepts a ticket key).","type":"string"}},"required":["duplicate_of"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Ticket marked as a duplicate."},"400":{"and the original cannot itself be a duplicate.":null,"description":"Tickets must be different","in the same project":null},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The ticket is already marked as a duplicate."}},"summary":"Close a ticket as a duplicate","tags":["Tickets"]}},"/tickets/{ticketId}/external":{"parameters":[{"$ref":"#/components/parameters/TicketId"}],"put":{"description":"The same object can be sent as `external` when creating a backlog ticket with an API key. A null body clears it.","operationId":"setTicketExternal","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalRef"}}},"required":true},"responses":{"204":{"description":"Linked."},"400":{"description":"Invalid reference (source must be bugfixes; project and id required; url http(s))."},"403":{"description":"Only integrations (API keys) set references."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Link a ticket to where an integration made it (API keys only), e.g. a Bugfixes report","tags":["Tickets"]}},"/tickets/{ticketId}/group":{"get":{"description":"When a ticket leaves its board (to the backlog, or another board), its sub-tickets on that board that aren't done, at any depth, go with it. Finished ones stay as the board's history; ones on other boards belong to those teams. Empty for a backlog ticket.","operationId":"getTicketGroup","parameters":[{"in":"path","name":"ticketId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"id":{"format":"uuid","type":"string"},"key":{"type":["string","null"]},"title":{"type":"string"}},"type":"object"},"type":"array"}}},"description":"Sub-tickets, parents first."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Open sub-tickets that would move with a ticket leaving its board","tags":["Tickets"]}},"/tickets/{ticketId}/history":{"get":{"description":"Each stage the ticket entered, or skipped on the way to a later one: the backlog, a board column (named as it was then), and resolved (done, wont_do or duplicate). Created straight on a board, its backlog and any earlier columns are skipped; moving forward skips the columns jumped over. Append-only and recorded with each change. Same access as reading the ticket; resolved tickets keep their history. History starts from when it was introduced (October 2026).","operationId":"getTicketHistory","parameters":[{"in":"path","name":"ticketId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"actor":{"description":"A user id or agent name; absent when the system did it.","type":"string"},"at":{"format":"date-time","type":"string"},"board_id":{"format":"uuid","type":"string"},"board_name":{"type":"string"},"column_id":{"format":"uuid","type":"string"},"column_name":{"type":"string"},"done":{"description":"The board's last column.","type":"boolean"},"outcome":{"enum":["entered","skipped"],"type":"string"},"resolution":{"enum":["done","wont_do","duplicate"],"type":"string"},"stage":{"enum":["backlog","column","resolved"],"type":"string"},"stage_order":{"description":"backlog 0, a column its position + 1, resolved last","type":"integer"}},"required":["at","stage","outcome","stage_order","done"],"type":"object"},"type":"array"}}},"description":"Entries, oldest first."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"A ticket's lifecycle history, oldest first","tags":["Tickets"]}},"/tickets/{ticketId}/labels":{"put":{"operationId":"setTicketLabels","parameters":[{"$ref":"#/components/parameters/TicketId"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"labels":{"$ref":"#/components/schemas/Labels"}},"required":["labels"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"labels":{"items":{"type":"string"},"type":"array"}},"type":"object"}}},"description":"Normalised labels as stored."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Replace a ticket's labels","tags":["Tickets"]}},"/tickets/{ticketId}/move":{"post":{"description":"To close a ticket, first get its board, then move it to the column with the greatest `position` (the board's Done column). `position` may be zero. Moving a ticket into a sprint (from the backlog, another board or outside the sprint) on the sprint's last day answers 409; moves inside the sprint, to Done, or out of it are allowed.","operationId":"moveTicket","parameters":[{"$ref":"#/components/parameters/TicketId"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveTicket"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"also_moved":{"items":{"properties":{"id":{"type":"string"},"key":{"type":["string","null"]},"title":{"type":"string"}},"type":"object"},"type":"array"},"message":{"type":"string"},"needs_review":{"type":"boolean"}},"type":"object"}}},"description":"Either an agent (API key) moved it into Done on a board with agent review: it waits in the column before Done for a person's review (see /tickets/{ticketId}/review). Or it moved to another board and its open sub-tickets on the old board came too (also_moved); if one can't, nothing moves and the error names it."},"204":{"description":"Ticket moved."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The target board's sprint ends today, so its scope is locked."},"500":{"$ref":"#/components/responses/InternalError"}},"summary":"Move a ticket to another column","tags":["Tickets"]}},"/tickets/{ticketId}/org":{"get":{"description":"For a ticket link opened while another organization is active, so the app can offer to switch. Answers only a signed-in person who belongs to the ticket's organization; anyone else (and API keys) gets 404, as if it didn't exist.","operationId":"ticketOrganization","parameters":[{"$ref":"#/components/parameters/TicketId"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"org_id":{"type":"string"}},"type":"object"}}},"description":"The ticket's organization."},"404":{"$ref":"#/components/responses/NotFound"}},"security":[{"clerkSession":[]}],"summary":"Which of my organizations a ticket is in","tags":["Tickets"]}},"/tickets/{ticketId}/planned-sprint":{"parameters":[{"$ref":"#/components/parameters/TicketId"}],"put":{"description":"The ticket stays in the backlog until the planned sprint starts. Without an estimate it keeps one already on the same scale. planned_sprint_id null takes it out of the plan (and clears the estimate). Going over capacity is allowed; the sprint then needs approval before it can start.","operationId":"planTicket","requestBody":{"content":{"application/json":{"schema":{"properties":{"estimate":{"type":"string"},"planned_sprint_id":{"format":"uuid","type":["string","null"]}},"type":"object"}}},"required":true},"responses":{"204":{"description":"Planned."},"400":{"description":"Estimate isn't on the board's scale."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The ticket is on a board","not in the backlog.":null}},"summary":"Plan a backlog ticket into a planned sprint, sized on that board's scale","tags":["Tickets"]}},"/tickets/{ticketId}/prs":{"get":{"description":"A PR in a repo linked to the ticket's project is linked when its branch, title, description or commit messages mention the ticket's key (TL-123; only the project's own prefix counts). Kept up to date by the provider webhooks. When a linked PR is merged, the ticket moves to its board's Done column (or is resolved in the backlog) with a \"Closed by\" comment.","operationId":"getTicketPRs","responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"author":{"type":"string"},"branch":{"type":"string"},"number":{"type":"integer"},"provider":{"enum":["github","bitbucket"],"type":"string"},"repo":{"type":"string"},"state":{"enum":["open","draft","merged","closed"],"type":"string"},"title":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"}}},"description":"Linked PRs."}},"summary":"Pull requests linked to the ticket, newest first","tags":["Tickets"]},"parameters":[{"$ref":"#/components/parameters/TicketId"}]},"/tickets/{ticketId}/release":{"post":{"description":"AI keys and signed-in people may release ticket assignments. Server/service keys cannot.","operationId":"releaseTicket","parameters":[{"$ref":"#/components/parameters/TicketId"}],"responses":{"204":{"description":"Assignment cleared."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Server/service API keys cannot release ticket assignments."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Ticket is not assigned to the caller."}},"summary":"Clear the caller's assignment from a ticket","tags":["Tickets"]}},"/tickets/{ticketId}/resolve":{"post":{"description":"Marks a backlog ticket resolved without putting it on a board. A resolved ticket counts as done everywhere (blockers, sub-ticket progress, the done flag), leaves backlog pages and open-ticket lists, keeps its links and stays readable by id. Resolving again is a no-op; moving it onto a board reopens it. If it was the last unfinished sub-ticket of its parent, the parent is completed too (resolved if it's in the backlog, moved to its board's Done column if it's on a board), and so on up the chain. A ticket on a board is moved to the board's Done column with the resolution (moving it out again reopens it), for people and service keys (e.g. Bugfixes when a report is resolved or ignored). AI keys finish board tickets by moving them instead (409), so a board's agent review still applies. MCP tool resolve_ticket. With resolution wont_do it's closed as decided against (the ticket's resolution says so; the reason, if given, is added as a comment). No body resolves it as done. Agents cannot resolve tickets that have entered human review.","operationId":"resolveTicket","parameters":[{"$ref":"#/components/parameters/TicketId"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"reason":{"maxLength":10000,"type":"string"},"resolution":{"default":"done","enum":["done","wont_do"],"type":"string"}},"type":"object"}}},"required":false},"responses":{"204":{"description":"Resolved."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The ticket is on a board or an agent is attempting to bypass human review."}},"summary":"Close a ticket as done or won't do","tags":["Tickets"]}},"/tickets/{ticketId}/review":{"parameters":[{"$ref":"#/components/parameters/TicketId"}],"post":{"description":"People only. The comment, if given, is added to the ticket by the reviewer.","operationId":"reviewTicket","requestBody":{"content":{"application/json":{"schema":{"properties":{"approve":{"type":"boolean"},"comment":{"maxLength":10000,"type":"string"}},"required":["approve"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Reviewed."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Agents (API keys) can't review."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The ticket isn't waiting for review."}},"summary":"Approve an agent's finished ticket (to Done) or send it back (to the first column)","tags":["Tickets"]}},"/webhooks/bitbucket/{link}":{"parameters":[{"description":"The linked repo the webhook was made for.","in":"path","name":"link","required":true,"schema":{"format":"uuid","type":"string"}}],"post":{"description":"TrackLin.es creates this webhook itself when an org admin links a Bitbucket repo (POST /projects/{id}/repos), with its own random secret (sealed, never shown) and this URL naming the link; unlinking removes it. Deliveries are verified with that link's secret (X-Hub-Signature) and only affect that link's org and repo: pullrequest:* events (X-Event-Key) link PRs to the tickets they mention; MERGED closes them, once.","operationId":"bitbucketWebhook","responses":{"204":{"description":"Accepted."},"400":{"description":"Bad signature."},"404":{"description":"Unknown or unlinked repo."}},"security":[],"summary":"Bitbucket repository webhook, one per linked repo","tags":["Organizations"]}},"/webhooks/clerk":{"post":{"description":"Authenticates the raw payload with the Svix headers and CLERK_WEBHOOK_SECRET (timestamps more than 5 minutes off are rejected). organizationMembership.created completes the new member's pending TrackLin.es invitations to that org (team and project access included) for their verified emails (TL-518); organizationMembership.deleted removes the user's admin and team rows in that org and revokes their API keys for it; user.deleted does the same across every org; organization.deleted removes the org's admins and teams and revokes its API keys (projects, boards and tickets are kept). Other event types are acknowledged and ignored. Safe to replay.","operationId":"clerkWebhook","parameters":[{"in":"header","name":"svix-id","required":true,"schema":{"type":"string"}},{"description":"Unix seconds.","in":"header","name":"svix-timestamp","required":true,"schema":{"type":"string"}},{"description":"Space-separated \"v1,\u003cbase64\u003e\" signatures.","in":"header","name":"svix-signature","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object"}}},"required":true},"responses":{"204":{"description":"Event applied or ignored."},"400":{"description":"Invalid body or signature."},"503":{"description":"CLERK_WEBHOOK_SECRET is not set","or the database is unavailable (Svix retries).":null}},"security":[],"summary":"Receive a Clerk webhook event (delivered by Svix)","tags":["Organizations"]}},"/webhooks/github":{"post":{"description":"pull_request events link PRs to the tickets whose keys they (or their commits) mention (in repos linked to a project of an org connected through that installation); merging closes them, once. An uninstalled app disconnects the org. A 5xx makes GitHub redeliver.","operationId":"githubWebhook","responses":{"204":{"description":"Accepted."},"400":{"description":"Bad signature."},"503":{"description":"Not configured."}},"security":[],"summary":"GitHub App webhook (X-Hub-Signature-256 with GITHUB_WEBHOOK_SECRET)","tags":["Organizations"]}},"/webhooks/google-chat":{"post":{"description":"Signed by Google for this endpoint (Authorization Bearer ID token); anything else is refused.","operationId":"googleChatEvents","responses":{"200":{"description":"The app's reply."},"401":{"description":"Not a request from Google Chat for this app."},"503":{"description":"The Chat app isn't set up on this server."}},"security":[],"summary":"Requests from the TrackLin.es Google Chat app (Google only)","tags":["Notifications"]}},"/webhooks/stripe":{"post":{"description":"Authenticates the raw payload with Stripe-Signature. The handler is mounted at this path both under /api and at the API root.","operationId":"stripeWebhook","parameters":[{"in":"header","name":"Stripe-Signature","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object"}}},"required":true},"responses":{"200":{"description":"Event acknowledged."},"400":{"description":"Invalid body or signature."},"502":{"description":"Stripe is unavailable while processing the event."},"503":{"description":"Billing is unavailable."}},"security":[],"servers":[{"url":"https://tracklin.es/api"},{"url":"http://localhost:8080/api"},{"url":"http://localhost:8080"}],"summary":"Receive a Stripe webhook event","tags":["Billing"]}}},"security":[{"bearerAuth":[]}],"servers":[{"description":"Production backend API","url":"https://api.tracklin.es/api"},{"description":"Local API","url":"http://localhost:8080/api"}],"tags":[{"description":"The public contact form; messages go to the TrackLin.es team.","name":"Contact"},{"description":"Create, list, inspect, update, and delete projects.","name":"Projects"},{"description":"Inspect membership in the active Clerk organization.","name":"Organizations"},{"description":"Manage role-free teams, team membership, and project links inside the active organization.","name":"Teams"},{"description":"Create, inspect, move, and close tickets.","name":"Tickets"},{"description":"Append updates to a ticket conversation.","name":"Comments"},{"description":"Team boards and columns.","name":"Boards"},{"description":"Board workflow columns.","name":"Columns"},{"description":"UploadThing attachment metadata.","name":"Attachments"},{"description":"Board sprints.","name":"Sprints"},{"description":"Hosted standups and refinements that people join and follow.","name":"Live"},{"description":"Project roadmaps and linked ticket progress.","name":"Roadmaps"},{"description":"Manage AI agent and server/service API keys with a Clerk session.","name":"API keys"},{"description":"Stripe subscription and webhook operations.","name":"Billing"},{"description":"Health check.","name":"System"}]}