{"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"},{"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 workflow."},{"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/:customerId/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/:customerId/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/:customerId/contacts/:id","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/:customerId/contacts/:id","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":"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"},{"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:create","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:create","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":"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"},{"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."}]},{"name":"metrics","description":"List metrics tracked by the organisation, with cursor pagination.","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"},{"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/:metricId/values","summary":"List metric values","capability":"metric:read","description":"Recorded values for a metric, ascending by recorded time; when limit caps the count, the newest values are kept. Optionally filter by date (since)."},{"name":"record-value","rest":"POST /api/v1/metrics/:metricId/values","summary":"Record a metric value","capability":"metric:create","description":"Record an observed value for a metric. recordedAt defaults to now; pass it to backfill history."}]},{"name":"objectives","description":"List strategic objectives with cursor pagination. Optionally scope to a vision.","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"},{"name":"create","rest":"POST /api/v1/objectives","summary":"Create an objective","capability":"objective:create","description":"Create a strategic objective under a vision. The objective IS the commitment to a number: it requires a DRI and a metric block (metricId, operator, target)."},{"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/:objectiveId/opportunities","summary":"List opportunities for an objective","capability":"objective:read","description":"The opportunities linked to this objective, with their link metadata."},{"name":"set-metric","rest":"POST /api/v1/objectives/:objectiveId/set-metric","summary":"Set an objective's metric","capability":"objective:create","description":"Replace which metric an objective points at, and/or change its target shape (operator, target, start value). An objective has exactly one metric."}]},{"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, workflow, category, initial objective links, and tags. 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, or PRD body. Lifecycle moves go through write_handoff (workflow 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 workflow 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, weight, invert), the shared 1-5 values with attribution, and the computed 0-100 total (null until every criterion is scored)."},{"name":"score","rest":"PATCH /api/v1/opportunities/:opportunityId/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/:opportunityId/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/:opportunityId/link-objective","summary":"Link an objective to an opportunity","capability":"opportunity:create","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/:opportunityId/link-objective/:objectiveId","summary":"Update an objective link","capability":"opportunity:update","description":"Repoint an opportunity's objective link at a different objective."},{"name":"unlink-objective","rest":"DELETE /api/v1/opportunities/:opportunityId/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-workflow","rest":"POST /api/v1/opportunities/:opportunityId/reject-via-workflow","summary":"Reject an opportunity and skip remaining workflow steps","capability":"opportunity:create","description":"Reject an opportunity: skips remaining workflow steps, lands it on the Cancelled terminal step (or flips a workflow-less one to cancelled), and records the categorized won't-build decision on the thread."},{"name":"post-update","rest":"POST /api/v1/opportunities/:opportunityId/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":"tasks","description":"List tasks with cursor pagination. Optionally filter by opportunity, status, priority, or team.","operations":[{"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, attached to an opportunity (opportunityId) or standalone (bug fixes and maintenance are first-class). Optionally seed subtasks, owner, team, category, workflow, 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 (title, description, content, priority, opportunity, team, category, subtask mode, estimate, due date). For a bare status move prefer set-status, which also auto-assigns an owner to ownerless work."},{"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 status. Moving an ownerless task to todo/in_progress assigns the caller as its owner — work in flight is never ownerless."},{"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 workflow 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":"write-handoff","rest":"POST /api/v1/tasks/handoff","summary":"Write a subtask handoff note","capability":"task:update","description":"Post the handoff note for a completed subtask in a sequential task, briefing the next owner. Complete the subtask first (set-status), then write the note; it lands on the parent task's thread and clears the pending-note reminder."}]},{"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."},{"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"},{"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/:teamId/members","summary":"List team members","capability":"team:read"},{"name":"add-member","rest":"POST /api/v1/teams/:teamId/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/:teamId/members/:userId","summary":"Remove a team member","capability":"team:delete","description":"Remove a user from a team's roster."}]},{"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"},{"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 tags (with usage counts) so you can cluster insights onto existing tags instead of minting duplicates.","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":"comments","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.","operations":[{"name":"create","rest":"POST /api/v1/comments","summary":"Post a comment","capability":"comment:create"}]},{"name":"insights","description":"List customer insights (the customer-voice evidence layer). Optionally filter by lifecycle status or customer.","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 (reject/archive) to record an outcome; delete is for mistakes."},{"name":"attach","rest":"POST /api/v1/insights/:insightId/attach","summary":"Attach an insight","capability":"insight:update","description":"Link an existing insight to an opportunity, task, or objective (optionally pinned)."}]},{"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.","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"},{"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":"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":"workflows","description":"List the org's workflow templates with cursor pagination, each with its category label.","operations":[{"name":"list","rest":"GET /api/v1/workflows","summary":"List workflows","capability":"workflow:read"},{"name":"get","rest":"GET /api/v1/workflows/:id","summary":"Get a workflow","capability":"workflow:read","description":"Get a workflow template with its ordered steps. The terminal Completed/Cancelled pair is system-managed and not included."},{"name":"create","rest":"POST /api/v1/workflows","summary":"Create a workflow","capability":"workflow:create","description":"Create a workflow template. Optionally set a categoryId to group it. Every workflow closes with system-managed Completed and Cancelled steps; add real steps with create-step."},{"name":"update","rest":"PATCH /api/v1/workflows/:id","summary":"Update a workflow","capability":"workflow:update","description":"Update a workflow's name, description, category, or default entity type."},{"name":"delete","rest":"DELETE /api/v1/workflows/:id","summary":"Delete a workflow","capability":"workflow:delete","description":"Delete a workflow template. Refused while active tasks or opportunities are using it."},{"name":"create-step","rest":"POST /api/v1/workflows/:workflowId/steps","summary":"Add a workflow step","capability":"workflow:create","description":"Append a step to a workflow, ahead of the terminal pair. Owner defaults to the caller when defaultDriUserId is omitted."},{"name":"update-step","rest":"PATCH /api/v1/workflows/steps/:id","summary":"Update a workflow step","capability":"workflow: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/workflows/steps/:id","summary":"Delete a workflow step","capability":"workflow: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/workflows/handoff","summary":"Write a workflow handoff","capability":"workflow:update","description":"Move an opportunity or customer-onboarding workflow 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":"workflow-categories","description":"List the org's workflow categories in display order (used to group workflows and to classify time entries).","operations":[{"name":"list","rest":"GET /api/v1/workflow-categories","summary":"List workflow categories","capability":"workflow_category:read"},{"name":"get","rest":"GET /api/v1/workflow-categories/:id","summary":"Get a workflow category","capability":"workflow_category:read"},{"name":"create","rest":"POST /api/v1/workflow-categories","summary":"Create a workflow category","capability":"workflow_category:create","description":"Create a workflow category. The slug is derived from the label; color is a theme token or hex value."},{"name":"update","rest":"PATCH /api/v1/workflow-categories/:id","summary":"Update a workflow category","capability":"workflow_category:update","description":"Update a category's label, color, or sort order. The slug never changes."},{"name":"delete","rest":"DELETE /api/v1/workflow-categories/:id","summary":"Delete a workflow category","capability":"workflow_category:delete","description":"Delete a workflow 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 workflow 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 workflow category","capability":"cost:read","description":"Total hours in a date range bucketed by workflow category, across all engineers in the org (optionally filtered to one team's members). Cost reporting: requires an org-wide cost:read grant."},{"name":"list-for-range","rest":"GET /api/v1/time-entries/for-range","summary":"List all engineers' time entries with cost","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. 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":"rooms","description":"List the rooms the authenticated user is a member of (DMs, group channels, system rooms), with unread counts and your per-room state, most recently active first.","operations":[{"name":"list","rest":"GET /api/v1/rooms","summary":"List your 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. DMs are started in-app, not here."},{"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":"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":"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":"edit","rest":"PATCH /api/v1/messages/:id","summary":"Edit a message","capability":"message:update","description":"Edit one of your own messages; it is marked edited. Only the author may edit a message, and system messages cannot be edited."},{"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."}]}],"conventions":{"envelope":"All responses: { ok, data, meta: { requestId, timestamp }, links, actions? }. Errors: { ok: false, error: { code, message, suggestion? } }","pagination":"Cursor-based. Pass ?cursor=<opaque_token>&limit=50 (max 200). Response includes pagination.cursor and pagination.hasMore.","filtering":"Query params: ?status=draft&priority=high,urgent. Operators: .gte, .lte, .gt, .lt, .startsWith (e.g., ?createdAt.gte=2026-01-01)","sorting":"?sort=field:asc,field2:desc","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, INTERNAL_ERROR. The 'suggestion' field tells you exactly how to fix the error."},"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)","workflow":"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"}}