openapi: 3.2.0 info: title: Taskfolk Portfolios API version: 1.0.0 description: Public REST API for Taskfolk (taskfolk.ai). All endpoints require a workspace-scoped bearer API key. Paths are nested under `/v1/workspaces/{slug}/…`. servers: - url: https://taskfolk.ai/api security: - bearerAuth: [] tags: - name: Portfolios paths: /v1/workspaces/{slug}/portfolios: get: summary: List portfolios. tags: - Portfolios security: - bearerAuth: - portfolios:read parameters: - schema: type: string example: taskfolk description: Workspace slug. required: true description: Workspace slug. name: slug in: path - schema: type: string required: false name: cursor in: query - schema: type: - integer - 'null' required: false name: limit in: query responses: '200': description: Success. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/Portfolio' pagination: $ref: '#/components/schemas/Pagination' required: - data - pagination '400': description: Validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: Missing / invalid API key. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: Key lacks the required scope. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: Resource not found (or cross-workspace — never leaked). content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: Rate limited. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' post: summary: Create a portfolio (owner/admin-tier scope; per-plan cap). tags: - Portfolios security: - bearerAuth: - portfolios:write parameters: - schema: type: string example: taskfolk description: Workspace slug. required: true description: Workspace slug. name: slug in: path requestBody: content: application/json: schema: $ref: '#/components/schemas/PortfolioCreateInput' responses: '201': description: Success. content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Portfolio' required: - data '400': description: Validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: Missing / invalid API key. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: Key lacks the required scope. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: Resource not found (or cross-workspace — never leaked). content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: Rate limited. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' /v1/workspaces/{slug}/portfolios/{portfolioId}: get: summary: Get a portfolio. tags: - Portfolios security: - bearerAuth: - portfolios:read parameters: - schema: type: string example: taskfolk description: Workspace slug. required: true description: Workspace slug. name: slug in: path - schema: type: string example: 019e6f12-… description: Portfolio id. required: true description: Portfolio id. name: portfolioId in: path responses: '200': description: Success. content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Portfolio' required: - data '400': description: Validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: Missing / invalid API key. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: Key lacks the required scope. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: Resource not found (or cross-workspace — never leaked). content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: Rate limited. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' patch: summary: Update a portfolio. tags: - Portfolios security: - bearerAuth: - portfolios:write parameters: - schema: type: string example: taskfolk description: Workspace slug. required: true description: Workspace slug. name: slug in: path - schema: type: string example: 019e6f12-… description: Portfolio id. required: true description: Portfolio id. name: portfolioId in: path requestBody: content: application/json: schema: $ref: '#/components/schemas/PortfolioPatchInput' responses: '200': description: Success. content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Portfolio' required: - data '400': description: Validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: Missing / invalid API key. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: Key lacks the required scope. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: Resource not found (or cross-workspace — never leaked). content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: Rate limited. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' delete: summary: Delete a portfolio (soft). Its goals become company goals; project links are removed. tags: - Portfolios security: - bearerAuth: - portfolios:write parameters: - schema: type: string example: taskfolk description: Workspace slug. required: true description: Workspace slug. name: slug in: path - schema: type: string example: 019e6f12-… description: Portfolio id. required: true description: Portfolio id. name: portfolioId in: path responses: '200': description: Success. content: application/json: schema: type: object properties: data: type: object properties: id: type: string deleted: type: boolean enum: - true required: - id - deleted required: - data '400': description: Validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: Missing / invalid API key. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: Key lacks the required scope. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: Resource not found (or cross-workspace — never leaked). content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: Rate limited. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' /v1/workspaces/{slug}/portfolios/{portfolioId}/projects: get: summary: List a portfolio's member projects. tags: - Portfolios security: - bearerAuth: - portfolios:read parameters: - schema: type: string example: taskfolk description: Workspace slug. required: true description: Workspace slug. name: slug in: path - schema: type: string example: 019e6f12-… description: Portfolio id. required: true description: Portfolio id. name: portfolioId in: path responses: '200': description: Success. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/PortfolioProject' pagination: $ref: '#/components/schemas/Pagination' required: - data - pagination '400': description: Validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: Missing / invalid API key. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: Key lacks the required scope. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: Resource not found (or cross-workspace — never leaked). content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: Rate limited. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' post: summary: Add a project to a portfolio. Idempotent. tags: - Portfolios security: - bearerAuth: - portfolios:write parameters: - schema: type: string example: taskfolk description: Workspace slug. required: true description: Workspace slug. name: slug in: path - schema: type: string example: 019e6f12-… description: Portfolio id. required: true description: Portfolio id. name: portfolioId in: path requestBody: content: application/json: schema: $ref: '#/components/schemas/PortfolioProjectAddInput' responses: '201': description: Success. content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/PortfolioProject' required: - data '400': description: Validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: Missing / invalid API key. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: Key lacks the required scope. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: Resource not found (or cross-workspace — never leaked). content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: Rate limited. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' /v1/workspaces/{slug}/portfolios/{portfolioId}/projects/{projectId}: delete: summary: Remove a project from a portfolio. Idempotent. tags: - Portfolios security: - bearerAuth: - portfolios:write parameters: - schema: type: string example: taskfolk description: Workspace slug. required: true description: Workspace slug. name: slug in: path - schema: type: string example: 019e6f12-… description: Portfolio id. required: true description: Portfolio id. name: portfolioId in: path - schema: type: string example: 019e6f12-… description: Project id. required: true description: Project id. name: projectId in: path responses: '200': description: Success. content: application/json: schema: type: object properties: data: type: object properties: portfolio_id: type: string project_id: type: string removed: type: boolean enum: - true required: - portfolio_id - project_id - removed required: - data '400': description: Validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: Missing / invalid API key. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: Key lacks the required scope. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: Resource not found (or cross-workspace — never leaked). content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: Rate limited. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' components: schemas: Pagination: type: object properties: next_cursor: type: - string - 'null' example: null required: - next_cursor Portfolio: type: object properties: id: type: string workspace_id: type: string name: type: string example: Platform description_md: type: - string - 'null' color: type: string example: '#8b8f98' lead_user_id: type: - string - 'null' created_by: type: string created_at: type: string format: date-time example: '2026-05-28T14:30:00Z' updated_at: type: string format: date-time example: '2026-05-28T14:30:00Z' required: - id - workspace_id - name - description_md - color - lead_user_id - created_by - created_at - updated_at PortfolioProjectAddInput: type: object properties: project_id: type: string minLength: 1 required: - project_id PortfolioPatchInput: type: object properties: name: type: string minLength: 1 maxLength: 120 description_md: type: - string - 'null' maxLength: 10000 color: type: string pattern: ^#[0-9a-fA-F]{6}$ lead_user_id: type: - string - 'null' ErrorEnvelope: type: object properties: error: type: object properties: code: type: string enum: - unauthorized - forbidden - not_found - validation - rate_limited - conflict - idempotency_violation - internal_error example: validation message: type: string example: Label "foo" does not exist on this project. details: type: object additionalProperties: {} required: - code - message required: - error PortfolioCreateInput: type: object properties: name: type: string minLength: 1 maxLength: 120 description_md: type: - string - 'null' maxLength: 10000 color: type: string pattern: ^#[0-9a-fA-F]{6}$ example: '#8b8f98' lead_user_id: type: - string - 'null' required: - name PortfolioProject: type: object properties: portfolio_id: type: string project_id: type: string required: - portfolio_id - project_id securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: API key description: 'Workspace API key. Send as `Authorization: Bearer tfk_live_…`. Each key is bound to exactly one workspace and carries a set of scopes.'