{"name":"Telos API","version":"v1","description":"Outcomes-anchored project management API. The chain: Vision → Objective (anchored to a Metric) ← Opportunity (M:N) → Task. Metrics inform Objectives; Opportunities tie to one or more Objectives with an optional suspected delta. Each entity has a materialised visionPath tracing it to the root vision. Opportunity owns the full lifecycle, Tasks carry the engineering spec, and rooms/messages are the collaboration surface. Every resource below is generated from the same action registry that serves the routes, so this schema cannot drift from the API.","baseUrl":"/api/v1","authentication":{"method":"Bearer token","header":"Authorization: Bearer telos_live_<key>","note":"API keys carry a subset of their user's capabilities (scopes). Each operation lists the capability it requires; capability null means self-scoped (you only ever touch your own rows). Financial fields (estimatedCost, salary) are additionally ACL-stripped unless the key holds cost:read / compensation:read."},"resources":[{"name":"customers","description":"List customers with cursor pagination. Optionally filter by status.","operations":[{"name":"list","rest":"GET /api/v1/customers","summary":"List customers","capability":"customer:read"},{"name":"get","rest":"GET /api/v1/customers/:id","summary":"Get a customer","capability":"customer:read","description":"Get a customer by UUID or by ref (e.g. CUS-12)."},{"name":"create","rest":"POST /api/v1/customers","summary":"Create a customer","capability":"customer:create","description":"Create a customer account (name plus optional profile, owner, ARR, and lifecycle status; status defaults to prospect)."},{"name":"update","rest":"PATCH /api/v1/customers/:id","summary":"Update a customer","capability":"customer:update","description":"Update a customer's profile, owner, ARR, health, or lifecycle status. Moving to churned/lost requires a reason (stored as an insight rooted at the customer); moving to onboarding activates the org's default onboarding template."},{"name":"delete","rest":"DELETE /api/v1/customers/:id","summary":"Delete a customer","capability":"customer:delete","description":"Hard-delete an account and its insights. Contacts, events, reviews, updates, and opportunity links go with it; linked opportunities survive."},{"name":"contacts","rest":"GET /api/v1/customers/:id/contacts","summary":"List a customer's contacts","capability":"customer:read","description":"List the named people on a customer account, primary contact first, then newest. Use a contact's id to attribute an insight to the person who said it."},{"name":"create-contact","rest":"POST /api/v1/customers/:id/contacts","summary":"Add a contact to a customer","capability":"customer:create","description":"Add a named person to a customer account (name plus optional title, role, email, phone, and notes). Marking the contact primary demotes the account's existing primary."},{"name":"update-contact","rest":"PATCH /api/v1/customers/:id/contacts/:contactId","summary":"Update a customer contact","capability":"customer:update","description":"Update a contact's details. Promoting a contact to primary demotes the account's existing primary."},{"name":"delete-contact","rest":"DELETE /api/v1/customers/:id/contacts/:contactId","summary":"Delete a customer contact","capability":"customer:delete","description":"Remove a contact from a customer account. Insights attributed to the contact survive and fall back to the customer as their demand root."},{"name":"opportunities","rest":"GET /api/v1/customers/:id/opportunities","summary":"List a customer's linked opportunities","capability":"customer:read","description":"The opportunities tied to this account, most-active first. `stepInstanceId` is the onboarding milestone the link is pinned to (null when unpinned)."},{"name":"link-opportunity","rest":"POST /api/v1/customers/:id/opportunities","summary":"Link an opportunity to a customer","capability":"customer:update","description":"Tie an opportunity to an account and set where it sits in the account's onboarding: the call is idempotent and always leaves the link pinned to `stepInstanceId`, so omitting it (or passing null) unpins. Re-call to re-pin."},{"name":"unlink-opportunity","rest":"DELETE /api/v1/customers/:id/opportunities/:opportunityId","summary":"Unlink an opportunity from a customer","capability":"customer:delete","description":"Remove the tie between an account and an opportunity. The opportunity itself survives."},{"name":"updates","rest":"GET /api/v1/customers/:id/updates","summary":"List a customer's update feed","capability":"customer:read","description":"The account's update stream, newest first: notes posted on the customer plus progress posted on any opportunity the account reaches (linked or through its insights)."},{"name":"post-update","rest":"POST /api/v1/customers/:id/updates","summary":"Post a customer update","capability":"customer:update","description":"Post an account note to the customer's feed, authored by the key's user. It notifies the account's subscribers and mirrors into the org's customer-updates channel. Opportunity progress goes through post_opportunity_update instead."},{"name":"commitments","rest":"GET /api/v1/customers/commitments","summary":"List every dated client promise","capability":"customer:read","description":"Every dated promise made to a client across the org (an open insight carrying a deadline), soonest first so overdue leads, resolved to the work delivering it. `atRisk` flags a near deadline with nothing in flight."},{"name":"arr","rest":"GET /api/v1/customers/:id/arr","summary":"List a customer's ARR history","capability":"customer:read","description":"The account's ARR level series oldest first: each point is 'ARR was this much from this date'. Deltas are derived between consecutive points, never stored."},{"name":"add-arr-point","rest":"POST /api/v1/customers/:id/arr","summary":"Record an ARR level for a customer","capability":"customer:create","description":"Record what the account's ARR was from a given date. A point dated at or after the latest recorded level also updates the account's current ARR; an earlier one only backfills history. Future dates are rejected: this records what happened, not forecasts."},{"name":"delete-arr-point","rest":"DELETE /api/v1/customers/:id/arr/:arrPointId","summary":"Delete an ARR history point","capability":"customer:delete","description":"Remove one point from the account's ARR series (typo repair). The account's current ARR is not recomputed."},{"name":"arr-summary","rest":"GET /api/v1/customers/arr-summary","summary":"Org-wide ARR roll-up and movement","capability":"customer:read","description":"The portfolio view of revenue in one call: `stats` totals ARR and account count per lifecycle position (live, pilot, onboarding, prospect), and `waterfall` gives ARR gained/lost/net per month over the requested window, derived from the same level series."},{"name":"reviews","rest":"GET /api/v1/customers/:id/reviews","summary":"List a customer's stored reviews","capability":"customer:read","description":"The account's pre-meeting reviews, newest first: what the client had going on in each stored date window. Summaries only; read one with get_customer_review."},{"name":"review","rest":"GET /api/v1/customers/:id/reviews/:reviewId","summary":"Get a stored customer review","capability":"customer:read","description":"One stored review in full: the digest (new asks, shipped work, updates, overdue commitments, delayed work, stuck asks) plus the generated narrative when there is one."},{"name":"generate-review","rest":"POST /api/v1/customers/:id/reviews","summary":"Generate a customer review","capability":"customer:update","description":"Build and store a review of everything that happened on the account between `from` and now: asks recorded, work shipped, updates posted, commitments missed. Writes an LLM narrative when the org has one configured; the digest stands on its own without it."},{"name":"onboarding","rest":"GET /api/v1/customers/:id/onboarding","summary":"Get a customer's onboarding state","capability":"customer:read","description":"The account's onboarding in one call: every milestone in order with the active one flagged, the ARR tranche each unlocks, and the work sitting on it (opportunities pinned to the milestone plus bespoke tasks). `hasOnboarding` is false when the account was never started."},{"name":"set-onboarding","rest":"POST /api/v1/customers/:id/onboarding","summary":"Start or swap a customer's onboarding template","capability":"customer:update","description":"Put the account on an onboarding template. Starting seeds the milestones and moves a prospect to onboarding; passing a different templateId on an account that already started swaps it. Omitting templateId uses the org's default customer onboarding template."},{"name":"advance-onboarding","rest":"POST /api/v1/customers/:id/onboarding/advance","summary":"Move a customer to another onboarding milestone","capability":"customer:update","description":"Move the account to a milestone, naming it either by the account's own step instance (stepInstanceId) or by the template's template step (stepId). The move lands in the account's update feed so every stage change is on the record. This does not end onboarding: use complete_customer_onboarding for that."},{"name":"complete-onboarding","rest":"POST /api/v1/customers/:id/onboarding/complete","summary":"Complete a customer's onboarding and take the account live","capability":"customer:update","description":"End onboarding: lands the account on the template's Completed step, flips a prospect/onboarding/pilot account to live, stamps customerSince, and notifies subscribers. Never resurrects a churned or lost account."},{"name":"update-onboarding-step","rest":"PATCH /api/v1/customers/:id/onboarding/steps/:stepInstanceId","summary":"Update one of a customer's onboarding milestones","capability":"customer:update","description":"Set what a milestone owns: its DRI, the ARR tranche it unlocks on completion (null clears), and an existing task attached to it. Every field is optional; only what you pass is written. Attaching a task writes the task row, so passing taskId also requires task:update."},{"name":"add-onboarding-work","rest":"POST /api/v1/customers/:id/onboarding/steps/:stepInstanceId/work","summary":"Create work on a customer's onboarding milestone","capability":"customer:create","description":"Create a new work item as a go-live requirement on a milestone. kind 'task' is bespoke per-client work (data migration, training) and requires task:create; kind 'opportunity' is a product bet, created and pinned to the milestone, and requires opportunity:create. customer:create alone is not enough — the row lands in the work graph. Returns the new item's kind and id. To attach work that already exists, use update_customer_onboarding_step (tasks) or link_customer_opportunity."}]},{"name":"strategies","description":"List strategies with cursor pagination. Optionally filter by status (draft, active, archived).","operations":[{"name":"list","rest":"GET /api/v1/strategies","summary":"List strategies","capability":"strategy:read"},{"name":"get","rest":"GET /api/v1/strategies/:id","summary":"Get a strategy","capability":"strategy:read","description":"Get a strategy by UUID or by its per-org ref (STR-12)."},{"name":"create","rest":"POST /api/v1/strategies","summary":"Create a strategy","capability":"strategy:create","description":"Create a strategy (name plus a markdown content body). Starts as a draft."},{"name":"update","rest":"PATCH /api/v1/strategies/:id","summary":"Update a strategy","capability":"strategy:update","description":"Update a strategy's name or content. An optional changeNote labels the version this edit creates. Owner-only; ACL enforced by the service."},{"name":"publish","rest":"POST /api/v1/strategies/:id/publish","summary":"Publish a strategy","capability":"strategy:update","description":"Publish a draft strategy, making it the active version. Owner-only; ACL enforced by the service."},{"name":"archive","rest":"POST /api/v1/strategies/:id/archive","summary":"Archive a strategy","capability":"strategy:update","description":"Archive a strategy, retiring it from the active set. Owner-only; ACL enforced by the service."},{"name":"versions","rest":"GET /api/v1/strategies/:id/versions","summary":"List a strategy's version history","capability":"strategy:read","description":"Version snapshots of a strategy, newest first, with cursor pagination. Every content edit creates one."},{"name":"rename-version","rest":"POST /api/v1/strategies/:id/versions/:version/rename","summary":"Rename a strategy version","capability":"strategy:update","description":"Label one snapshot in a strategy's version history. An empty note clears the label, so the version falls back to 'v{n}'. Owner-only; ACL enforced by the service."}]},{"name":"visions","description":"List strategic visions with cursor pagination: the company vision first, then product visions. Optionally filter by kind (company, product).","operations":[{"name":"list","rest":"GET /api/v1/visions","summary":"List visions","capability":"vision:read"},{"name":"get","rest":"GET /api/v1/visions/:id","summary":"Get a vision","capability":"vision:read","description":"Get a vision by UUID or by its per-org ref (VIS-12)."},{"name":"create","rest":"POST /api/v1/visions","summary":"Create a vision","capability":"vision:create","description":"Create a vision: the company vision (kind 'company', one per org) or a product vision (kind 'product'). The narrative is the persuasive 2-5 year story, not a slogan."},{"name":"update","rest":"PATCH /api/v1/visions/:id","summary":"Update a vision","capability":"vision:update","description":"Update a vision's name or narrative. Edits are versioned."},{"name":"versions","rest":"GET /api/v1/visions/:id/versions","summary":"List a vision's version history","capability":"vision:read","description":"Version snapshots of a vision, newest first, with cursor pagination. Every name or narrative edit creates one."},{"name":"objectives","rest":"GET /api/v1/visions/:id/objectives","summary":"List objectives for a vision","capability":"vision:read","description":"The objectives anchored to this vision, with cursor pagination. Returns objective rows, so it needs objective:read as well as vision:read."},{"name":"children","rest":"GET /api/v1/visions/:id/children","summary":"Count a vision's children","capability":"vision:read","description":"Blast radius of a vision: how many objectives, opportunities and tasks hang off it. Objectives link by FK; opportunities and tasks by their visionPath prefix. 404 when the vision is not one of yours — zero children and no such vision are different answers."},{"name":"rename-version","rest":"POST /api/v1/visions/:id/versions/:version/rename","summary":"Rename a vision version","capability":"vision:update","description":"Label one snapshot in a vision's version history. An empty note clears the label, so the version falls back to 'v{n}'."}]},{"name":"metrics","description":"List metrics tracked by the organisation, with cursor pagination. Filter by steward (pass your own user id from whoami for 'my metrics').","operations":[{"name":"list","rest":"GET /api/v1/metrics","summary":"List metrics","capability":"metric:read"},{"name":"get","rest":"GET /api/v1/metrics/:id","summary":"Get a metric","capability":"metric:read","description":"Get a metric by UUID or by its per-org ref (MET-12)."},{"name":"create","rest":"POST /api/v1/metrics","summary":"Create a metric","capability":"metric:create","description":"Create a metric (name, unit, optional data source and steward)."},{"name":"update","rest":"PATCH /api/v1/metrics/:id","summary":"Update a metric","capability":"metric:update","description":"Update a metric's name, description, unit, data source, or steward."},{"name":"delete","rest":"DELETE /api/v1/metrics/:id","summary":"Delete a metric","capability":"metric:delete","description":"Permanently delete a metric."},{"name":"values","rest":"GET /api/v1/metrics/:id/values","summary":"List metric values","capability":"metric:read","description":"Recorded values for a metric, oldest first, with cursor pagination over the whole series. Optionally filter by date (since)."},{"name":"record-value","rest":"POST /api/v1/metrics/:id/values","summary":"Record a metric value","capability":"metric:update","description":"Record an observed value for a metric. recordedAt defaults to now; pass it to backfill history."},{"name":"list-builtins","rest":"GET /api/v1/metrics/builtins","summary":"List built-in metrics Telos can track","capability":"metric:read","description":"The catalog of metrics Telos computes from its own data (task throughput, lead/cycle time, time in status, cycle predictability), with whether each is already tracked in this workspace."},{"name":"track-builtin","rest":"POST /api/v1/metrics/track-builtin","summary":"Start tracking a built-in metric","capability":"metric:create","description":"Create a system-sourced metric from the built-in catalog. Telos backfills recent history and keeps it current nightly; its readings cannot be entered by hand. Some builtins take params: an optional category scope, or a required status group for time-in-status."},{"name":"opportunities","rest":"GET /api/v1/metrics/:id/opportunities","summary":"Get the opportunities betting on a metric","capability":"metric:read","description":"The complete set of opportunities that reach this metric through the objectives anchored to it, alphabetically by title. Not paginated: it is a roll-up of one metric's trace, ordered after the query, so there is no stable cursor to page it on. Use `objective list --metric-id` for the objectives themselves, and `list_opportunities` when you want a paged walk. 404 when the metric is not one of yours — an empty list means nobody is betting on it."},{"name":"dependents","rest":"GET /api/v1/metrics/:id/dependents","summary":"Count what depends on a metric","capability":"metric:read","description":"Blast radius before deleting or re-anchoring a metric: how many opportunities and objectives it carries, and how many of those opportunities are still live bets."}]},{"name":"objectives","description":"List strategic objectives with cursor pagination. Filter by vision, by the metric they are anchored to, by DRI (pass your own user id from whoami for 'my objectives'), or by status.","operations":[{"name":"list","rest":"GET /api/v1/objectives","summary":"List objectives","capability":"objective:read"},{"name":"get","rest":"GET /api/v1/objectives/:id","summary":"Get an objective","capability":"objective:read","description":"Get an objective by UUID or by its per-org ref (OBJ-12)."},{"name":"create","rest":"POST /api/v1/objectives","summary":"Create an objective","capability":"objective:create","description":"Create a strategic objective under a vision. Requires a DRI. The metric block (metricId, operator, target) is optional: goals with no clean measurement are created without one and can be anchored later."},{"name":"update","rest":"PATCH /api/v1/objectives/:id","summary":"Update an objective","capability":"objective:update","description":"Update an objective's title, description, deadline, DRI, or status."},{"name":"delete","rest":"DELETE /api/v1/objectives/:id","summary":"Delete an objective","capability":"objective:delete","description":"Permanently delete an objective."},{"name":"opportunities","rest":"GET /api/v1/objectives/:id/opportunities","summary":"Get the opportunities linked to an objective","capability":"objective:read","description":"The complete set of opportunities linked to this objective, with their link metadata, alphabetically by title. Not paginated: it is a roll-up of one objective's trace, ordered on a value derived after the query, so there is no stable cursor to page it on. Use `list_opportunities` for a paged walk of the org's opportunities."},{"name":"set-metric","rest":"POST /api/v1/objectives/:id/set-metric","summary":"Set an objective's metric","capability":"objective:update","description":"Replace which metric an objective points at, change its target shape (operator, target, start value), or pass metricId: null to clear the anchor entirely. An objective has at most one metric, and operator and target travel with it."},{"name":"alignment","rest":"GET /api/v1/objectives/alignment","summary":"Read the alignment grid","capability":"objective:read","description":"Every objective in the org as a column, plus the objectives and non-cancelled opportunities that claim to move them as rows. Each row carries `serves`: one entry per objective it advances, with kind 'primary' (this is what the row is really for) or 'supporting'. A primary objective→objective link is also the nesting parent. Not paginated: columns, rows and marks are one picture and a page cursor would draw a lying one; it is clamped instead. Use `list_objectives` for a paged walk."},{"name":"nesting","rest":"GET /api/v1/objectives/:id/nesting","summary":"Get an objective's parent and sub-objectives","capability":"objective:read","description":"The objective one level up (`parent`), the objectives one level down (`children`), and the supporting ties in both directions (`alsoServes`, `supportedBy`). Nesting is one level deep, so this is the whole tree around an objective, not a slice of it. Not paginated: it is bounded by the org's objective count and read as one shape."},{"name":"set-alignment-link","rest":"POST /api/v1/objectives/:id/set-alignment-link","summary":"Point an objective or opportunity at an objective","capability":"objective:update","description":"Set, change or clear one claim in the alignment grid. Pass kind 'primary' for 'this is what the source is really for', 'supporting' for a secondary contribution, or null to remove the link. An objective may sit under at most one primary parent, and nesting is one level deep: a parent may not itself be a sub-objective."},{"name":"claims","rest":"GET /api/v1/objectives/:id/claims","summary":"Get the customer claims on an objective","capability":"objective:read","description":"Every open customer insight tied to this objective, each with its estimated delta, committed date and the work items that satisfy it (conditionStatus: no_work, in_progress, delivered, broken). Not paginated: the claim set is assembled from three queries and ordered after the fact, so there is no stable cursor to page it on. Use `list_insights --objective-id` for a paged walk of the same insights."}]},{"name":"opportunities","description":"List opportunities with cursor pagination. Optionally filter by lifecycle state or team.","operations":[{"name":"list","rest":"GET /api/v1/opportunities","summary":"List opportunities","capability":"opportunity:read"},{"name":"get","rest":"GET /api/v1/opportunities/:id","summary":"Get an opportunity","capability":"opportunity:read","description":"Get a specific opportunity by id."},{"name":"create","rest":"POST /api/v1/opportunities","summary":"Create an opportunity","capability":"opportunity:create","description":"Create a work opportunity. The description seeds the PRD (or a PRD template does, when omitted). Optionally set lead, team, template, category, initial objective links, tags, and values for active scoring criteria. A patternPromotion additionally requires insight:update because it actions the pattern and links evidence atomically. Financial fields are ACL-gated."},{"name":"update","rest":"PATCH /api/v1/opportunities/:id","summary":"Update an opportunity","capability":"opportunity:update","description":"Update an opportunity's title, lead, team, PRD body, or tag set. Editing the PRD clears any cached AI synthesis. Lifecycle moves go through set_opportunity_state, write_template_handoff (template steps) or reject_opportunity, not here."},{"name":"delete","rest":"DELETE /api/v1/opportunities/:id","summary":"Delete an opportunity","capability":"opportunity:delete","description":"Permanently delete an opportunity and its dependency edges. Prefer reject_opportunity to record a won't-build decision; delete is for mistakes, not outcomes."},{"name":"tasks","rest":"GET /api/v1/opportunities/:id/tasks","summary":"List tasks attached to an opportunity","capability":"opportunity:read","description":"The tasks attached to this opportunity, with cursor pagination."},{"name":"context","rest":"GET /api/v1/opportunities/:ref/context","summary":"Get an opportunity's full context bundle","capability":"opportunity:read","description":"Get everything needed to act on an opportunity in one call: the opportunity and PRD, linked objectives, the customer(s), comments, linked insights, the active template step, and any linked PRs. Sections you lack the read capability for (e.g. customers, insights) come back null. Pass the opportunity ref (e.g. OPP-23)."},{"name":"scorecard","rest":"GET /api/v1/opportunities/:id/scorecard","summary":"Get an opportunity's scorecard","capability":"opportunity:read","description":"Get an opportunity's scorecard: the org's active scoring criteria (name, normalized decimal weight, invert), the shared 1-5 values with attribution, and the computed 0-100 total (null until every non-zero-weight criterion is scored). Positive active weights sum to 1 and correspond to the percentages shown in Settings; zero-weight criteria do not affect the total."},{"name":"score","rest":"PATCH /api/v1/opportunities/:id/scores/:criterionId","summary":"Set a scorecard value","capability":"opportunity:update","description":"Set one scorecard value on an opportunity: a shared 1-5 value for one of the org's scoring criteria (last write wins, attributed to the caller). Use get_opportunity_scorecard for criteria ids and the computed total."},{"name":"clear-score","rest":"DELETE /api/v1/opportunities/:id/scores/:criterionId","summary":"Clear a scorecard value","capability":"opportunity:update","description":"Clear one scorecard value on an opportunity (the total returns to null until re-scored)."},{"name":"link-objective","rest":"POST /api/v1/opportunities/:id/link-objective","summary":"Link an objective to an opportunity","capability":"opportunity:update","description":"Tie an opportunity to an objective (membership only: 'this bet claims to matter for that number'). Impact evidence lives on insights, not the link."},{"name":"update-objective-link","rest":"PATCH /api/v1/opportunities/:id/link-objective/:objectiveId","summary":"Update an objective link","capability":"opportunity:update","description":"Repoint an opportunity's objective link at a different objective. `objectiveId` names the link you are moving; `newObjectiveId` is where it lands."},{"name":"unlink-objective","rest":"DELETE /api/v1/opportunities/:id/link-objective/:objectiveId","summary":"Unlink an objective from an opportunity","capability":"opportunity:delete","description":"Remove the tie between an opportunity and an objective."},{"name":"reject-via-template","rest":"POST /api/v1/opportunities/:id/reject-via-template","summary":"Reject an opportunity and skip remaining template steps","capability":"opportunity:update","description":"Reject an opportunity: skips remaining template steps, lands it on the Cancelled terminal step (or flips a template-less one to cancelled), and records the categorized won't-build decision on the thread."},{"name":"updates","rest":"GET /api/v1/opportunities/:id/updates","summary":"List an opportunity's status updates","capability":"opportunity:read","description":"The status updates posted on this opportunity, newest first. These are the same rows that reach every linked customer's feed; plain discussion comments are separate."},{"name":"post-update","rest":"POST /api/v1/opportunities/:id/post-update","summary":"Post an opportunity status update","capability":"opportunity:update","description":"Post a status update on an opportunity, authored by the key's user. It lands in the opportunity's updates stream and flows into every linked customer's feed. For a plain discussion comment, use create_comment."},{"name":"set-state","rest":"POST /api/v1/opportunities/:id/set-state","summary":"Set an opportunity's lifecycle state","capability":"opportunity:update","description":"Move an opportunity between backlog, in_progress and completed. On a template-bearing opportunity this drives the template (activating resumes the step it paused on, completing lands the Completed terminal step); a template-less one flips its status column. To record a won't-build decision use reject_opportunity, which takes the category and reason this action deliberately has no room for."},{"name":"add-prototype","rest":"POST /api/v1/opportunities/:id/prototypes","summary":"Add a prototype to an opportunity","capability":"opportunity:create","description":"Attach a prototype to an opportunity, attributed to the key's user. Provide exactly one of `url` (http(s) only) or `fileId` (a previously uploaded file). Returns the opportunity's whole prototype list."},{"name":"remove-prototype","rest":"DELETE /api/v1/opportunities/:id/prototypes","summary":"Remove a prototype link from an opportunity","capability":"opportunity:delete","description":"Detach a prototype from an opportunity by its URL. Returns the remaining prototype list."},{"name":"synthesize","rest":"POST /api/v1/opportunities/:id/synthesize","summary":"Draft a PRD from the opportunity's evidence","capability":"opportunity:update","description":"Run the org's configured LLM over the opportunity's insights, customers, prototypes and strategy context and return a draft PRD in markdown. The result is cached for an hour (cached=true, ageMs, and staleSinceLastSynth report the cache's age and how many insights landed since); pass bypassCache to re-draft. This returns a draft — it never writes the PRD, which is what update is for."},{"name":"risks","rest":"GET /api/v1/opportunities/:id/risks","summary":"List an opportunity's validation risks and votes","capability":"opportunity:read","description":"The four validation risks (value, usability, feasibility, viability) for one opportunity: each risk's aggregate and every teammate's vote with its note."},{"name":"cast-risk-vote","rest":"POST /api/v1/opportunities/:id/risks/:risk","summary":"Vote on one of an opportunity's validation risks","capability":"opportunity:update","description":"Record the key's user vote (approve, risky, reject) plus a note on one validation risk. One vote per user per risk, last write wins. Returns all four risks with their recomputed aggregates."},{"name":"apply-prd-template","rest":"POST /api/v1/opportunities/:id/apply-prd-template","summary":"Apply a PRD template to an opportunity","capability":"opportunity:update","description":"Write a PRD template's markdown into an opportunity's PRD, replacing the body or appending to it. Use list_prd_templates for template ids. This writes the OPPORTUNITY, not the template, so it is gated on opportunity:update; the template's own read ACL still applies."},{"name":"scoring-criteria","rest":"GET /api/v1/opportunities/scoring-criteria","summary":"List the org's scoring criteria","capability":"opportunity:read","description":"The org's scorecard criteria (name, weight, invert, order) — the inputs every opportunity scorecard is scored against. Positive active weights are normalized decimal shares that sum to 1 (the Settings surface shows the same values as percentages totaling 100%); a newly added zero-weight criterion is allocation-neutral. Read-only on the wire, and a pure read: defining criteria is a workspace-settings act gated on scoring_criteria:manage and kept in-app, so an org that has never configured scoring answers with an empty list rather than being seeded by your GET."},{"name":"phases","rest":"GET /api/v1/opportunities/:id/phases","summary":"List an opportunity's delivery phases","capability":"opportunity:read","description":"An opportunity's phases in delivery order. A phase is a milestone, not work: it carries a title, an owner, and one target date, and it spawns no task. Its band on the plan runs from the previous phase's target to its own, and is open ended when it has none. taskCount and doneCount count only leaf tasks - work with no live children - because a task with children is a container and counting both would report the same work twice; cancelled and duplicate work is left out of the ratio entirely, since it can never complete. Set a task's phase with update_task."},{"name":"unphased-task-count","rest":"GET /api/v1/opportunities/:id/unphased-task-count","summary":"Count an opportunity's unphased work","capability":"opportunity:read","description":"How many of an opportunity's top level tasks no phase claims. Work is never hidden by being unphased - it is its own group at the end of the arc - so this is what says whether that group exists at all. Counts top level tasks only, for the same reason list_opportunity_phases counts leaves: a container and its children are one piece of work, not two."},{"name":"phase-handoff","rest":"POST /api/v1/opportunities/phases/:fromPhaseId/handoff","summary":"Write the handoff note out of a finished phase","capability":"opportunity:update","description":"Post the handoff note a finished phase owes its successor. It lands as an update on the opportunity, so it reads in the same stream as everything else the team said about the work, and it closes the pending handoff on whichever phase was waiting on this one. An empty note is a no-op: the note is encouraged, never required, and skipping leaves the phase's pending handoff open rather than posting a hollow update."},{"name":"add-phase","rest":"POST /api/v1/opportunities/:id/phases","summary":"Add a delivery phase to an opportunity","capability":"opportunity:update","description":"Append a phase to an opportunity's delivery arc. A phase is a milestone, not work: a title, one optional target date, an optional owner. It spawns no task. Phases do not require a template - an opportunity that never ran one can still have them. New phases land last; use reorder_opportunity_phases to place one."},{"name":"update-phase","rest":"PATCH /api/v1/opportunities/phases/:phaseId","summary":"Rename, re-date or re-own a phase","capability":"opportunity:update","description":"Change a phase's title, target date or owner. Sending a null target date makes the phase open ended, which means the band after it can no longer be anchored. Use list_opportunity_phases for phase ids."},{"name":"remove-phase","rest":"DELETE /api/v1/opportunities/phases/:phaseId","summary":"Remove a phase","capability":"opportunity:update","description":"Delete a phase. The work filed under it is not deleted: those tasks become unphased and stay on the opportunity. Remaining phases close the gap so the arc stays contiguous."},{"name":"reorder-phases","rest":"POST /api/v1/opportunities/:id/reorder-phases","summary":"Reorder an opportunity's phases","capability":"opportunity:update","description":"Set the delivery order of every phase at once. The list must name each of the opportunity's phases exactly once: a partial order would strand whatever it left out at a position it no longer owns. Order decides the bands, because a phase's band runs from the previous phase's target date to its own."}]},{"name":"tasks","description":"List tasks with cursor pagination. Optionally filter by opportunity, stable lifecycle status, concrete statusId, priority, team, owner, or to work that no cycle holds yet.","operations":[{"name":"planned-work-summary","rest":"GET /api/v1/tasks/planned-work-summary","summary":"Summarise planned work","capability":"task:read","description":"Coverage of the planned work in one window: how much of it carries an explicit estimate, a planning window, an opportunity tie, and current source data. Estimate values are totalled per team and per unit and are never blended across either."},{"name":"planned-work-gaps","rest":"GET /api/v1/tasks/planned-work-gaps","summary":"Get planned work gaps","capability":"task:read","description":"The grouped queue of what is missing from the planned-work read model: unestimated, unscheduled, unteamed and unaligned work, teams mixing estimate units, and source work whose binding has stopped converging. Each group names why it changes the calculation and where to fix it."},{"name":"list","rest":"GET /api/v1/tasks","summary":"List tasks","capability":"task:read"},{"name":"get","rest":"GET /api/v1/tasks/:id","summary":"Get a task","capability":"task:read","description":"Get a specific task by id."},{"name":"create","rest":"POST /api/v1/tasks","summary":"Create a task","capability":"task:create","description":"Create a task using the stable lifecycle status, optionally refined by a concrete workspace statusId (which wins). The task may attach to an opportunity or stand alone. Optionally seed subtasks, owner, team, category, template, and tags in the same call."},{"name":"update","rest":"PATCH /api/v1/tasks/:id","summary":"Update a task","capability":"task:update","description":"Update a task's fields. Status remains the stable lifecycle vocabulary; statusId optionally selects a concrete workspace status and wins when both are sent. For a bare status move prefer set-status, which also auto-assigns ownerless work."},{"name":"post-update","rest":"POST /api/v1/tasks/:id/post-update","summary":"Post a development update","capability":"task:update","description":"Post one development update for this task. It appears in Development Updates and reaches every customer linked through the task's insights."},{"name":"updates","rest":"GET /api/v1/tasks/:id/updates","summary":"List development updates","capability":"task:read","description":"List development updates posted on this task."},{"name":"delete","rest":"DELETE /api/v1/tasks/:id","summary":"Delete a task","capability":"task:delete","description":"Permanently delete a task. Its time entries, tags, checklist items, and insight links go with it, and its dependency edges are cleared; any subtasks are detached and survive as standalone tasks. To retire work without losing the record, set its status to cancelled instead."},{"name":"set-status","rest":"POST /api/v1/tasks/:id/set-status","summary":"Set a task's status","capability":"task:update","description":"Set a task's stable lifecycle status, optionally refined by a concrete workspace statusId which wins. Moving ownerless work to a todo/in_progress status assigns the caller."},{"name":"set-owner","rest":"POST /api/v1/tasks/:id/set-owner","summary":"Set a task's owner","capability":"task:update","description":"Assign the single accountable owner, or pass null to unassign. Reassigning away from an agent owner cancels its in-flight run; assigning an agent owner dispatches a new one, and confirmedRepo answers the repository prompt that dispatch raises when the target repo is ambiguous."},{"name":"assign-cycle","rest":"POST /api/v1/tasks/:id/assign-cycle","summary":"Preview assigning a task to a cycle","capability":"task:update","description":"Return the proposed planning move and blast-radius impact for assigning this task to a cycle. This does not write; submit the returned move through the planning commit step to apply it, or call move-cycle to preview and apply in one step."},{"name":"move-cycle","rest":"POST /api/v1/tasks/:id/move-cycle","summary":"Move a task into a cycle","capability":"task:update","description":"Assign a task to a cycle and apply the move, the write assign-cycle only previews. A move that takes live work out of a started cycle a customer promise rides on is refused with PROMISE_BREAKS, and the error carries the breaking {taskId, insightId} pairs: send them back as acknowledgedBreaks to proceed, with announceToCustomers to post the change to each affected customer."},{"name":"count-by-status","rest":"GET /api/v1/tasks/count-by-status","summary":"Count tasks per lifecycle status","capability":"task:read","description":"True totals for the six stable lifecycle status types under the same filters `list` takes. Custom named statuses deliberately roll up to their parent type, preserving the aggregate answer to 'how much work is in flight'."},{"name":"attach-session","rest":"POST /api/v1/tasks/:ref/attach-session","summary":"Pin a coding session to a task","capability":"task:update","description":"Attach the coding session working this task, so the engineer can resume it later (the task page offers `claude --resume <sessionId>`) and model usage can be attributed. Idempotent per (provider, sessionId, task); call again with inputTokens/outputTokens/costCents totals to report usage — the stored session updates in place. A session attached to several tasks is several refs, each carrying the session's full totals, so spend double-counts until aggregation dedupes by session."},{"name":"time-in-status","rest":"GET /api/v1/tasks/:id/time-in-status","summary":"Time a task has spent in each status","capability":"task:read","description":"Wall-clock totals per workflow status, folded from the task's status-transition ledger. One row per concrete status the task has visited (named custom statuses stay distinct; rows with no resolvable status fall back to the lifecycle type), with `current` marking the status still accruing. Imported history is only as complete as the source provided."},{"name":"context","rest":"GET /api/v1/tasks/:ref/context","summary":"Get a task's full context bundle","capability":"task:read","description":"Get everything needed to act on a task in one call: the task spec, its parent opportunity and PRD, the customer(s), comments, linked insights, the active template step, and any linked PRs. Sections you lack the read capability for (e.g. the parent opportunity, customers, insights) come back null. Pass the task ref (e.g. TF-24)."}]},{"name":"task-statuses","description":"List the workspace's concrete task statuses in stable lifecycle and custom position order.","operations":[{"name":"list-statuses","rest":"GET /api/v1/task-statuses","summary":"List task statuses","capability":"task:read"},{"name":"create-status","rest":"POST /api/v1/task-statuses","summary":"Create a task status","capability":"task:create","description":"Create a named status inside one stable lifecycle type."},{"name":"update-status","rest":"PATCH /api/v1/task-statuses/:id","summary":"Update a task status","capability":"task:update","description":"Rename or recolor a task status. Its lifecycle type cannot change."},{"name":"reorder-status","rest":"POST /api/v1/task-statuses/reorder","summary":"Reorder a task status","capability":"task:update","description":"Reorder a status within its lifecycle type."},{"name":"archive-status","rest":"POST /api/v1/task-statuses/:id/archive","summary":"Archive a task status","capability":"task:update","description":"Move remaining tasks to a same-type replacement, then archive the status atomically."},{"name":"restore-status","rest":"POST /api/v1/task-statuses/:id/restore","summary":"Restore a task status","capability":"task:update","description":"Restore an archived task status."}]},{"name":"recurring-tasks","description":"List recurring-task definitions with cursor pagination: each one's schedule (human-readable cadence + next occurrence), team, owner, and template. These are the definitions a background sweep materializes into real tasks; use list to see the tasks they produced.","operations":[{"name":"list-recurring","rest":"GET /api/v1/recurring-tasks","summary":"List recurring tasks","capability":"task:read"},{"name":"get-recurring","rest":"GET /api/v1/recurring-tasks/:id","summary":"Get a recurring task","capability":"task:read","description":"Get a recurring-task definition by id: the stored row (rrule, template, active flag). The human-readable cadence, next occurrence, team and owner are list-only."},{"name":"create-recurring","rest":"POST /api/v1/recurring-tasks","summary":"Create a recurring task","capability":"task:create","description":"Define a recurring task: an RRULE schedule plus a task template. A background sweep creates one real task for the given team on every occurrence (status todo, due = occurrence + dueOffsetDays). This creates the DEFINITION, not a single task — use create for a one-off. rrule is an iCal RRULE string (e.g. 'FREQ=WEEKLY;BYDAY=MO'); dtstart/tzid default to now/UTC and it never backfills."},{"name":"update-recurring","rest":"PATCH /api/v1/recurring-tasks/:id","summary":"Update a recurring task","capability":"task:update","description":"Update a definition's schedule, template, or active flag. Editing the schedule recomputes forward from now, never retroactively."},{"name":"set-recurring-active","rest":"POST /api/v1/recurring-tasks/:id/set-active","summary":"Pause or resume a recurring task","capability":"task:update","description":"Pause or resume a recurring-task definition. Paused definitions stop materializing tasks; resuming never backfills the paused gap."},{"name":"delete-recurring","rest":"DELETE /api/v1/recurring-tasks/:id","summary":"Delete a recurring task","capability":"task:delete","description":"Delete a recurring-task definition. Tasks it already created stay; no new ones are generated."}]},{"name":"teams","description":"List teams in the organisation, with cursor pagination.","operations":[{"name":"list","rest":"GET /api/v1/teams","summary":"List teams","capability":"team:read"},{"name":"get","rest":"GET /api/v1/teams/:id","summary":"Get a team","capability":"team:read","description":"Get one team: its name, description, ref prefix (the task-ref namespace, e.g. ENG-42) and default PRD template. The roster is a separate read (list_team_members)."},{"name":"create","rest":"POST /api/v1/teams","summary":"Create a team","capability":"team:create","description":"Create a team. The 2-4 character prefix becomes the team's task ref prefix (e.g. ENG-42)."},{"name":"update","rest":"PATCH /api/v1/teams/:id","summary":"Update a team","capability":"team:update","description":"Update a team's name, description, ref prefix, or default PRD template."},{"name":"delete","rest":"DELETE /api/v1/teams/:id","summary":"Delete a team","capability":"team:delete","description":"Delete a team. Its opportunities/tasks keep their rows (team unset, refs stable); memberships and team-scoped config are removed. Refuses to delete the org's only team."},{"name":"members","rest":"GET /api/v1/teams/:id/members","summary":"List team members","capability":"team:read","description":"The team's roster with cursor pagination: each member's user id, name, email, avatar, membership id, and org role. Salary and weekly-hours fields are included only for a caller holding compensation:read over this team, and are absent otherwise rather than null. Requires team:read."},{"name":"add-member","rest":"POST /api/v1/teams/:id/members","summary":"Add a team member","capability":"team:create","description":"Add a user to a team's roster."},{"name":"remove-member","rest":"DELETE /api/v1/teams/:id/members/:userId","summary":"Remove a team member","capability":"team:delete","description":"Remove a user from a team's roster."},{"name":"ownership-config","rest":"GET /api/v1/teams/ownership-config","summary":"List team ownership config","capability":"team:read","description":"Who owns insight review for each team. One row per team, optionally narrowed to one team; teams with nobody assigned come back with a null reviewer."}]},{"name":"users","description":"List org members with cursor pagination.","operations":[{"name":"list","rest":"GET /api/v1/users","summary":"List users","capability":"user:read"},{"name":"get","rest":"GET /api/v1/users/:id","summary":"Get a user","capability":"user:read","description":"Get one org member's profile: name, email, avatar, org role and weekly hours. Salary fields are included only for a caller holding compensation:read, and are absent otherwise rather than null. Use whoami for the key's own user."},{"name":"update","rest":"PATCH /api/v1/users/:id","summary":"Update a user","capability":"user:update","description":"Update an org member's profile (name, weekly hours). Salary fields require a leadership role and travel as a pair; role changes need org:manage, which API keys never carry."}]},{"name":"tags","description":"List every tag alphabetically with usage counts so you can cluster insights onto existing tags instead of minting duplicates. Follow nextCursor until hasMore is false.","operations":[{"name":"list","rest":"GET /api/v1/tags","summary":"List tags","capability":"tag:read"},{"name":"create","rest":"POST /api/v1/tags","summary":"Create a tag","capability":"tag:create","description":"Create a tag for clustering insights. List existing tags first (list_tags) and reuse them rather than minting duplicates."},{"name":"update","rest":"PATCH /api/v1/tags/:id","summary":"Update a tag","capability":"tag:update","description":"Rename a tag or change its colour. Renaming keeps every insight, task, and opportunity attached to it."},{"name":"delete","rest":"DELETE /api/v1/tags/:id","summary":"Delete a tag","capability":"tag:delete","description":"Delete a tag and detach it from everything it labelled. Check usageCount on list first: the rows it tagged keep no record of it."}]},{"name":"tag-themes","description":"List the themes tags can be grouped under, with the colour each theme lends its tags.","operations":[{"name":"list-themes","rest":"GET /api/v1/tag-themes","summary":"List tag themes","capability":"tag:read"},{"name":"create-theme","rest":"POST /api/v1/tag-themes","summary":"Create a tag theme","capability":"tag:create","description":"Create a theme to group tags under (e.g. 'Clients', 'Risk'). Its colour becomes the default colour of every tag in it."},{"name":"update-theme","rest":"PATCH /api/v1/tag-themes/:id","summary":"Update a tag theme","capability":"tag:update","description":"Rename a theme or change the colour it lends its tags."},{"name":"delete-theme","rest":"DELETE /api/v1/tag-themes/:id","summary":"Delete a tag theme","capability":"tag:delete","description":"Delete a theme. Its tags survive and fall back to their own colour; nothing is deleted with it."}]},{"name":"comments","description":"List a discussion thread, newest first, with cursor pagination. Pass entityType and entityId for an entity's top-level comments, or parentId for the replies threaded under one comment. Rows carry the author, reaction aggregate, and (for top-level rows) the reply count.","operations":[{"name":"list","rest":"GET /api/v1/comments","summary":"List comments","capability":"comment:read"},{"name":"create","rest":"POST /api/v1/comments","summary":"Post a comment","capability":"comment:create","description":"Post a comment on a task or opportunity, authored by the key's user. It lands on the entity's discussion thread. For an opportunity status broadcast that flows into linked customers' feeds, use post_opportunity_update instead."},{"name":"update","rest":"PATCH /api/v1/comments/:id","summary":"Edit a comment","capability":"comment:update","description":"Edit one of your own comments; it is marked edited. Only the author may edit a comment, and comments ingested from an integration cannot be edited."},{"name":"delete","rest":"DELETE /api/v1/comments/:id","summary":"Delete a comment","capability":"comment:delete","description":"Delete one of your own comments. Its thread replies cascade with it, and their file attachments are swept. Only the author (or an org manager) may delete it."},{"name":"set-reaction","rest":"POST /api/v1/comments/:id/reactions","summary":"Add or remove your reaction to a comment","capability":"comment:update","description":"Set whether the authenticated user reacts to a comment with an emoji. Absolute, not a toggle: `on` is the state you want, so a retried call never silently removes the reaction it just added. Returns the comment's full reaction aggregate."}]},{"name":"insights","description":"List customer insights (the customer-voice evidence layer). Filters compose: lifecycle status, demand root (customer), linked work (opportunity/task/objective), review team, recorder, tag, and full-text search over the verbatim. Archived insights are hidden unless includeArchived is set.","operations":[{"name":"list","rest":"GET /api/v1/insights","summary":"List insights","capability":"insight:read"},{"name":"get","rest":"GET /api/v1/insights/:id","summary":"Get an insight","capability":"insight:read","description":"Get an insight with its demand root, links, tags, and committed impacts."},{"name":"create","rest":"POST /api/v1/insights","summary":"Create an insight","capability":"insight:create","description":"Record a customer insight (one discrete signal, not a whole document). Roots at exactly one demand source: a customer, an internal stakeholder (org user), or an other stakeholder (externalStakeholderId — a named voice with no Telos seat). Optionally set source, moscow priority, deadline, tagIds for clustering, a reviewTeamId to route it into a team's review queue, and attach to an opportunity/task/objective."},{"name":"update","rest":"PATCH /api/v1/insights/:id","summary":"Update an insight","capability":"insight:update","description":"Update an insight's verbatim, source, demand root, contact, moscow priority, or deadline. The service keeps exactly one demand root after the merge."},{"name":"delete","rest":"DELETE /api/v1/insights/:id","summary":"Delete an insight","capability":"insight:delete","description":"Permanently delete an insight and its links. Prefer the review lifecycle (set-status rejected/archived) to record an outcome; delete is for mistakes."},{"name":"attach","rest":"POST /api/v1/insights/:id/attach","summary":"Attach an insight","capability":"insight:update","description":"Link an existing insight to an opportunity, task, or objective (optionally pinned). Call it once per insight to attach a whole cluster of related insights to one opportunity."},{"name":"detach","rest":"POST /api/v1/insights/:id/detach","summary":"Detach an insight","capability":"insight:update","description":"Remove the link between an insight and an opportunity, task, or objective. The insight survives; only the tie goes."},{"name":"set-link","rest":"POST /api/v1/insights/:id/set-link","summary":"Set the flags on an insight's link","capability":"insight:update","description":"Set the flags on an existing insight link: pinned (surface it at the top of the work item's evidence rail) and blocking (this client ask blocks the work). Blocking is opportunity/task only — an objective cannot be blocked by a client ask. Pass at least one flag; omitted flags are left alone."},{"name":"set-status","rest":"POST /api/v1/insights/:id/set-status","summary":"Set an insight's review outcome","capability":"insight:update","description":"Move an insight to a review outcome. This is the whole lifecycle in one action: created, review, need_context, active, rejected, delivered, stale, or archived. `rejected` requires a note (the reason, 5-1000 chars) and a rejectionCategory, and publishes the decision to the org and to the customer's feed. `need_context` requires a note (the question, 5-1000 chars) and sends the insight back to whoever recorded it. `archived` hides it from every default list without deleting it. Returns the insight."},{"name":"set-review-team","rest":"POST /api/v1/insights/:id/set-review-team","summary":"Route an insight to a team's review queue","capability":"insight:update","description":"Route an insight to a team for review (this moves it to status review and notifies the team's insight reviewer), or pass null to pull it back out of review. Clearing returns a live insight to active when it has linked work and created when it does not; a terminal insight keeps its outcome. Returns the insight."},{"name":"set-tags","rest":"POST /api/v1/insights/:id/set-tags","summary":"Set an insight's tags","capability":"insight:update","description":"Replace an insight's tag set (tags are how insights cluster into themes). Pass the full list you want, not a delta; an empty list clears them. Create or look up tag ids with the tag actions. Returns the insight."},{"name":"set-impact","rest":"POST /api/v1/insights/:id/set-impact","summary":"Set an insight's objective impacts","capability":"insight:update","description":"Replace the insight's impact ties: which objectives it moves, and by how much. This is the only place a number is typed on an insight; work ties carry no delta. Pass the full list, not a delta; an empty list clears them. Returns the insight."},{"name":"blocking","rest":"GET /api/v1/insights/blocking","summary":"List the insights blocking a work item","capability":"insight:read","description":"The client-voiced blockers on one opportunity or task: open insights whose link to it is flagged blocking. Use it before promising a delivery date."},{"name":"similar","rest":"GET /api/v1/insights/:id/similar","summary":"Find near-duplicate insights and past rejections","capability":"insight:read","description":"Embedding-similarity signals for one insight: near-duplicate insights the org already holds, and near-duplicate asks it already REJECTED (with the reason, the category, and who said no). No LLM call. Read this before recording or reviewing a signal so the org does not relitigate a decision it already made."},{"name":"patterns","rest":"GET /api/v1/insights/patterns","summary":"Read the insight pattern board","capability":"insight:read","description":"The pattern board: semantic clusters of open insights that a human-triggered judge has already scored against the vision, strategies and objectives, plus the raw similarity graph (nodes and edges) behind them. `enabled` and `embeddingModelConfigured` tell you whether clustering is running at all; `unjudgedCount` is how many current clusters have no verdict yet; `truncated` means more open insights exist than the newest window considered."},{"name":"accept-pattern","rest":"POST /api/v1/insights/patterns/:patternId/accept","summary":"Accept an insight pattern (deprecated)","capability":"insight:update","description":"Deprecated compatibility action for clients from before atomic pattern promotion. It records the pattern as actioned but cannot record a work association because the legacy request carries no target. New callers must create work with patternPromotion or use attach-pattern-work."},{"name":"dismiss-pattern","rest":"POST /api/v1/insights/patterns/:patternId/dismiss","summary":"Dismiss an insight pattern","capability":"insight:update","description":"Dismiss a scored pattern. It leaves the board and suppresses future candidates that substantially overlap its members, so the same cluster does not come back every run."},{"name":"attach-pattern-work","rest":"POST /api/v1/insights/patterns/:patternId/attach-work","summary":"Attach a pattern to existing work","capability":"insight:update","description":"Action a suggested insight pattern by attaching its current insights to an existing opportunity or task. The lifecycle transition, work association, and evidence links commit atomically."},{"name":"link-pattern-insights","rest":"POST /api/v1/insights/patterns/:patternId/link-insights","summary":"Link new pattern insights","capability":"insight:update","description":"Link selected, currently unlinked insights in an actioned pattern to one or more associated live work items. Membership and existing links are recomputed server-side, so stale or manually linked selections are safely skipped."}]},{"name":"external-stakeholders","description":"List the org's other stakeholders (named demand voices with no Telos seat), alphabetically, each with how many insights it roots. Use one's id as externalStakeholderId when recording an insight. Returns at most 500; an org past that needs the in-app directory.","operations":[{"name":"list-external-stakeholders","rest":"GET /api/v1/external-stakeholders","summary":"List other stakeholders","capability":"insight:read"},{"name":"create-external-stakeholder","rest":"POST /api/v1/external-stakeholders","summary":"Create an other stakeholder","capability":"insight:create","description":"Create a named demand voice with no Telos seat, for rooting insights. Deduplicated case-insensitively on name: an existing voice with the same name is returned instead of a second row, so the same person typed twice stays one voice."}]},{"name":"prd-templates","description":"List the PRD templates visible to the key's user: shared (org-wide) templates plus those owned by the user's teams. Pass allTeams to read the whole org catalog instead, which additionally requires prd_template:update.","operations":[{"name":"list","rest":"GET /api/v1/prd-templates","summary":"List PRD templates","capability":"prd_template:read"},{"name":"get","rest":"GET /api/v1/prd-templates/:id","summary":"Get a PRD template","capability":"prd_template:read","description":"Get one PRD template: its name, markdown content, and team scope (teamId null means shared org-wide). Visible when the template is shared or owned by one of the key user's teams. Writing it into an opportunity is apply_opportunity_prd_template."},{"name":"create","rest":"POST /api/v1/prd-templates","summary":"Create a PRD template","capability":"prd_template:create","description":"Create a PRD template (markdown scaffold for opportunity PRDs). teamId scopes it to a team; omit for a shared org-wide template, which requires the prd_template:update capability."},{"name":"update","rest":"PATCH /api/v1/prd-templates/:id","summary":"Update a PRD template","capability":"prd_template:update","description":"Update a PRD template's name, content, or team scope."},{"name":"delete","rest":"DELETE /api/v1/prd-templates/:id","summary":"Delete a PRD template","capability":"prd_template:delete","description":"Delete a PRD template. Opportunities that already applied it keep their PRD content."}]},{"name":"webhook-subscriptions","description":"List the org's outbound webhook subscriptions. Secrets are write-once and never returned.","operations":[{"name":"list","rest":"GET /api/v1/webhook-subscriptions","summary":"List webhook subscriptions","capability":"webhook:read"},{"name":"create","rest":"POST /api/v1/webhook-subscriptions","summary":"Create a webhook subscription","capability":"webhook:create","description":"Subscribe an HTTPS endpoint to outbound events. Deliveries are signed with the secret (t=/v1= scheme); the secret is encrypted at rest and never returned, so store it now."},{"name":"update","rest":"PATCH /api/v1/webhook-subscriptions/:id","summary":"Update a webhook subscription","capability":"webhook:update","description":"Change a subscription's target URL, event types, description, or active flag. The signing secret cannot be changed here — it is write-once, so rotate by deleting the subscription and creating a new one."},{"name":"test","rest":"POST /api/v1/webhook-subscriptions/:id/test","summary":"Send a test event to a webhook subscription","capability":"webhook:update","description":"Emit a synthetic event so you can verify the endpoint and its signature check end to end. Returns the emitted event id; the delivery itself lands at your endpoint, signed with the subscription's secret."},{"name":"delete","rest":"DELETE /api/v1/webhook-subscriptions/:id","summary":"Delete a webhook subscription","capability":"webhook:delete","description":"Delete a webhook subscription. Deliveries stop immediately."}]},{"name":"templates","description":"List the org's template templates with cursor pagination. Templates carry no category: categories group tasks and opportunities, not templates.","operations":[{"name":"list","rest":"GET /api/v1/templates","summary":"List templates","capability":"template:read"},{"name":"get","rest":"GET /api/v1/templates/:id","summary":"Get a template","capability":"template:read","description":"Get a template template with its ordered steps. The terminal Completed/Cancelled pair is system-managed and not included."},{"name":"create","rest":"POST /api/v1/templates","summary":"Create a template","capability":"template:create","description":"Create a template template (name, optional description, optional defaultEntityType). Every template closes with system-managed Completed and Cancelled steps; add real steps with create-step."},{"name":"update","rest":"PATCH /api/v1/templates/:id","summary":"Update a template","capability":"template:update","description":"Update a template's name, description, or default entity type."},{"name":"delete","rest":"DELETE /api/v1/templates/:id","summary":"Delete a template","capability":"template:delete","description":"Delete a template template. Refused while active tasks or opportunities are using it."},{"name":"create-step","rest":"POST /api/v1/templates/:id/steps","summary":"Add a template step","capability":"template:create","description":"Append a step to a template, ahead of the terminal pair. Owner defaults to the caller when defaultDriUserId is omitted."},{"name":"update-step","rest":"PATCH /api/v1/templates/steps/:stepId","summary":"Update a template step","capability":"template:update","description":"Update a step's title, description, position, owner, or SLA. Terminal steps are system-managed and can't be edited."},{"name":"delete-step","rest":"DELETE /api/v1/templates/steps/:stepId","summary":"Delete a template step","capability":"template:delete","description":"Delete a step and close the position gap. Refused for terminal steps and for steps active work is currently on."},{"name":"handoff","rest":"POST /api/v1/templates/handoff","summary":"Write a template handoff","capability":"template:update","description":"Move an opportunity or customer-onboarding template to a step and post a handoff message for it. Jumps to the target step instance (optionally completing the rest) and records the handoff note on the entity's thread. For a subtask sequence inside a task, use write_task_handoff instead."},{"name":"usage","rest":"GET /api/v1/templates/:id/usage","summary":"List active work using a template","capability":"template:read","description":"The tasks and opportunities currently running on this template, with counts. Delete refuses while either count is above zero; reassign-and-delete is the way through."},{"name":"default","rest":"GET /api/v1/templates/default/:entityType","summary":"Get the org's default template for an entity type","capability":"template:read","description":"The template a new task, opportunity or customer gets when the caller does not choose one: the flagged default, else the oldest matching template. 404 when the org has none for that entity type."},{"name":"reassign-and-delete","rest":"POST /api/v1/templates/:id/reassign","summary":"Move every entity to another template, then delete this one","capability":"template:delete","description":"Repoint every task and opportunity on this template onto the replacement, re-seed step instances for the active ones, and delete the template. Destructive: step progress on the reassigned work resets, so moving tasks also requires task:update and moving opportunities also requires opportunity:update. Use plain delete when nothing is using it."},{"name":"instance","rest":"GET /api/v1/templates/instances/:entityType/:entityId","summary":"Read the template running on an entity","capability":"template:read","description":"The live process on a task, opportunity or customer: which step is active, how far it has run, and every step instance in order with its owner and status. This is the read an agent needs before advancing a step or writing a handoff. 404 when the entity carries no template."},{"name":"attach","rest":"POST /api/v1/templates/instances","summary":"Start a template on an entity","capability":"template:update","description":"Put a task, opportunity or customer on a template for the first time and seed its step instances (opportunities also get one schedulable task per phase). Owners resolve from each step's default user or role. Set activateFirstStep false to seed everything pending. Refused when the entity already runs a template; use swap for that."},{"name":"swap","rest":"POST /api/v1/templates/instances/swap","summary":"Move an entity onto a different template","capability":"template:update","description":"Replace the template an entity is already running, in one transaction. Destructive: the old step instances are deleted and progress resets, since template steps rarely line up, so it also requires the target entity's own update permission (task:update, opportunity:update or customer:update). No-op when the entity is already on the target template."},{"name":"update-instance-step","rest":"PATCH /api/v1/templates/instances/steps/:stepInstanceId","summary":"Advance or reassign a running step","capability":"template:update","description":"Set a step instance's status and/or its owner. Status moves are checked against the allowed transitions, and completing or skipping a step activates the next pending one. To move the pointer to an arbitrary step and announce it, use write_template_handoff instead. This is the running instance, not the template step update_template_step edits."}]},{"name":"categories","description":"List the org's categories in display order (used to group templates and to classify time entries).","operations":[{"name":"list","rest":"GET /api/v1/categories","summary":"List categories","capability":"category:read"},{"name":"get","rest":"GET /api/v1/categories/:id","summary":"Get a category","capability":"category:read","description":"Get one category: its label, slug, color and sort order. Categories group tasks and opportunities and classify time entries; the system Bug and Incident categories are present in every org."},{"name":"create","rest":"POST /api/v1/categories","summary":"Create a category","capability":"category:create","description":"Create a category. The slug is derived from the label; color is a theme token or hex value."},{"name":"update","rest":"PATCH /api/v1/categories/:id","summary":"Update a category","capability":"category:update","description":"Update a category's label, color, or sort order. The slug never changes."},{"name":"delete","rest":"DELETE /api/v1/categories/:id","summary":"Delete a category","capability":"category:delete","description":"Delete a category. The system Bug and Incident categories can't be deleted, nor can a category time entries still reference."}]},{"name":"notifications","description":"List the authenticated user's notifications, newest first, with cursor pagination. Filter to unread only, pinned only, or a set of notification types.","operations":[{"name":"list","rest":"GET /api/v1/notifications","summary":"List notifications","capability":null},{"name":"unread-count","rest":"GET /api/v1/notifications/unread-count","summary":"Get unread notification count","capability":null,"description":"Count the authenticated user's unread notifications."},{"name":"mark-read","rest":"POST /api/v1/notifications/:id/read","summary":"Mark a notification read","capability":null,"description":"Mark one of your notifications read."},{"name":"mark-all-read","rest":"POST /api/v1/notifications/read-all","summary":"Mark all notifications read","capability":null,"description":"Mark all of your notifications read."},{"name":"pin","rest":"POST /api/v1/notifications/:id/pin","summary":"Pin a notification","capability":null,"description":"Pin one of your notifications so it stays at the top of the inbox."},{"name":"unpin","rest":"POST /api/v1/notifications/:id/unpin","summary":"Unpin a notification","capability":null,"description":"Unpin one of your notifications."},{"name":"unsubscribe","rest":"POST /api/v1/notifications/:id/unsubscribe","summary":"Unsubscribe at notification source","capability":null,"description":"Unsubscribe from the entity a notification came from, muting its future notifications for you."}]},{"name":"time-entries","description":"List time entries in a date range (from/to, max 366 days), newest first, with cursor pagination. Defaults to the authenticated user's own entries. engineerId reads another engineer's entries and requires the salary read permission (compensation:read); a team-scoped grant admits only engineers on the caller's teams.","operations":[{"name":"list","rest":"GET /api/v1/time-entries","summary":"List time entries","capability":"time_entry:read"},{"name":"create","rest":"POST /api/v1/time-entries","summary":"Log a time entry","capability":"time_entry:create","description":"Log time for the authenticated user against exactly one target: an opportunity (opportunityId), a task (taskId), or a misc category (miscCategoryId, which requires a note). Hours are 0-24 per entry."},{"name":"update","rest":"PATCH /api/v1/time-entries/:id","summary":"Update a time entry","capability":"time_entry:update","description":"Update one of your own time entries (entryDate, hours, note). Only the engineer who logged an entry may edit it."},{"name":"delete","rest":"DELETE /api/v1/time-entries/:id","summary":"Delete a time entry","capability":"time_entry:delete","description":"Delete one of your own time entries. Only the engineer who logged an entry may delete it."},{"name":"aggregate-by-category","rest":"GET /api/v1/time-entries/aggregate-by-category","summary":"Aggregate time by category","capability":"cost:read","description":"Total hours in a date range bucketed by category, across all engineers in the org (optionally filtered to one team's members). Cost reporting: requires an org-wide cost:read grant."},{"name":"costs-for-range","rest":"GET /api/v1/time-entries/costs-for-range","summary":"Get every engineer's costed time in a date range","capability":"cost:read","description":"Hydrated time entries for every engineer in a date range (hours, derived cost, engineer, category, target), capped at 5000 rows; `truncated` flags the cap. Not a paginated list: it is one capped snapshot of the whole window, so narrow the range rather than paging. Use list_time_entries to walk your own entries with a cursor. Cost reporting: requires an org-wide cost:read grant."},{"name":"actual-cost-for-opportunity","rest":"GET /api/v1/time-entries/actual-cost-for-opportunity/:opportunityId","summary":"Actual labor cost of an opportunity","capability":"cost:read","description":"Sum of hours times each entry's snapshotted hourly rate for every time entry logged against the opportunity. Returns 0 when nothing is logged. Requires an org-wide cost:read grant."},{"name":"actual-cost-for-task","rest":"GET /api/v1/time-entries/actual-cost-for-task/:taskId","summary":"Actual labor cost of a task","capability":"cost:read","description":"Sum of hours times each entry's snapshotted hourly rate for every time entry logged against the task. Returns 0 when nothing is logged. Requires an org-wide cost:read grant."},{"name":"upsert-range","rest":"POST /api/v1/time-entries/upsert-range","summary":"Replace a timesheet window","capability":"time_entry:update","description":"Replace the authenticated user's entries in a date window with exactly these rows: every existing entry inside [from, to] is deleted first, so an empty rows array clears the window. Each row targets exactly one of opportunityId, taskId, or miscCategoryId (misc requires a note) and must fall inside the window. The window is capped at 366 days."},{"name":"list-for-opportunity","rest":"GET /api/v1/time-entries/for-opportunity/:opportunityId","summary":"List an opportunity's time entries","capability":"time_entry:read","description":"Every entry logged against one opportunity, newest first: date, hours, and note. Hours only — the cost of those hours rides cost:read on get_time_entry_cost_for_opportunity. Who logged each entry is individual-grain time, so engineerId/engineerName come back null unless the caller holds compensation:read (which an API key never does): sum the hours, do not attribute them."}]},{"name":"rooms","description":"List rooms with cursor pagination. Scope 'mine' (default) returns the rooms you are a member of (DMs, group channels, system rooms) with unread counts and your per-room state, most recently active first. Scope 'open' returns every live open group channel in the org — the channel browser — with isSubscribed telling you which ones you are already in; the per-membership fields are null on those rows. Archived channels appear in neither scope.","operations":[{"name":"list","rest":"GET /api/v1/rooms","summary":"List rooms","capability":"room:read"},{"name":"get","rest":"GET /api/v1/rooms/:id","summary":"Get a room","capability":"room:read","description":"Get a room by UUID with your membership role. Members always read; any org user reads an open group channel; a private group, DM, or system room without a membership row is forbidden."},{"name":"create","rest":"POST /api/v1/rooms","summary":"Create a group channel","capability":"room:create","description":"Create a group channel with at least one other org member; the creator becomes its admin. Visibility 'open' (default) lets any org user read and join; 'private' is invite-only. For a DM use find_or_create_dm."},{"name":"update","rest":"PATCH /api/v1/rooms/:id","summary":"Update a group channel","capability":"room:update","description":"Rename a group channel or edit its description. Room-admin only; DMs and system rooms cannot be edited."},{"name":"archive","rest":"POST /api/v1/rooms/:id/archive","summary":"Archive a group channel","capability":"room:update","description":"Archive a group channel, removing it from everyone's room list. Terminal: rooms have no hard delete. Room-admin only; DMs and system rooms cannot be archived."},{"name":"find-or-create-dm","rest":"POST /api/v1/rooms/dm","summary":"Open a DM with an org member","capability":"room:create","description":"Get the direct-message room you share with another org member, creating it if it does not exist yet. Idempotent: the same pair always resolves to the same room. You cannot DM yourself, and the other user must be in your org."},{"name":"members","rest":"GET /api/v1/rooms/:id/members","summary":"List a room's members","capability":"room:read","description":"List the members of a room you can read, oldest join first, with each member's room role. Requires read access to the room (member, or any org user for an open group channel)."},{"name":"add-member","rest":"POST /api/v1/rooms/:id/members","summary":"Add a member to a group channel","capability":"room:create","description":"Add an org member to a group channel. Room-admin only, and the user must be in your org; DMs and system rooms refuse membership changes. Idempotent: adding an existing member is a no-op."},{"name":"remove-member","rest":"DELETE /api/v1/rooms/:id/members/:userId","summary":"Remove a member from a group channel","capability":"room:delete","description":"Remove a member from a group channel. Room-admin only, and an admin cannot remove themselves (hand the room off first); DMs and system rooms refuse membership changes."},{"name":"set-membership","rest":"PATCH /api/v1/rooms/:id/membership","summary":"Set your own membership state in a room","capability":"room:update","description":"Set the authenticated user's own state on a room: join or leave an open group channel (joined), pin it to the top of your room list (pinned), and choose how much it notifies you (notificationLevel). Every field is absolute, not a toggle, so repeating the call is a no-op. Fields apply in the order join, pin, notification level, leave; you must be able to read the room at all, pinning or muting a room you are not a member of is forbidden, and only live (non-archived) open group channels can be joined this way."}]},{"name":"messages","description":"List a room's top-level messages, newest first, with cursor pagination (service-capped at 100 per page). Rows carry author, reactions, attachments, and thread reply counts; fetch a thread's replies with the thread action. Requires read access to the room (member, or any org user for an open group channel).","operations":[{"name":"list","rest":"GET /api/v1/messages","summary":"List a room's messages","capability":"message:read"},{"name":"thread","rest":"GET /api/v1/messages/:id/thread","summary":"List a message's thread replies","capability":"message:read","description":"List the replies threaded under a top-level message, newest first, with cursor pagination. Requires read access to the message's room."},{"name":"pinned","rest":"GET /api/v1/messages/pinned","summary":"List pinned messages","capability":"message:read","description":"List pinned messages, most recently pinned first, with cursor pagination. Pass roomId to scope to one room you can read; omit it for every pin across the rooms you are a member of. Rows are message rows — read the room's name with get_room."},{"name":"search","rest":"GET /api/v1/messages/search","summary":"Search messages","capability":"message:read","description":"Search the rooms you are a member of by message body (case-insensitive substring), newest first, with an optional room, author, DM-counterpart, and date filters. Returns the top 40 matches with a snippet around the hit; it is a search, not a paginated list, so narrow with filters rather than paging. A query under two characters needs at least one filter to anchor on."},{"name":"create","rest":"POST /api/v1/messages","summary":"Post a message","capability":"message:create","description":"Post a message to a room you can read, authored by the key's user. Pass parentId (a top-level message UUID) to reply in its thread; threads are two-level. @Name mentions in the body notify the named org members."},{"name":"update","rest":"PATCH /api/v1/messages/:id","summary":"Update a message","capability":"message:update","description":"Rewrite the body of one of your own messages; it is marked edited. Only the author may edit a message, and system messages cannot be edited. Returns the same enriched message shape create and list return."},{"name":"delete","rest":"DELETE /api/v1/messages/:id","summary":"Delete a message","capability":"message:delete","description":"Delete one of your own messages (soft delete; attachments are swept). Only the author may delete it on this surface."},{"name":"set-pinned","rest":"POST /api/v1/messages/:id/pinned","summary":"Pin or unpin a message","capability":"message:update","description":"Pin a message to its room, or unpin it. Absolute, not a toggle: pass pinned true or false and repeating the call leaves the same state. Any member who can read the room can curate its pins."},{"name":"set-reaction","rest":"POST /api/v1/messages/:id/reactions","summary":"Add or remove your reaction to a message","capability":"message:update","description":"Set whether the authenticated user reacts to a message with an emoji. Absolute, not a toggle: `on` is the state you want, so a retried call never silently removes the reaction it just added. Returns the message's full reaction aggregate."}]},{"name":"activities","description":"The activity log for one entity, newest first, with cursor pagination: who changed which field, from what to what, and when. Requires the read capability of the entity you are asking about. Read it before proposing a change so you know what already happened.","operations":[{"name":"list","rest":"GET /api/v1/activities","summary":"List an entity's activity","capability":null}]},{"name":"files","description":"List file metadata with cursor pagination. Pass entityType AND entityId together for one entity's attachments (oldest first); omit both for the org-wide manifest (newest first). Bytes never travel this API — `get` returns a short-lived presigned download URL. Rides file:manage because the unfiltered form is the same org manifest the in-app admin surface gates on it.","operations":[{"name":"list","rest":"GET /api/v1/files","summary":"List file metadata","capability":"file:manage"},{"name":"get","rest":"GET /api/v1/files/:id","summary":"Get a file's metadata and download URL","capability":"file:read","description":"Get one file's metadata. Once the upload has been scanned and processed (status `clean`) the response carries `downloadUrl`, a presigned GET valid for about 15 minutes; while the file is pending, processing, or failed that field is null and `status` / `processingError` say why."},{"name":"storage-usage","rest":"GET /api/v1/files/storage-usage","summary":"Get the org's storage usage","capability":"file:read","description":"Bytes currently held against the org's storage quota, the quota itself, and the percentage used. Check this before a large upload: request-upload refuses anything that would cross the quota."},{"name":"request-upload","rest":"POST /api/v1/files","summary":"Request a presigned file upload","capability":"file:manage","description":"Step 1 of 2. Reserves the file row and returns `uploadUrl` plus the `fields` of a presigned POST; send the bytes to that URL yourself, then call confirm-upload with the ETag storage returned. `sha256` must be the lowercase hex digest of the exact bytes you will send — it is bound into the upload policy, so storage rejects a mismatch. Passing entityType/entityId attaches the file to that entity as soon as it lands."},{"name":"confirm-upload","rest":"POST /api/v1/files/:id/confirm","summary":"Confirm a completed file upload","capability":"file:manage","description":"Step 2 of 2. Tells Telos the bytes landed, which moves the file to `processing` and queues the integrity check (size, checksum, MIME sniff) that ends in `clean` or `failed`. Idempotent: confirming an already-confirmed file returns its current state."},{"name":"attach","rest":"POST /api/v1/files/:id/attachments","summary":"Attach a file to an entity","capability":"file:manage","description":"Bind an existing file to an entity so it shows on that entity's attachments rail. Idempotent, and one file may be attached to several entities."},{"name":"detach","rest":"DELETE /api/v1/files/:id/attachments","summary":"Detach a file from an entity","capability":"file:manage","description":"Remove one entity's link to a file. The file itself survives with its other attachments; use delete to destroy it."},{"name":"delete","rest":"DELETE /api/v1/files/:id","summary":"Delete a file","capability":"file:manage","description":"Permanently delete a file: the stored object, its thumbnail, the row, and every attachment pointing at it. Irreversible."}]},{"name":"import-runs","description":"The org's bulk-import runs (Linear, Jira, CSV) with cursor pagination, newest first: source, status (pending/running/paused/completed/partial/failed), per-phase counts, error text, and timings. Starting a run is in-app only; use this to find the run to watch.","operations":[{"name":"list","rest":"GET /api/v1/import-runs","summary":"List import runs","capability":"import:read"},{"name":"get","rest":"GET /api/v1/import-runs/:id","summary":"Get an import run","capability":"import:read","description":"One import run's durable row: source, status, the sanitized run plan, accumulated per-phase counts, resume checkpoint, error text, and timings. Poll it to watch a run progress; a status of completed, partial, or failed is terminal."}]},{"name":"integrations","description":"List the org's installed integrations: connector type, name, status, health counters, and the non-secret config (watched channels, repo maps). Stored credentials are never returned. Import-only sources are excluded, matching the in-app directory.","operations":[{"name":"list","rest":"GET /api/v1/integrations","summary":"List integrations","capability":"integration:read"},{"name":"get","rest":"GET /api/v1/integrations/:id","summary":"Get an integration","capability":"integration:read","description":"Get one integration's connector type, name, status, health counters, and non-secret config. Stored credentials are never returned."},{"name":"events","rest":"GET /api/v1/integrations/:id/events","summary":"List an integration's delivery log","capability":"integration:read","description":"The integration's inbound/outbound delivery ledger with cursor pagination, newest first: kind, status, dedup key, attempt count, error text, and timings. The raw third-party payload and request headers are deliberately withheld — they carry provider verification tokens and signature headers."}]},{"name":"roles","description":"The org's roles with the permission grants each one confers, so you can see who can do what before proposing a change. Creating, renaming, deleting a role and rewriting its grants are in-app only.","operations":[{"name":"list","rest":"GET /api/v1/roles","summary":"List roles","capability":"role:read"}]},{"name":"search","description":"Ranked title and body matches across visions, strategies, objectives, metrics, opportunities, tasks, customers, insights, teams, and users, each with its ref and parent. Only the entity types you hold the read capability for are searched, so a narrow key sees a narrow result set. Needs at least 2 characters and returns at most 40 results; use the per-entity list actions to page a single type.","operations":[{"name":"global","rest":"GET /api/v1/search","summary":"Search across the workspace","capability":null}]},{"name":"subscriptions","description":"Report whether the authenticated user currently follows an entity, i.e. receives its notifications. False covers both never-subscribed and explicitly muted.","operations":[{"name":"status","rest":"GET /api/v1/subscriptions","summary":"Check whether you follow an entity","capability":null},{"name":"set","rest":"POST /api/v1/subscriptions","summary":"Follow or unfollow an entity","capability":null,"description":"Set whether the authenticated user follows an entity and receives its notifications. Absolute, not a toggle: `subscribed` is the state you want, so repeating the call is a no-op. Requires the read capability of the entity you are following. Unfollowing mutes the entity permanently for you — participation (commenting, being assigned) will not silently re-subscribe you."}]},{"name":"external-links","description":"Everything outside Telos attached to one work item, in a single read: `refs` are the outbound links (pull requests, issues, Slack threads, Notion pages, linked Telos chat messages) and `blockers` are the open 'waiting on someone outside the team' flags. Blockers exist for tasks and opportunities only, so an insight always answers with an empty list. Links to messages in rooms you cannot read are filtered out, and every message URL is rebuilt from the room's current slug. Reading an opportunity's or insight's links also requires that entity's read permission, not just task:read.","operations":[{"name":"external-links","rest":"GET /api/v1/external-links","summary":"Get a work item's external links and blockers","capability":"task:read"},{"name":"block-on-external","rest":"POST /api/v1/external-links/blockers","summary":"Flag work as waiting on someone outside the team","capability":"task:update","description":"Record that a task or opportunity is waiting on a stakeholder, client, or partner. Advisory only: nothing about the schedule moves, it makes the wait visible. Optionally name the customer contact being waited on and why. Flagging an opportunity also requires opportunity:update, not just task:update."},{"name":"resolve-external-block","rest":"DELETE /api/v1/external-links/blockers/:blockerId","summary":"Resolve an external blocker","capability":"task:update","description":"Mark an external blocker resolved: the external party came back. The row is kept with a resolution timestamp rather than deleted, and it stops appearing on the item's external links."},{"name":"link-message","rest":"POST /api/v1/external-links/messages","summary":"Link a work item back to the chat message it came from","capability":"task:update","description":"Record that a task, opportunity, or insight originated in a Telos chat message. The permalink is built server-side from the message's own room and thread, so you only supply the message id; you must be able to read the source room. Anchoring on an opportunity or insight also requires that entity's update permission, not just task:update. Idempotent on the same pair."}]},{"name":"agent-runs","description":"Coding-agent dispatch history with cursor pagination, newest first. Supply exactly one of taskId (one task's runs, with the repo and branch the agent worked in) or integrationId (one connector's runs, with each run's task ref). The field the other filter would have told you comes back null.","operations":[{"name":"agent-runs","rest":"GET /api/v1/agent-runs","summary":"List coding-agent runs","capability":"task:read"},{"name":"cancel-agent-run","rest":"POST /api/v1/agent-runs/:id/cancel","summary":"Cancel a coding-agent run","capability":"task:update","description":"Stop an in-flight coding-agent run: the provider session is cancelled best-effort and the run lands in the terminal `cancelled` state with a summary. Cancelling an already-finished run returns it unchanged."}]},{"name":"viewer","description":"Who this credential acts as: user id, name, email, org id, role, the teams the caller belongs to, and the permission grants the credential actually carries. Start here to find out what you are allowed to do.","operations":[{"name":"me","rest":"GET /api/v1/viewer","summary":"Get the authenticated caller","capability":null},{"name":"set-avatar","rest":"POST /api/v1/viewer/avatar","summary":"Set the caller's own avatar","capability":null,"description":"Point the caller's avatar at an uploaded file. Upload it first with request-file-upload and confirm-file-upload; this takes the resulting file id. Self-scoped: a credential can only set its own avatar."}]},{"name":"cycles","description":"List the organisation's retained cycle windows with cursor pagination. Reading never generates new windows.","operations":[{"name":"list","rest":"GET /api/v1/cycles","summary":"List cycles","capability":"cycle:read"},{"name":"get","rest":"GET /api/v1/cycles/:id","summary":"Get a cycle","capability":"cycle:read","description":"Get one cycle window and its lifecycle state."},{"name":"grids","rest":"GET /api/v1/cycles/grids","summary":"List the cycle grids","capability":"cycle:read","description":"List the grids cycles are generated on: one row for the organisation in org mode, or one row per cycle-enabled team in team mode. Answers which teamId every other cycle read should be narrowed by."},{"name":"list-with-stats","rest":"GET /api/v1/cycles/stats","summary":"List one grid's cycles with delivery stats","capability":"cycle:read","description":"List one grid's cycle windows newest first, each decorated with derived delivery analytics: lifecycle status, capacity and committed days, scope, work added after start, work spilled out, work completed, success percentage, and the customer commitments falling due inside the window. Closed cycles read their frozen close-time digest, open ones read live bindings."},{"name":"get-by-number","rest":"GET /api/v1/cycles/by-number/:grid/:number","summary":"Get a cycle by grid and number","capability":"cycle:read","description":"Resolve the cycle a grid numbers as N, the addressing the /cycles/{grid}/{number} deep links use. The grid is a team slug, or the literal 'org' for the organisation grid. Answers with the cycle plus its team slug and derived lifecycle status."},{"name":"burnup","rest":"GET /api/v1/cycles/:id/burnup","summary":"Get a cycle's burnup series","capability":"cycle:read","description":"Get the daily cumulative scope and completed series across a cycle's window, in estimate days. Scope rises as work is bound after the start; completions land on their completion day, with post-window completions clamped onto the final point. Closed cycles read their frozen close-time digest."},{"name":"breakdown","rest":"GET /api/v1/cycles/:id/breakdown","summary":"Break a cycle's work down by owner, priority and team","capability":"cycle:read","description":"Group a cycle's member work by owner, by priority and by team, each with a count and a percentage of the cycle's scope. The composition summary; ask task list with a cycle filter for the rows themselves."},{"name":"options","rest":"GET /api/v1/cycles/options","summary":"Search cycles for a picker","capability":"cycle:read","description":"A bounded, searchable window over every retained cycle, grouped by grid and newest window first, for building a cycle selector. Matches name or number. The answer carries the unbounded total, so a caller can tell a partial window from a complete one; cycle list is the paginated read that walks them all."},{"name":"impact-candidates","rest":"GET /api/v1/cycles/impact-candidates","summary":"List the cycles work can still be assigned to","capability":"cycle:read","description":"The active and future cycles of every enabled grid, current window first: the only windows a task may be assigned to. Narrow by teamId to one task's effective grid. Retained history is excluded by design and never displaces these rows."},{"name":"get-verdict","rest":"GET /api/v1/cycles/:id/verdict","summary":"Get a cycle verdict","capability":"cycle:read","description":"Get the immutable close-time verdict for a committed cycle, if it has closed."},{"name":"rollover","rest":"POST /api/v1/cycles/:id/rollover","summary":"Roll over an ended cycle now","capability":"cycle:manage","description":"Explicitly move unfinished work from an ended cycle to its next window. This can resume a cycle parked because automatic rollover is off."},{"name":"commit","rest":"POST /api/v1/cycles/:id/commit","summary":"Commit a cycle baseline","capability":"cycle:manage","description":"Capture the one-shot opening commitment digest for a cycle. The baseline cannot be replaced after commitment."},{"name":"commitments","rest":"GET /api/v1/cycles/:id/commitments","summary":"List a cycle's customer commitments","capability":"cycle:read","description":"List the customer promises falling due inside a cycle's window, attributed to its grid, with delivery coverage: delivered, covered, at risk (with breach days), or uncovered."},{"name":"commitments-batch","rest":"GET /api/v1/cycles/commitments","summary":"List several cycles' customer commitments at once","capability":"cycle:read","description":"Read the customer promises of up to twenty cycle windows in one call, each answering exactly what cycle commitments answers for that window: the promises falling due inside it, attributed to its grid, with delivery coverage. Ids outside the organisation are dropped rather than reported. For a planning horizon of cycle columns; ask cycle commitments for one window."},{"name":"load","rest":"GET /api/v1/cycles/:id/load","summary":"Get a cycle's load per person","capability":"cycle:read","description":"Break a cycle's live membership down by owner as assigned minutes against that person's capacity for the window: one workday per weekday, the unit estimates are written in. Unestimated work counts at the organisation default and unowned work answers in its own bucket. Everyone carrying work gets a row; the grid's idle members fill the remaining seats and the rest are counted in idleOmitted, so a large roster does not have to be paged. Completed members stay in the load because the window's capacity was spent on them."},{"name":"load-batch","rest":"GET /api/v1/cycles/load","summary":"Get several cycles' load per person at once","capability":"cycle:read","description":"Read the per-person load of up to twenty cycle windows in one call, each answering exactly what cycle load answers for that window: assigned minutes by owner against that window's own capacity. Ids outside the organisation are dropped rather than reported. For a planning horizon of cycle columns; ask cycle load for one window."},{"name":"events","rest":"GET /api/v1/cycles/:id/events","summary":"List a cycle's ledger events","capability":"cycle:read","description":"List the cycle's informational ledger, newest first: scope added after start, promises at risk on entry, promises moved out, and promises unfinished at close."},{"name":"impact","rest":"GET /api/v1/cycles/:id/impact","summary":"Get cycle impact","capability":"cycle:read","description":"Preview the selected cycle's standing commitments, rollover effects, and cross-team collisions without mutating work."},{"name":"update","rest":"PATCH /api/v1/cycles/:id","summary":"Update a future cycle","capability":"cycle:manage","description":"Change the identity, grid, or half-open window of an open, uncommitted future cycle."},{"name":"delete","rest":"DELETE /api/v1/cycles/:id","summary":"Delete an empty future cycle","capability":"cycle:manage","description":"Delete an open, uncommitted future cycle only when no task of any status references it."}]},{"name":"cycle-config","description":"Set the organisation-wide minutes counted for an unestimated task in cycle capacity, load and burnup math. It changes what every cycle's numbers mean, so it is workspace administration and requires cycle:manage.","operations":[{"name":"update-default-estimate","rest":"PATCH /api/v1/cycle-config/default-estimate","summary":"Set the default estimate for unestimated work","capability":"cycle:manage"},{"name":"get-config","rest":"GET /api/v1/cycle-config","summary":"Get cycle configuration","capability":"cycle:manage","description":"Get the organisation's cycle-planning configuration. Null means it has never been configured."},{"name":"configure","rest":"PATCH /api/v1/cycle-config","summary":"Configure cycle planning","capability":"cycle:manage","description":"Configure cycle planning for the organisation. Cycle configuration is workspace administration and requires cycle:manage."}]}],"conventions":{"envelope":"All responses: { ok, data, meta: { requestId, timestamp }, links, actions? }. Errors: { ok: false, error: { code, message, suggestion?, data? } }","pagination":"Cursor-based. Pass ?cursor=<opaque_token>&limit=50 (max 200); the same default and cap apply on MCP and the CLI. Response includes pagination.cursor and pagination.hasMore.","filtering":"Per-operation. Each list operation declares its own query params (e.g. ?status=draft on GET /opportunities); they are listed in GET /api/v1/openapi.json. There are no generic operator suffixes and an undeclared param is ignored.","sorting":"Not a query parameter. Each list operation has one fixed order; walk it with cursor.","idempotency":"Send Idempotency-Key header on mutations. Same key+method+path returns cached result for 24h.","batch":"POST /api/v1/batch with { operations: [{ id, method, path, body? }] }. Max 20 operations.","actions":"Responses include an 'actions' array showing valid next operations with exact method, href, and body to send.","errors":"Error codes: VALIDATION_FAILED, NOT_FOUND, ALREADY_EXISTS, INVALID_STATE_TRANSITION, FORBIDDEN, UNAUTHORIZED, RATE_LIMITED, IDEMPOTENCY_CONFLICT, BATCH_PARTIAL_FAILURE, PLAN_LIMIT_EXCEEDED, PLAN_FEATURE_REQUIRED, AGENT_REPO_CONFIRMATION_REQUIRED, SOURCE_OWNED_FIELDS, PROMISE_BREAKS, INTERNAL_ERROR. The 'suggestion' field tells you exactly how to fix the error. Recoverable refusals return 409 and carry an 'error.data' object with the rows to send back: PROMISE_BREAKS carries { breaks: [{ taskId, insightId, ... }] }, which you re-send as acknowledgedBreaks on POST /tasks/:id/move-cycle."},"ai":{"description":"AI context endpoint for terminal-based agents (Claude Code, Codex, Copilot). Returns vision context + system prompt in one call.","endpoint":"GET /api/v1/ai/context/vision/:visionId","returns":"systemPrompt (use as system message), context (current vision state + gaps), tools (REST endpoints for applying changes)","template":"1. Fetch context. 2. Use systemPrompt to challenge the vision with the user. 3. Apply changes via PATCH/POST endpoints."},"hierarchy":"vision → objective → metric → opportunity → task. Use nested routes (e.g., GET /visions/:id/objectives) or flat routes with filters.","rateLimits":{"default":"600 requests/minute per API key","headers":"X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset"}}