diff --git a/docs/api-reference/assets/update-user-asset.md b/docs/api-reference/v1/assets/update-user-asset.md
similarity index 100%
rename from docs/api-reference/assets/update-user-asset.md
rename to docs/api-reference/v1/assets/update-user-asset.md
diff --git a/docs/api-reference/assets/update-workspace-asset.md b/docs/api-reference/v1/assets/update-workspace-asset.md
similarity index 100%
rename from docs/api-reference/assets/update-workspace-asset.md
rename to docs/api-reference/v1/assets/update-workspace-asset.md
diff --git a/docs/api-reference/customer/add-customer-property.md b/docs/api-reference/v1/customer/add-customer-property.md
similarity index 100%
rename from docs/api-reference/customer/add-customer-property.md
rename to docs/api-reference/v1/customer/add-customer-property.md
diff --git a/docs/api-reference/customer/add-customer-request.md b/docs/api-reference/v1/customer/add-customer-request.md
similarity index 100%
rename from docs/api-reference/customer/add-customer-request.md
rename to docs/api-reference/v1/customer/add-customer-request.md
diff --git a/docs/api-reference/customer/add-customer.md b/docs/api-reference/v1/customer/add-customer.md
similarity index 100%
rename from docs/api-reference/customer/add-customer.md
rename to docs/api-reference/v1/customer/add-customer.md
diff --git a/docs/api-reference/customer/delete-customer-property.md b/docs/api-reference/v1/customer/delete-customer-property.md
similarity index 100%
rename from docs/api-reference/customer/delete-customer-property.md
rename to docs/api-reference/v1/customer/delete-customer-property.md
diff --git a/docs/api-reference/customer/delete-customer-request.md b/docs/api-reference/v1/customer/delete-customer-request.md
similarity index 100%
rename from docs/api-reference/customer/delete-customer-request.md
rename to docs/api-reference/v1/customer/delete-customer-request.md
diff --git a/docs/api-reference/customer/delete-customer.md b/docs/api-reference/v1/customer/delete-customer.md
similarity index 100%
rename from docs/api-reference/customer/delete-customer.md
rename to docs/api-reference/v1/customer/delete-customer.md
diff --git a/docs/api-reference/customer/get-customer-detail.md b/docs/api-reference/v1/customer/get-customer-detail.md
similarity index 100%
rename from docs/api-reference/customer/get-customer-detail.md
rename to docs/api-reference/v1/customer/get-customer-detail.md
diff --git a/docs/api-reference/customer/get-customer-property-detail.md b/docs/api-reference/v1/customer/get-customer-property-detail.md
similarity index 100%
rename from docs/api-reference/customer/get-customer-property-detail.md
rename to docs/api-reference/v1/customer/get-customer-property-detail.md
diff --git a/docs/api-reference/customer/get-customer-property-value.md b/docs/api-reference/v1/customer/get-customer-property-value.md
similarity index 100%
rename from docs/api-reference/customer/get-customer-property-value.md
rename to docs/api-reference/v1/customer/get-customer-property-value.md
diff --git a/docs/api-reference/customer/get-customer-request-detail.md b/docs/api-reference/v1/customer/get-customer-request-detail.md
similarity index 100%
rename from docs/api-reference/customer/get-customer-request-detail.md
rename to docs/api-reference/v1/customer/get-customer-request-detail.md
diff --git a/docs/api-reference/customer/link-work-items-to-customer.md b/docs/api-reference/v1/customer/link-work-items-to-customer.md
similarity index 100%
rename from docs/api-reference/customer/link-work-items-to-customer.md
rename to docs/api-reference/v1/customer/link-work-items-to-customer.md
diff --git a/docs/api-reference/customer/list-customer-properties.md b/docs/api-reference/v1/customer/list-customer-properties.md
similarity index 100%
rename from docs/api-reference/customer/list-customer-properties.md
rename to docs/api-reference/v1/customer/list-customer-properties.md
diff --git a/docs/api-reference/customer/list-customer-property-values.md b/docs/api-reference/v1/customer/list-customer-property-values.md
similarity index 100%
rename from docs/api-reference/customer/list-customer-property-values.md
rename to docs/api-reference/v1/customer/list-customer-property-values.md
diff --git a/docs/api-reference/customer/list-customer-requests.md b/docs/api-reference/v1/customer/list-customer-requests.md
similarity index 100%
rename from docs/api-reference/customer/list-customer-requests.md
rename to docs/api-reference/v1/customer/list-customer-requests.md
diff --git a/docs/api-reference/customer/list-customer-work-items.md b/docs/api-reference/v1/customer/list-customer-work-items.md
similarity index 100%
rename from docs/api-reference/customer/list-customer-work-items.md
rename to docs/api-reference/v1/customer/list-customer-work-items.md
diff --git a/docs/api-reference/customer/list-customers.md b/docs/api-reference/v1/customer/list-customers.md
similarity index 100%
rename from docs/api-reference/customer/list-customers.md
rename to docs/api-reference/v1/customer/list-customers.md
diff --git a/docs/api-reference/customer/overview.md b/docs/api-reference/v1/customer/overview.md
similarity index 100%
rename from docs/api-reference/customer/overview.md
rename to docs/api-reference/v1/customer/overview.md
diff --git a/docs/api-reference/customer/unlink-work-item-from-customer.md b/docs/api-reference/v1/customer/unlink-work-item-from-customer.md
similarity index 100%
rename from docs/api-reference/customer/unlink-work-item-from-customer.md
rename to docs/api-reference/v1/customer/unlink-work-item-from-customer.md
diff --git a/docs/api-reference/customer/update-customer-detail.md b/docs/api-reference/v1/customer/update-customer-detail.md
similarity index 100%
rename from docs/api-reference/customer/update-customer-detail.md
rename to docs/api-reference/v1/customer/update-customer-detail.md
diff --git a/docs/api-reference/customer/update-customer-property-detail.md b/docs/api-reference/v1/customer/update-customer-property-detail.md
similarity index 100%
rename from docs/api-reference/customer/update-customer-property-detail.md
rename to docs/api-reference/v1/customer/update-customer-property-detail.md
diff --git a/docs/api-reference/customer/update-customer-property-value.md b/docs/api-reference/v1/customer/update-customer-property-value.md
similarity index 100%
rename from docs/api-reference/customer/update-customer-property-value.md
rename to docs/api-reference/v1/customer/update-customer-property-value.md
diff --git a/docs/api-reference/customer/update-customer-request-detail.md b/docs/api-reference/v1/customer/update-customer-request-detail.md
similarity index 100%
rename from docs/api-reference/customer/update-customer-request-detail.md
rename to docs/api-reference/v1/customer/update-customer-request-detail.md
diff --git a/docs/api-reference/cycle/add-cycle-work-items.md b/docs/api-reference/v1/cycle/add-cycle-work-items.md
similarity index 100%
rename from docs/api-reference/cycle/add-cycle-work-items.md
rename to docs/api-reference/v1/cycle/add-cycle-work-items.md
diff --git a/docs/api-reference/cycle/add-cycle.md b/docs/api-reference/v1/cycle/add-cycle.md
similarity index 100%
rename from docs/api-reference/cycle/add-cycle.md
rename to docs/api-reference/v1/cycle/add-cycle.md
diff --git a/docs/api-reference/cycle/archive-cycle.md b/docs/api-reference/v1/cycle/archive-cycle.md
similarity index 100%
rename from docs/api-reference/cycle/archive-cycle.md
rename to docs/api-reference/v1/cycle/archive-cycle.md
diff --git a/docs/api-reference/cycle/delete-cycle.md b/docs/api-reference/v1/cycle/delete-cycle.md
similarity index 100%
rename from docs/api-reference/cycle/delete-cycle.md
rename to docs/api-reference/v1/cycle/delete-cycle.md
diff --git a/docs/api-reference/cycle/get-cycle-detail.md b/docs/api-reference/v1/cycle/get-cycle-detail.md
similarity index 100%
rename from docs/api-reference/cycle/get-cycle-detail.md
rename to docs/api-reference/v1/cycle/get-cycle-detail.md
diff --git a/docs/api-reference/cycle/list-archived-cycles.md b/docs/api-reference/v1/cycle/list-archived-cycles.md
similarity index 100%
rename from docs/api-reference/cycle/list-archived-cycles.md
rename to docs/api-reference/v1/cycle/list-archived-cycles.md
diff --git a/docs/api-reference/cycle/list-cycle-work-items.md b/docs/api-reference/v1/cycle/list-cycle-work-items.md
similarity index 100%
rename from docs/api-reference/cycle/list-cycle-work-items.md
rename to docs/api-reference/v1/cycle/list-cycle-work-items.md
diff --git a/docs/api-reference/cycle/list-cycles.md b/docs/api-reference/v1/cycle/list-cycles.md
similarity index 100%
rename from docs/api-reference/cycle/list-cycles.md
rename to docs/api-reference/v1/cycle/list-cycles.md
diff --git a/docs/api-reference/cycle/overview.md b/docs/api-reference/v1/cycle/overview.md
similarity index 100%
rename from docs/api-reference/cycle/overview.md
rename to docs/api-reference/v1/cycle/overview.md
diff --git a/docs/api-reference/cycle/remove-cycle-work-item.md b/docs/api-reference/v1/cycle/remove-cycle-work-item.md
similarity index 100%
rename from docs/api-reference/cycle/remove-cycle-work-item.md
rename to docs/api-reference/v1/cycle/remove-cycle-work-item.md
diff --git a/docs/api-reference/cycle/transfer-cycle-work-items.md b/docs/api-reference/v1/cycle/transfer-cycle-work-items.md
similarity index 100%
rename from docs/api-reference/cycle/transfer-cycle-work-items.md
rename to docs/api-reference/v1/cycle/transfer-cycle-work-items.md
diff --git a/docs/api-reference/cycle/unarchive-cycle.md b/docs/api-reference/v1/cycle/unarchive-cycle.md
similarity index 100%
rename from docs/api-reference/cycle/unarchive-cycle.md
rename to docs/api-reference/v1/cycle/unarchive-cycle.md
diff --git a/docs/api-reference/cycle/update-cycle-detail.md b/docs/api-reference/v1/cycle/update-cycle-detail.md
similarity index 100%
rename from docs/api-reference/cycle/update-cycle-detail.md
rename to docs/api-reference/v1/cycle/update-cycle-detail.md
diff --git a/docs/api-reference/epics/add-epic-work-items.md b/docs/api-reference/v1/epics/add-epic-work-items.md
similarity index 100%
rename from docs/api-reference/epics/add-epic-work-items.md
rename to docs/api-reference/v1/epics/add-epic-work-items.md
diff --git a/docs/api-reference/epics/create-epic.md b/docs/api-reference/v1/epics/create-epic.md
similarity index 100%
rename from docs/api-reference/epics/create-epic.md
rename to docs/api-reference/v1/epics/create-epic.md
diff --git a/docs/api-reference/epics/delete-epic.md b/docs/api-reference/v1/epics/delete-epic.md
similarity index 100%
rename from docs/api-reference/epics/delete-epic.md
rename to docs/api-reference/v1/epics/delete-epic.md
diff --git a/docs/api-reference/epics/get-epic-detail.md b/docs/api-reference/v1/epics/get-epic-detail.md
similarity index 100%
rename from docs/api-reference/epics/get-epic-detail.md
rename to docs/api-reference/v1/epics/get-epic-detail.md
diff --git a/docs/api-reference/epics/list-epic-work-items.md b/docs/api-reference/v1/epics/list-epic-work-items.md
similarity index 100%
rename from docs/api-reference/epics/list-epic-work-items.md
rename to docs/api-reference/v1/epics/list-epic-work-items.md
diff --git a/docs/api-reference/epics/list-epics.md b/docs/api-reference/v1/epics/list-epics.md
similarity index 100%
rename from docs/api-reference/epics/list-epics.md
rename to docs/api-reference/v1/epics/list-epics.md
diff --git a/docs/api-reference/epics/overview.md b/docs/api-reference/v1/epics/overview.md
similarity index 100%
rename from docs/api-reference/epics/overview.md
rename to docs/api-reference/v1/epics/overview.md
diff --git a/docs/api-reference/epics/update-epic.md b/docs/api-reference/v1/epics/update-epic.md
similarity index 100%
rename from docs/api-reference/epics/update-epic.md
rename to docs/api-reference/v1/epics/update-epic.md
diff --git a/docs/api-reference/estimate/add-estimate-points.md b/docs/api-reference/v1/estimate/add-estimate-points.md
similarity index 100%
rename from docs/api-reference/estimate/add-estimate-points.md
rename to docs/api-reference/v1/estimate/add-estimate-points.md
diff --git a/docs/api-reference/estimate/add-estimate.md b/docs/api-reference/v1/estimate/add-estimate.md
similarity index 100%
rename from docs/api-reference/estimate/add-estimate.md
rename to docs/api-reference/v1/estimate/add-estimate.md
diff --git a/docs/api-reference/estimate/delete-estimate-point.md b/docs/api-reference/v1/estimate/delete-estimate-point.md
similarity index 100%
rename from docs/api-reference/estimate/delete-estimate-point.md
rename to docs/api-reference/v1/estimate/delete-estimate-point.md
diff --git a/docs/api-reference/estimate/delete-estimate.md b/docs/api-reference/v1/estimate/delete-estimate.md
similarity index 100%
rename from docs/api-reference/estimate/delete-estimate.md
rename to docs/api-reference/v1/estimate/delete-estimate.md
diff --git a/docs/api-reference/estimate/get-estimate.md b/docs/api-reference/v1/estimate/get-estimate.md
similarity index 100%
rename from docs/api-reference/estimate/get-estimate.md
rename to docs/api-reference/v1/estimate/get-estimate.md
diff --git a/docs/api-reference/estimate/list-estimate-points.md b/docs/api-reference/v1/estimate/list-estimate-points.md
similarity index 100%
rename from docs/api-reference/estimate/list-estimate-points.md
rename to docs/api-reference/v1/estimate/list-estimate-points.md
diff --git a/docs/api-reference/estimate/overview.md b/docs/api-reference/v1/estimate/overview.md
similarity index 100%
rename from docs/api-reference/estimate/overview.md
rename to docs/api-reference/v1/estimate/overview.md
diff --git a/docs/api-reference/estimate/update-estimate-point.md b/docs/api-reference/v1/estimate/update-estimate-point.md
similarity index 100%
rename from docs/api-reference/estimate/update-estimate-point.md
rename to docs/api-reference/v1/estimate/update-estimate-point.md
diff --git a/docs/api-reference/estimate/update-estimate.md b/docs/api-reference/v1/estimate/update-estimate.md
similarity index 100%
rename from docs/api-reference/estimate/update-estimate.md
rename to docs/api-reference/v1/estimate/update-estimate.md
diff --git a/docs/api-reference/idp-group-sync/create-project-mapping.md b/docs/api-reference/v1/idp-group-sync/create-project-mapping.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/create-project-mapping.md
rename to docs/api-reference/v1/idp-group-sync/create-project-mapping.md
diff --git a/docs/api-reference/idp-group-sync/create-workspace-mapping.md b/docs/api-reference/v1/idp-group-sync/create-workspace-mapping.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/create-workspace-mapping.md
rename to docs/api-reference/v1/idp-group-sync/create-workspace-mapping.md
diff --git a/docs/api-reference/idp-group-sync/delete-project-mapping.md b/docs/api-reference/v1/idp-group-sync/delete-project-mapping.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/delete-project-mapping.md
rename to docs/api-reference/v1/idp-group-sync/delete-project-mapping.md
diff --git a/docs/api-reference/idp-group-sync/delete-workspace-mapping.md b/docs/api-reference/v1/idp-group-sync/delete-workspace-mapping.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/delete-workspace-mapping.md
rename to docs/api-reference/v1/idp-group-sync/delete-workspace-mapping.md
diff --git a/docs/api-reference/idp-group-sync/get-group-sync-config.md b/docs/api-reference/v1/idp-group-sync/get-group-sync-config.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/get-group-sync-config.md
rename to docs/api-reference/v1/idp-group-sync/get-group-sync-config.md
diff --git a/docs/api-reference/idp-group-sync/get-project-mapping.md b/docs/api-reference/v1/idp-group-sync/get-project-mapping.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/get-project-mapping.md
rename to docs/api-reference/v1/idp-group-sync/get-project-mapping.md
diff --git a/docs/api-reference/idp-group-sync/get-workspace-mapping.md b/docs/api-reference/v1/idp-group-sync/get-workspace-mapping.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/get-workspace-mapping.md
rename to docs/api-reference/v1/idp-group-sync/get-workspace-mapping.md
diff --git a/docs/api-reference/idp-group-sync/list-project-mappings.md b/docs/api-reference/v1/idp-group-sync/list-project-mappings.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/list-project-mappings.md
rename to docs/api-reference/v1/idp-group-sync/list-project-mappings.md
diff --git a/docs/api-reference/idp-group-sync/list-workspace-mappings.md b/docs/api-reference/v1/idp-group-sync/list-workspace-mappings.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/list-workspace-mappings.md
rename to docs/api-reference/v1/idp-group-sync/list-workspace-mappings.md
diff --git a/docs/api-reference/idp-group-sync/overview.md b/docs/api-reference/v1/idp-group-sync/overview.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/overview.md
rename to docs/api-reference/v1/idp-group-sync/overview.md
diff --git a/docs/api-reference/idp-group-sync/update-group-sync-config.md b/docs/api-reference/v1/idp-group-sync/update-group-sync-config.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/update-group-sync-config.md
rename to docs/api-reference/v1/idp-group-sync/update-group-sync-config.md
diff --git a/docs/api-reference/idp-group-sync/update-project-mapping.md b/docs/api-reference/v1/idp-group-sync/update-project-mapping.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/update-project-mapping.md
rename to docs/api-reference/v1/idp-group-sync/update-project-mapping.md
diff --git a/docs/api-reference/idp-group-sync/update-workspace-mapping.md b/docs/api-reference/v1/idp-group-sync/update-workspace-mapping.md
similarity index 100%
rename from docs/api-reference/idp-group-sync/update-workspace-mapping.md
rename to docs/api-reference/v1/idp-group-sync/update-workspace-mapping.md
diff --git a/docs/api-reference/inbox-issue/add-inbox-issue.md b/docs/api-reference/v1/inbox-issue/add-inbox-issue.md
similarity index 100%
rename from docs/api-reference/inbox-issue/add-inbox-issue.md
rename to docs/api-reference/v1/inbox-issue/add-inbox-issue.md
diff --git a/docs/api-reference/inbox-issue/delete-inbox-issue.md b/docs/api-reference/v1/inbox-issue/delete-inbox-issue.md
similarity index 100%
rename from docs/api-reference/inbox-issue/delete-inbox-issue.md
rename to docs/api-reference/v1/inbox-issue/delete-inbox-issue.md
diff --git a/docs/api-reference/inbox-issue/get-inbox-issue-detail.md b/docs/api-reference/v1/inbox-issue/get-inbox-issue-detail.md
similarity index 100%
rename from docs/api-reference/inbox-issue/get-inbox-issue-detail.md
rename to docs/api-reference/v1/inbox-issue/get-inbox-issue-detail.md
diff --git a/docs/api-reference/inbox-issue/list-inbox-issues.md b/docs/api-reference/v1/inbox-issue/list-inbox-issues.md
similarity index 100%
rename from docs/api-reference/inbox-issue/list-inbox-issues.md
rename to docs/api-reference/v1/inbox-issue/list-inbox-issues.md
diff --git a/docs/api-reference/inbox-issue/overview.md b/docs/api-reference/v1/inbox-issue/overview.md
similarity index 100%
rename from docs/api-reference/inbox-issue/overview.md
rename to docs/api-reference/v1/inbox-issue/overview.md
diff --git a/docs/api-reference/inbox-issue/update-inbox-issue-detail.md b/docs/api-reference/v1/inbox-issue/update-inbox-issue-detail.md
similarity index 100%
rename from docs/api-reference/inbox-issue/update-inbox-issue-detail.md
rename to docs/api-reference/v1/inbox-issue/update-inbox-issue-detail.md
diff --git a/docs/api-reference/initiative/add-epics-to-initiative.md b/docs/api-reference/v1/initiative/add-epics-to-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/add-epics-to-initiative.md
rename to docs/api-reference/v1/initiative/add-epics-to-initiative.md
diff --git a/docs/api-reference/initiative/add-initiative-label.md b/docs/api-reference/v1/initiative/add-initiative-label.md
similarity index 100%
rename from docs/api-reference/initiative/add-initiative-label.md
rename to docs/api-reference/v1/initiative/add-initiative-label.md
diff --git a/docs/api-reference/initiative/add-initiative.md b/docs/api-reference/v1/initiative/add-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/add-initiative.md
rename to docs/api-reference/v1/initiative/add-initiative.md
diff --git a/docs/api-reference/initiative/add-labels-to-initiative.md b/docs/api-reference/v1/initiative/add-labels-to-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/add-labels-to-initiative.md
rename to docs/api-reference/v1/initiative/add-labels-to-initiative.md
diff --git a/docs/api-reference/initiative/add-projects-to-initiative.md b/docs/api-reference/v1/initiative/add-projects-to-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/add-projects-to-initiative.md
rename to docs/api-reference/v1/initiative/add-projects-to-initiative.md
diff --git a/docs/api-reference/initiative/delete-initiative-label.md b/docs/api-reference/v1/initiative/delete-initiative-label.md
similarity index 100%
rename from docs/api-reference/initiative/delete-initiative-label.md
rename to docs/api-reference/v1/initiative/delete-initiative-label.md
diff --git a/docs/api-reference/initiative/delete-initiative.md b/docs/api-reference/v1/initiative/delete-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/delete-initiative.md
rename to docs/api-reference/v1/initiative/delete-initiative.md
diff --git a/docs/api-reference/initiative/get-initiative-detail.md b/docs/api-reference/v1/initiative/get-initiative-detail.md
similarity index 100%
rename from docs/api-reference/initiative/get-initiative-detail.md
rename to docs/api-reference/v1/initiative/get-initiative-detail.md
diff --git a/docs/api-reference/initiative/get-initiative-label-detail.md b/docs/api-reference/v1/initiative/get-initiative-label-detail.md
similarity index 100%
rename from docs/api-reference/initiative/get-initiative-label-detail.md
rename to docs/api-reference/v1/initiative/get-initiative-label-detail.md
diff --git a/docs/api-reference/initiative/list-initiative-epics.md b/docs/api-reference/v1/initiative/list-initiative-epics.md
similarity index 100%
rename from docs/api-reference/initiative/list-initiative-epics.md
rename to docs/api-reference/v1/initiative/list-initiative-epics.md
diff --git a/docs/api-reference/initiative/list-initiative-labels-for-initiative.md b/docs/api-reference/v1/initiative/list-initiative-labels-for-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/list-initiative-labels-for-initiative.md
rename to docs/api-reference/v1/initiative/list-initiative-labels-for-initiative.md
diff --git a/docs/api-reference/initiative/list-initiative-labels.md b/docs/api-reference/v1/initiative/list-initiative-labels.md
similarity index 100%
rename from docs/api-reference/initiative/list-initiative-labels.md
rename to docs/api-reference/v1/initiative/list-initiative-labels.md
diff --git a/docs/api-reference/initiative/list-initiative-projects.md b/docs/api-reference/v1/initiative/list-initiative-projects.md
similarity index 100%
rename from docs/api-reference/initiative/list-initiative-projects.md
rename to docs/api-reference/v1/initiative/list-initiative-projects.md
diff --git a/docs/api-reference/initiative/list-initiatives.md b/docs/api-reference/v1/initiative/list-initiatives.md
similarity index 100%
rename from docs/api-reference/initiative/list-initiatives.md
rename to docs/api-reference/v1/initiative/list-initiatives.md
diff --git a/docs/api-reference/initiative/overview.md b/docs/api-reference/v1/initiative/overview.md
similarity index 100%
rename from docs/api-reference/initiative/overview.md
rename to docs/api-reference/v1/initiative/overview.md
diff --git a/docs/api-reference/initiative/remove-epics-from-initiative.md b/docs/api-reference/v1/initiative/remove-epics-from-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/remove-epics-from-initiative.md
rename to docs/api-reference/v1/initiative/remove-epics-from-initiative.md
diff --git a/docs/api-reference/initiative/remove-labels-from-initiative.md b/docs/api-reference/v1/initiative/remove-labels-from-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/remove-labels-from-initiative.md
rename to docs/api-reference/v1/initiative/remove-labels-from-initiative.md
diff --git a/docs/api-reference/initiative/remove-projects-from-initiative.md b/docs/api-reference/v1/initiative/remove-projects-from-initiative.md
similarity index 100%
rename from docs/api-reference/initiative/remove-projects-from-initiative.md
rename to docs/api-reference/v1/initiative/remove-projects-from-initiative.md
diff --git a/docs/api-reference/initiative/update-initiative-detail.md b/docs/api-reference/v1/initiative/update-initiative-detail.md
similarity index 100%
rename from docs/api-reference/initiative/update-initiative-detail.md
rename to docs/api-reference/v1/initiative/update-initiative-detail.md
diff --git a/docs/api-reference/initiative/update-initiative-label-detail.md b/docs/api-reference/v1/initiative/update-initiative-label-detail.md
similarity index 100%
rename from docs/api-reference/initiative/update-initiative-label-detail.md
rename to docs/api-reference/v1/initiative/update-initiative-label-detail.md
diff --git a/docs/api-reference/intake-issue/add-intake-issue.md b/docs/api-reference/v1/intake-issue/add-intake-issue.md
similarity index 100%
rename from docs/api-reference/intake-issue/add-intake-issue.md
rename to docs/api-reference/v1/intake-issue/add-intake-issue.md
diff --git a/docs/api-reference/intake-issue/delete-intake-issue.md b/docs/api-reference/v1/intake-issue/delete-intake-issue.md
similarity index 100%
rename from docs/api-reference/intake-issue/delete-intake-issue.md
rename to docs/api-reference/v1/intake-issue/delete-intake-issue.md
diff --git a/docs/api-reference/intake-issue/get-intake-issue-detail.md b/docs/api-reference/v1/intake-issue/get-intake-issue-detail.md
similarity index 100%
rename from docs/api-reference/intake-issue/get-intake-issue-detail.md
rename to docs/api-reference/v1/intake-issue/get-intake-issue-detail.md
diff --git a/docs/api-reference/intake-issue/list-intake-issues.md b/docs/api-reference/v1/intake-issue/list-intake-issues.md
similarity index 100%
rename from docs/api-reference/intake-issue/list-intake-issues.md
rename to docs/api-reference/v1/intake-issue/list-intake-issues.md
diff --git a/docs/api-reference/intake-issue/overview.md b/docs/api-reference/v1/intake-issue/overview.md
similarity index 100%
rename from docs/api-reference/intake-issue/overview.md
rename to docs/api-reference/v1/intake-issue/overview.md
diff --git a/docs/api-reference/intake-issue/update-intake-issue-detail.md b/docs/api-reference/v1/intake-issue/update-intake-issue-detail.md
similarity index 100%
rename from docs/api-reference/intake-issue/update-intake-issue-detail.md
rename to docs/api-reference/v1/intake-issue/update-intake-issue-detail.md
diff --git a/docs/api-reference/introduction.md b/docs/api-reference/v1/introduction.md
similarity index 100%
rename from docs/api-reference/introduction.md
rename to docs/api-reference/v1/introduction.md
diff --git a/docs/api-reference/issue-activity/get-issue-activity-detail.md b/docs/api-reference/v1/issue-activity/get-issue-activity-detail.md
similarity index 100%
rename from docs/api-reference/issue-activity/get-issue-activity-detail.md
rename to docs/api-reference/v1/issue-activity/get-issue-activity-detail.md
diff --git a/docs/api-reference/issue-activity/list-issue-activities.md b/docs/api-reference/v1/issue-activity/list-issue-activities.md
similarity index 100%
rename from docs/api-reference/issue-activity/list-issue-activities.md
rename to docs/api-reference/v1/issue-activity/list-issue-activities.md
diff --git a/docs/api-reference/issue-activity/overview.md b/docs/api-reference/v1/issue-activity/overview.md
similarity index 100%
rename from docs/api-reference/issue-activity/overview.md
rename to docs/api-reference/v1/issue-activity/overview.md
diff --git a/docs/api-reference/issue-attachments/complete-upload.md b/docs/api-reference/v1/issue-attachments/complete-upload.md
similarity index 100%
rename from docs/api-reference/issue-attachments/complete-upload.md
rename to docs/api-reference/v1/issue-attachments/complete-upload.md
diff --git a/docs/api-reference/issue-attachments/delete-attachment.md b/docs/api-reference/v1/issue-attachments/delete-attachment.md
similarity index 100%
rename from docs/api-reference/issue-attachments/delete-attachment.md
rename to docs/api-reference/v1/issue-attachments/delete-attachment.md
diff --git a/docs/api-reference/issue-attachments/get-attachment-detail.md b/docs/api-reference/v1/issue-attachments/get-attachment-detail.md
similarity index 100%
rename from docs/api-reference/issue-attachments/get-attachment-detail.md
rename to docs/api-reference/v1/issue-attachments/get-attachment-detail.md
diff --git a/docs/api-reference/issue-attachments/get-attachments.md b/docs/api-reference/v1/issue-attachments/get-attachments.md
similarity index 100%
rename from docs/api-reference/issue-attachments/get-attachments.md
rename to docs/api-reference/v1/issue-attachments/get-attachments.md
diff --git a/docs/api-reference/issue-attachments/get-upload-credentials.md b/docs/api-reference/v1/issue-attachments/get-upload-credentials.md
similarity index 100%
rename from docs/api-reference/issue-attachments/get-upload-credentials.md
rename to docs/api-reference/v1/issue-attachments/get-upload-credentials.md
diff --git a/docs/api-reference/issue-attachments/overview.md b/docs/api-reference/v1/issue-attachments/overview.md
similarity index 93%
rename from docs/api-reference/issue-attachments/overview.md
rename to docs/api-reference/v1/issue-attachments/overview.md
index f32a0196..3b2f1241 100644
--- a/docs/api-reference/issue-attachments/overview.md
+++ b/docs/api-reference/v1/issue-attachments/overview.md
@@ -12,9 +12,9 @@ Allows you to manage file attachments associated with work items and Intake work
## Upload process
-1. Get the [upload credentials](/api-reference/issue-attachments/get-upload-credentials).
-2. [Upload the file](/api-reference/issue-attachments/upload-file) to storage.
-3. [Complete attachment upload](/api-reference/issue-attachments/complete-upload) to notify server.
+1. Get the [upload credentials](/api-reference/v1/issue-attachments/get-upload-credentials).
+2. [Upload the file](/api-reference/v1/issue-attachments/upload-file) to storage.
+3. [Complete attachment upload](/api-reference/v1/issue-attachments/complete-upload) to notify server.
diff --git a/docs/api-reference/issue-attachments/update-attachment.md b/docs/api-reference/v1/issue-attachments/update-attachment.md
similarity index 100%
rename from docs/api-reference/issue-attachments/update-attachment.md
rename to docs/api-reference/v1/issue-attachments/update-attachment.md
diff --git a/docs/api-reference/issue-attachments/upload-file.md b/docs/api-reference/v1/issue-attachments/upload-file.md
similarity index 100%
rename from docs/api-reference/issue-attachments/upload-file.md
rename to docs/api-reference/v1/issue-attachments/upload-file.md
diff --git a/docs/api-reference/issue-comment/add-issue-comment.md b/docs/api-reference/v1/issue-comment/add-issue-comment.md
similarity index 100%
rename from docs/api-reference/issue-comment/add-issue-comment.md
rename to docs/api-reference/v1/issue-comment/add-issue-comment.md
diff --git a/docs/api-reference/issue-comment/delete-issue-comment.md b/docs/api-reference/v1/issue-comment/delete-issue-comment.md
similarity index 100%
rename from docs/api-reference/issue-comment/delete-issue-comment.md
rename to docs/api-reference/v1/issue-comment/delete-issue-comment.md
diff --git a/docs/api-reference/issue-comment/get-issue-comment-detail.md b/docs/api-reference/v1/issue-comment/get-issue-comment-detail.md
similarity index 100%
rename from docs/api-reference/issue-comment/get-issue-comment-detail.md
rename to docs/api-reference/v1/issue-comment/get-issue-comment-detail.md
diff --git a/docs/api-reference/issue-comment/list-issue-comments.md b/docs/api-reference/v1/issue-comment/list-issue-comments.md
similarity index 100%
rename from docs/api-reference/issue-comment/list-issue-comments.md
rename to docs/api-reference/v1/issue-comment/list-issue-comments.md
diff --git a/docs/api-reference/issue-comment/overview.md b/docs/api-reference/v1/issue-comment/overview.md
similarity index 100%
rename from docs/api-reference/issue-comment/overview.md
rename to docs/api-reference/v1/issue-comment/overview.md
diff --git a/docs/api-reference/issue-comment/update-issue-comment-detail.md b/docs/api-reference/v1/issue-comment/update-issue-comment-detail.md
similarity index 100%
rename from docs/api-reference/issue-comment/update-issue-comment-detail.md
rename to docs/api-reference/v1/issue-comment/update-issue-comment-detail.md
diff --git a/docs/api-reference/issue-types/options/add-dropdown-options.md b/docs/api-reference/v1/issue-types/options/add-dropdown-options.md
similarity index 100%
rename from docs/api-reference/issue-types/options/add-dropdown-options.md
rename to docs/api-reference/v1/issue-types/options/add-dropdown-options.md
diff --git a/docs/api-reference/issue-types/options/delete-dropdown-options.md b/docs/api-reference/v1/issue-types/options/delete-dropdown-options.md
similarity index 100%
rename from docs/api-reference/issue-types/options/delete-dropdown-options.md
rename to docs/api-reference/v1/issue-types/options/delete-dropdown-options.md
diff --git a/docs/api-reference/issue-types/options/get-option-details.md b/docs/api-reference/v1/issue-types/options/get-option-details.md
similarity index 100%
rename from docs/api-reference/issue-types/options/get-option-details.md
rename to docs/api-reference/v1/issue-types/options/get-option-details.md
diff --git a/docs/api-reference/issue-types/options/list-dropdown-options.md b/docs/api-reference/v1/issue-types/options/list-dropdown-options.md
similarity index 100%
rename from docs/api-reference/issue-types/options/list-dropdown-options.md
rename to docs/api-reference/v1/issue-types/options/list-dropdown-options.md
diff --git a/docs/api-reference/issue-types/options/overview.md b/docs/api-reference/v1/issue-types/options/overview.md
similarity index 100%
rename from docs/api-reference/issue-types/options/overview.md
rename to docs/api-reference/v1/issue-types/options/overview.md
diff --git a/docs/api-reference/issue-types/options/update-dropdown-options.md b/docs/api-reference/v1/issue-types/options/update-dropdown-options.md
similarity index 100%
rename from docs/api-reference/issue-types/options/update-dropdown-options.md
rename to docs/api-reference/v1/issue-types/options/update-dropdown-options.md
diff --git a/docs/api-reference/issue-types/properties/add-property.md b/docs/api-reference/v1/issue-types/properties/add-property.md
similarity index 100%
rename from docs/api-reference/issue-types/properties/add-property.md
rename to docs/api-reference/v1/issue-types/properties/add-property.md
diff --git a/docs/api-reference/issue-types/properties/delete-property.md b/docs/api-reference/v1/issue-types/properties/delete-property.md
similarity index 100%
rename from docs/api-reference/issue-types/properties/delete-property.md
rename to docs/api-reference/v1/issue-types/properties/delete-property.md
diff --git a/docs/api-reference/issue-types/properties/get-property-details.md b/docs/api-reference/v1/issue-types/properties/get-property-details.md
similarity index 100%
rename from docs/api-reference/issue-types/properties/get-property-details.md
rename to docs/api-reference/v1/issue-types/properties/get-property-details.md
diff --git a/docs/api-reference/issue-types/properties/list-properties.md b/docs/api-reference/v1/issue-types/properties/list-properties.md
similarity index 100%
rename from docs/api-reference/issue-types/properties/list-properties.md
rename to docs/api-reference/v1/issue-types/properties/list-properties.md
diff --git a/docs/api-reference/issue-types/properties/overview.md b/docs/api-reference/v1/issue-types/properties/overview.md
similarity index 100%
rename from docs/api-reference/issue-types/properties/overview.md
rename to docs/api-reference/v1/issue-types/properties/overview.md
diff --git a/docs/api-reference/issue-types/properties/update-property.md b/docs/api-reference/v1/issue-types/properties/update-property.md
similarity index 100%
rename from docs/api-reference/issue-types/properties/update-property.md
rename to docs/api-reference/v1/issue-types/properties/update-property.md
diff --git a/docs/api-reference/issue-types/types/add-issue-type.md b/docs/api-reference/v1/issue-types/types/add-issue-type.md
similarity index 100%
rename from docs/api-reference/issue-types/types/add-issue-type.md
rename to docs/api-reference/v1/issue-types/types/add-issue-type.md
diff --git a/docs/api-reference/issue-types/types/delete-issue-type.md b/docs/api-reference/v1/issue-types/types/delete-issue-type.md
similarity index 100%
rename from docs/api-reference/issue-types/types/delete-issue-type.md
rename to docs/api-reference/v1/issue-types/types/delete-issue-type.md
diff --git a/docs/api-reference/issue-types/types/get-issue-type-details.md b/docs/api-reference/v1/issue-types/types/get-issue-type-details.md
similarity index 100%
rename from docs/api-reference/issue-types/types/get-issue-type-details.md
rename to docs/api-reference/v1/issue-types/types/get-issue-type-details.md
diff --git a/docs/api-reference/issue-types/types/get-work-item-type-schema.md b/docs/api-reference/v1/issue-types/types/get-work-item-type-schema.md
similarity index 100%
rename from docs/api-reference/issue-types/types/get-work-item-type-schema.md
rename to docs/api-reference/v1/issue-types/types/get-work-item-type-schema.md
diff --git a/docs/api-reference/issue-types/types/list-issue-types.md b/docs/api-reference/v1/issue-types/types/list-issue-types.md
similarity index 100%
rename from docs/api-reference/issue-types/types/list-issue-types.md
rename to docs/api-reference/v1/issue-types/types/list-issue-types.md
diff --git a/docs/api-reference/issue-types/types/overview.md b/docs/api-reference/v1/issue-types/types/overview.md
similarity index 100%
rename from docs/api-reference/issue-types/types/overview.md
rename to docs/api-reference/v1/issue-types/types/overview.md
diff --git a/docs/api-reference/issue-types/types/update-issue-types.md b/docs/api-reference/v1/issue-types/types/update-issue-types.md
similarity index 100%
rename from docs/api-reference/issue-types/types/update-issue-types.md
rename to docs/api-reference/v1/issue-types/types/update-issue-types.md
diff --git a/docs/api-reference/issue-types/values/add-property-values.md b/docs/api-reference/v1/issue-types/values/add-property-values.md
similarity index 100%
rename from docs/api-reference/issue-types/values/add-property-values.md
rename to docs/api-reference/v1/issue-types/values/add-property-values.md
diff --git a/docs/api-reference/issue-types/values/delete-property-value.md b/docs/api-reference/v1/issue-types/values/delete-property-value.md
similarity index 100%
rename from docs/api-reference/issue-types/values/delete-property-value.md
rename to docs/api-reference/v1/issue-types/values/delete-property-value.md
diff --git a/docs/api-reference/issue-types/values/get-property-value-detail.md b/docs/api-reference/v1/issue-types/values/get-property-value-detail.md
similarity index 100%
rename from docs/api-reference/issue-types/values/get-property-value-detail.md
rename to docs/api-reference/v1/issue-types/values/get-property-value-detail.md
diff --git a/docs/api-reference/issue-types/values/list-property-values.md b/docs/api-reference/v1/issue-types/values/list-property-values.md
similarity index 100%
rename from docs/api-reference/issue-types/values/list-property-values.md
rename to docs/api-reference/v1/issue-types/values/list-property-values.md
diff --git a/docs/api-reference/issue-types/values/overview.md b/docs/api-reference/v1/issue-types/values/overview.md
similarity index 100%
rename from docs/api-reference/issue-types/values/overview.md
rename to docs/api-reference/v1/issue-types/values/overview.md
diff --git a/docs/api-reference/issue-types/values/update-property-value.md b/docs/api-reference/v1/issue-types/values/update-property-value.md
similarity index 100%
rename from docs/api-reference/issue-types/values/update-property-value.md
rename to docs/api-reference/v1/issue-types/values/update-property-value.md
diff --git a/docs/api-reference/issue/add-issue.md b/docs/api-reference/v1/issue/add-issue.md
similarity index 100%
rename from docs/api-reference/issue/add-issue.md
rename to docs/api-reference/v1/issue/add-issue.md
diff --git a/docs/api-reference/issue/advanced-search-work-items.md b/docs/api-reference/v1/issue/advanced-search-work-items.md
similarity index 100%
rename from docs/api-reference/issue/advanced-search-work-items.md
rename to docs/api-reference/v1/issue/advanced-search-work-items.md
diff --git a/docs/api-reference/issue/delete-issue.md b/docs/api-reference/v1/issue/delete-issue.md
similarity index 100%
rename from docs/api-reference/issue/delete-issue.md
rename to docs/api-reference/v1/issue/delete-issue.md
diff --git a/docs/api-reference/issue/get-issue-detail.md b/docs/api-reference/v1/issue/get-issue-detail.md
similarity index 100%
rename from docs/api-reference/issue/get-issue-detail.md
rename to docs/api-reference/v1/issue/get-issue-detail.md
diff --git a/docs/api-reference/issue/get-issue-sequence-id.md b/docs/api-reference/v1/issue/get-issue-sequence-id.md
similarity index 100%
rename from docs/api-reference/issue/get-issue-sequence-id.md
rename to docs/api-reference/v1/issue/get-issue-sequence-id.md
diff --git a/docs/api-reference/issue/list-issues.md b/docs/api-reference/v1/issue/list-issues.md
similarity index 100%
rename from docs/api-reference/issue/list-issues.md
rename to docs/api-reference/v1/issue/list-issues.md
diff --git a/docs/api-reference/issue/overview.md b/docs/api-reference/v1/issue/overview.md
similarity index 100%
rename from docs/api-reference/issue/overview.md
rename to docs/api-reference/v1/issue/overview.md
diff --git a/docs/api-reference/issue/search-issues.md b/docs/api-reference/v1/issue/search-issues.md
similarity index 100%
rename from docs/api-reference/issue/search-issues.md
rename to docs/api-reference/v1/issue/search-issues.md
diff --git a/docs/api-reference/issue/update-issue-detail.md b/docs/api-reference/v1/issue/update-issue-detail.md
similarity index 100%
rename from docs/api-reference/issue/update-issue-detail.md
rename to docs/api-reference/v1/issue/update-issue-detail.md
diff --git a/docs/api-reference/label/add-label.md b/docs/api-reference/v1/label/add-label.md
similarity index 100%
rename from docs/api-reference/label/add-label.md
rename to docs/api-reference/v1/label/add-label.md
diff --git a/docs/api-reference/label/delete-label.md b/docs/api-reference/v1/label/delete-label.md
similarity index 100%
rename from docs/api-reference/label/delete-label.md
rename to docs/api-reference/v1/label/delete-label.md
diff --git a/docs/api-reference/label/get-label-detail.md b/docs/api-reference/v1/label/get-label-detail.md
similarity index 100%
rename from docs/api-reference/label/get-label-detail.md
rename to docs/api-reference/v1/label/get-label-detail.md
diff --git a/docs/api-reference/label/list-labels.md b/docs/api-reference/v1/label/list-labels.md
similarity index 100%
rename from docs/api-reference/label/list-labels.md
rename to docs/api-reference/v1/label/list-labels.md
diff --git a/docs/api-reference/label/overview.md b/docs/api-reference/v1/label/overview.md
similarity index 100%
rename from docs/api-reference/label/overview.md
rename to docs/api-reference/v1/label/overview.md
diff --git a/docs/api-reference/label/update-label-detail.md b/docs/api-reference/v1/label/update-label-detail.md
similarity index 100%
rename from docs/api-reference/label/update-label-detail.md
rename to docs/api-reference/v1/label/update-label-detail.md
diff --git a/docs/api-reference/link/add-link.md b/docs/api-reference/v1/link/add-link.md
similarity index 100%
rename from docs/api-reference/link/add-link.md
rename to docs/api-reference/v1/link/add-link.md
diff --git a/docs/api-reference/link/delete-link.md b/docs/api-reference/v1/link/delete-link.md
similarity index 100%
rename from docs/api-reference/link/delete-link.md
rename to docs/api-reference/v1/link/delete-link.md
diff --git a/docs/api-reference/link/get-link-detail.md b/docs/api-reference/v1/link/get-link-detail.md
similarity index 100%
rename from docs/api-reference/link/get-link-detail.md
rename to docs/api-reference/v1/link/get-link-detail.md
diff --git a/docs/api-reference/link/list-links.md b/docs/api-reference/v1/link/list-links.md
similarity index 100%
rename from docs/api-reference/link/list-links.md
rename to docs/api-reference/v1/link/list-links.md
diff --git a/docs/api-reference/link/overview.md b/docs/api-reference/v1/link/overview.md
similarity index 100%
rename from docs/api-reference/link/overview.md
rename to docs/api-reference/v1/link/overview.md
diff --git a/docs/api-reference/link/update-link-detail.md b/docs/api-reference/v1/link/update-link-detail.md
similarity index 100%
rename from docs/api-reference/link/update-link-detail.md
rename to docs/api-reference/v1/link/update-link-detail.md
diff --git a/docs/api-reference/members/add-project-member.md b/docs/api-reference/v1/members/add-project-member.md
similarity index 100%
rename from docs/api-reference/members/add-project-member.md
rename to docs/api-reference/v1/members/add-project-member.md
diff --git a/docs/api-reference/members/delete-project-member.md b/docs/api-reference/v1/members/delete-project-member.md
similarity index 100%
rename from docs/api-reference/members/delete-project-member.md
rename to docs/api-reference/v1/members/delete-project-member.md
diff --git a/docs/api-reference/members/get-project-member-detail.md b/docs/api-reference/v1/members/get-project-member-detail.md
similarity index 100%
rename from docs/api-reference/members/get-project-member-detail.md
rename to docs/api-reference/v1/members/get-project-member-detail.md
diff --git a/docs/api-reference/members/get-project-members.md b/docs/api-reference/v1/members/get-project-members.md
similarity index 100%
rename from docs/api-reference/members/get-project-members.md
rename to docs/api-reference/v1/members/get-project-members.md
diff --git a/docs/api-reference/members/get-workspace-members.md b/docs/api-reference/v1/members/get-workspace-members.md
similarity index 100%
rename from docs/api-reference/members/get-workspace-members.md
rename to docs/api-reference/v1/members/get-workspace-members.md
diff --git a/docs/api-reference/members/overview.md b/docs/api-reference/v1/members/overview.md
similarity index 100%
rename from docs/api-reference/members/overview.md
rename to docs/api-reference/v1/members/overview.md
diff --git a/docs/api-reference/members/remove-workspace-member.md b/docs/api-reference/v1/members/remove-workspace-member.md
similarity index 100%
rename from docs/api-reference/members/remove-workspace-member.md
rename to docs/api-reference/v1/members/remove-workspace-member.md
diff --git a/docs/api-reference/members/update-project-member.md b/docs/api-reference/v1/members/update-project-member.md
similarity index 100%
rename from docs/api-reference/members/update-project-member.md
rename to docs/api-reference/v1/members/update-project-member.md
diff --git a/docs/api-reference/milestones/add-milestone.md b/docs/api-reference/v1/milestones/add-milestone.md
similarity index 100%
rename from docs/api-reference/milestones/add-milestone.md
rename to docs/api-reference/v1/milestones/add-milestone.md
diff --git a/docs/api-reference/milestones/delete-milestone.md b/docs/api-reference/v1/milestones/delete-milestone.md
similarity index 100%
rename from docs/api-reference/milestones/delete-milestone.md
rename to docs/api-reference/v1/milestones/delete-milestone.md
diff --git a/docs/api-reference/milestones/get-milestone-detail.md b/docs/api-reference/v1/milestones/get-milestone-detail.md
similarity index 100%
rename from docs/api-reference/milestones/get-milestone-detail.md
rename to docs/api-reference/v1/milestones/get-milestone-detail.md
diff --git a/docs/api-reference/milestones/list-milestone-work-items.md b/docs/api-reference/v1/milestones/list-milestone-work-items.md
similarity index 100%
rename from docs/api-reference/milestones/list-milestone-work-items.md
rename to docs/api-reference/v1/milestones/list-milestone-work-items.md
diff --git a/docs/api-reference/milestones/list-milestones.md b/docs/api-reference/v1/milestones/list-milestones.md
similarity index 100%
rename from docs/api-reference/milestones/list-milestones.md
rename to docs/api-reference/v1/milestones/list-milestones.md
diff --git a/docs/api-reference/milestones/overview.md b/docs/api-reference/v1/milestones/overview.md
similarity index 100%
rename from docs/api-reference/milestones/overview.md
rename to docs/api-reference/v1/milestones/overview.md
diff --git a/docs/api-reference/milestones/update-milestone-detail.md b/docs/api-reference/v1/milestones/update-milestone-detail.md
similarity index 100%
rename from docs/api-reference/milestones/update-milestone-detail.md
rename to docs/api-reference/v1/milestones/update-milestone-detail.md
diff --git a/docs/api-reference/module/add-module-work-items.md b/docs/api-reference/v1/module/add-module-work-items.md
similarity index 100%
rename from docs/api-reference/module/add-module-work-items.md
rename to docs/api-reference/v1/module/add-module-work-items.md
diff --git a/docs/api-reference/module/add-module.md b/docs/api-reference/v1/module/add-module.md
similarity index 100%
rename from docs/api-reference/module/add-module.md
rename to docs/api-reference/v1/module/add-module.md
diff --git a/docs/api-reference/module/archive-module.md b/docs/api-reference/v1/module/archive-module.md
similarity index 100%
rename from docs/api-reference/module/archive-module.md
rename to docs/api-reference/v1/module/archive-module.md
diff --git a/docs/api-reference/module/delete-module.md b/docs/api-reference/v1/module/delete-module.md
similarity index 100%
rename from docs/api-reference/module/delete-module.md
rename to docs/api-reference/v1/module/delete-module.md
diff --git a/docs/api-reference/module/get-module-detail.md b/docs/api-reference/v1/module/get-module-detail.md
similarity index 100%
rename from docs/api-reference/module/get-module-detail.md
rename to docs/api-reference/v1/module/get-module-detail.md
diff --git a/docs/api-reference/module/list-archived-modules.md b/docs/api-reference/v1/module/list-archived-modules.md
similarity index 100%
rename from docs/api-reference/module/list-archived-modules.md
rename to docs/api-reference/v1/module/list-archived-modules.md
diff --git a/docs/api-reference/module/list-module-work-items.md b/docs/api-reference/v1/module/list-module-work-items.md
similarity index 100%
rename from docs/api-reference/module/list-module-work-items.md
rename to docs/api-reference/v1/module/list-module-work-items.md
diff --git a/docs/api-reference/module/list-modules.md b/docs/api-reference/v1/module/list-modules.md
similarity index 100%
rename from docs/api-reference/module/list-modules.md
rename to docs/api-reference/v1/module/list-modules.md
diff --git a/docs/api-reference/module/overview.md b/docs/api-reference/v1/module/overview.md
similarity index 100%
rename from docs/api-reference/module/overview.md
rename to docs/api-reference/v1/module/overview.md
diff --git a/docs/api-reference/module/remove-module-work-item.md b/docs/api-reference/v1/module/remove-module-work-item.md
similarity index 100%
rename from docs/api-reference/module/remove-module-work-item.md
rename to docs/api-reference/v1/module/remove-module-work-item.md
diff --git a/docs/api-reference/module/unarchive-module.md b/docs/api-reference/v1/module/unarchive-module.md
similarity index 100%
rename from docs/api-reference/module/unarchive-module.md
rename to docs/api-reference/v1/module/unarchive-module.md
diff --git a/docs/api-reference/module/update-module-detail.md b/docs/api-reference/v1/module/update-module-detail.md
similarity index 100%
rename from docs/api-reference/module/update-module-detail.md
rename to docs/api-reference/v1/module/update-module-detail.md
diff --git a/docs/api-reference/page/add-project-page.md b/docs/api-reference/v1/page/add-project-page.md
similarity index 100%
rename from docs/api-reference/page/add-project-page.md
rename to docs/api-reference/v1/page/add-project-page.md
diff --git a/docs/api-reference/page/add-workspace-page.md b/docs/api-reference/v1/page/add-workspace-page.md
similarity index 100%
rename from docs/api-reference/page/add-workspace-page.md
rename to docs/api-reference/v1/page/add-workspace-page.md
diff --git a/docs/api-reference/page/get-project-page.md b/docs/api-reference/v1/page/get-project-page.md
similarity index 100%
rename from docs/api-reference/page/get-project-page.md
rename to docs/api-reference/v1/page/get-project-page.md
diff --git a/docs/api-reference/page/get-workspace-page.md b/docs/api-reference/v1/page/get-workspace-page.md
similarity index 100%
rename from docs/api-reference/page/get-workspace-page.md
rename to docs/api-reference/v1/page/get-workspace-page.md
diff --git a/docs/api-reference/page/list-project-pages.md b/docs/api-reference/v1/page/list-project-pages.md
similarity index 100%
rename from docs/api-reference/page/list-project-pages.md
rename to docs/api-reference/v1/page/list-project-pages.md
diff --git a/docs/api-reference/page/list-workspace-pages.md b/docs/api-reference/v1/page/list-workspace-pages.md
similarity index 100%
rename from docs/api-reference/page/list-workspace-pages.md
rename to docs/api-reference/v1/page/list-workspace-pages.md
diff --git a/docs/api-reference/page/overview.md b/docs/api-reference/v1/page/overview.md
similarity index 100%
rename from docs/api-reference/page/overview.md
rename to docs/api-reference/v1/page/overview.md
diff --git a/docs/api-reference/project-features/get-project-features.md b/docs/api-reference/v1/project-features/get-project-features.md
similarity index 100%
rename from docs/api-reference/project-features/get-project-features.md
rename to docs/api-reference/v1/project-features/get-project-features.md
diff --git a/docs/api-reference/project-features/overview.md b/docs/api-reference/v1/project-features/overview.md
similarity index 100%
rename from docs/api-reference/project-features/overview.md
rename to docs/api-reference/v1/project-features/overview.md
diff --git a/docs/api-reference/project-features/update-project-features.md b/docs/api-reference/v1/project-features/update-project-features.md
similarity index 100%
rename from docs/api-reference/project-features/update-project-features.md
rename to docs/api-reference/v1/project-features/update-project-features.md
diff --git a/docs/api-reference/project-labels/add-project-label.md b/docs/api-reference/v1/project-labels/add-project-label.md
similarity index 100%
rename from docs/api-reference/project-labels/add-project-label.md
rename to docs/api-reference/v1/project-labels/add-project-label.md
diff --git a/docs/api-reference/project-labels/delete-project-label.md b/docs/api-reference/v1/project-labels/delete-project-label.md
similarity index 100%
rename from docs/api-reference/project-labels/delete-project-label.md
rename to docs/api-reference/v1/project-labels/delete-project-label.md
diff --git a/docs/api-reference/project-labels/get-project-label-detail.md b/docs/api-reference/v1/project-labels/get-project-label-detail.md
similarity index 100%
rename from docs/api-reference/project-labels/get-project-label-detail.md
rename to docs/api-reference/v1/project-labels/get-project-label-detail.md
diff --git a/docs/api-reference/project-labels/list-project-labels.md b/docs/api-reference/v1/project-labels/list-project-labels.md
similarity index 100%
rename from docs/api-reference/project-labels/list-project-labels.md
rename to docs/api-reference/v1/project-labels/list-project-labels.md
diff --git a/docs/api-reference/project-labels/overview.md b/docs/api-reference/v1/project-labels/overview.md
similarity index 98%
rename from docs/api-reference/project-labels/overview.md
rename to docs/api-reference/v1/project-labels/overview.md
index e5f29224..7aad212e 100644
--- a/docs/api-reference/project-labels/overview.md
+++ b/docs/api-reference/v1/project-labels/overview.md
@@ -8,7 +8,7 @@ keywords: plane, plane api, rest api, api integration, project labels, workspace
Project labels define reusable classifications that can be attached to projects across a workspace.
-[Learn more about Projects](https://developers.plane.so/api-reference/project/overview)
+[Learn more about Projects](https://developers.plane.so/api-reference/v1/project/overview)
diff --git a/docs/api-reference/project-labels/update-project-label-detail.md b/docs/api-reference/v1/project-labels/update-project-label-detail.md
similarity index 100%
rename from docs/api-reference/project-labels/update-project-label-detail.md
rename to docs/api-reference/v1/project-labels/update-project-label-detail.md
diff --git a/docs/api-reference/project/add-project.md b/docs/api-reference/v1/project/add-project.md
similarity index 100%
rename from docs/api-reference/project/add-project.md
rename to docs/api-reference/v1/project/add-project.md
diff --git a/docs/api-reference/project/archive-project.md b/docs/api-reference/v1/project/archive-project.md
similarity index 100%
rename from docs/api-reference/project/archive-project.md
rename to docs/api-reference/v1/project/archive-project.md
diff --git a/docs/api-reference/project/create-project-with-template.md b/docs/api-reference/v1/project/create-project-with-template.md
similarity index 100%
rename from docs/api-reference/project/create-project-with-template.md
rename to docs/api-reference/v1/project/create-project-with-template.md
diff --git a/docs/api-reference/project/delete-project.md b/docs/api-reference/v1/project/delete-project.md
similarity index 100%
rename from docs/api-reference/project/delete-project.md
rename to docs/api-reference/v1/project/delete-project.md
diff --git a/docs/api-reference/project/get-project-detail.md b/docs/api-reference/v1/project/get-project-detail.md
similarity index 100%
rename from docs/api-reference/project/get-project-detail.md
rename to docs/api-reference/v1/project/get-project-detail.md
diff --git a/docs/api-reference/project/list-projects.md b/docs/api-reference/v1/project/list-projects.md
similarity index 100%
rename from docs/api-reference/project/list-projects.md
rename to docs/api-reference/v1/project/list-projects.md
diff --git a/docs/api-reference/project/overview.md b/docs/api-reference/v1/project/overview.md
similarity index 100%
rename from docs/api-reference/project/overview.md
rename to docs/api-reference/v1/project/overview.md
diff --git a/docs/api-reference/project/unarchive-project.md b/docs/api-reference/v1/project/unarchive-project.md
similarity index 100%
rename from docs/api-reference/project/unarchive-project.md
rename to docs/api-reference/v1/project/unarchive-project.md
diff --git a/docs/api-reference/project/update-project-detail.md b/docs/api-reference/v1/project/update-project-detail.md
similarity index 100%
rename from docs/api-reference/project/update-project-detail.md
rename to docs/api-reference/v1/project/update-project-detail.md
diff --git a/docs/api-reference/state/add-state.md b/docs/api-reference/v1/state/add-state.md
similarity index 100%
rename from docs/api-reference/state/add-state.md
rename to docs/api-reference/v1/state/add-state.md
diff --git a/docs/api-reference/state/delete-state.md b/docs/api-reference/v1/state/delete-state.md
similarity index 100%
rename from docs/api-reference/state/delete-state.md
rename to docs/api-reference/v1/state/delete-state.md
diff --git a/docs/api-reference/state/get-state-detail.md b/docs/api-reference/v1/state/get-state-detail.md
similarity index 100%
rename from docs/api-reference/state/get-state-detail.md
rename to docs/api-reference/v1/state/get-state-detail.md
diff --git a/docs/api-reference/state/list-states.md b/docs/api-reference/v1/state/list-states.md
similarity index 100%
rename from docs/api-reference/state/list-states.md
rename to docs/api-reference/v1/state/list-states.md
diff --git a/docs/api-reference/state/overview.md b/docs/api-reference/v1/state/overview.md
similarity index 100%
rename from docs/api-reference/state/overview.md
rename to docs/api-reference/v1/state/overview.md
diff --git a/docs/api-reference/state/update-state-detail.md b/docs/api-reference/v1/state/update-state-detail.md
similarity index 100%
rename from docs/api-reference/state/update-state-detail.md
rename to docs/api-reference/v1/state/update-state-detail.md
diff --git a/docs/api-reference/sticky/add-sticky.md b/docs/api-reference/v1/sticky/add-sticky.md
similarity index 100%
rename from docs/api-reference/sticky/add-sticky.md
rename to docs/api-reference/v1/sticky/add-sticky.md
diff --git a/docs/api-reference/sticky/delete-sticky.md b/docs/api-reference/v1/sticky/delete-sticky.md
similarity index 100%
rename from docs/api-reference/sticky/delete-sticky.md
rename to docs/api-reference/v1/sticky/delete-sticky.md
diff --git a/docs/api-reference/sticky/get-sticky-detail.md b/docs/api-reference/v1/sticky/get-sticky-detail.md
similarity index 100%
rename from docs/api-reference/sticky/get-sticky-detail.md
rename to docs/api-reference/v1/sticky/get-sticky-detail.md
diff --git a/docs/api-reference/sticky/list-stickies.md b/docs/api-reference/v1/sticky/list-stickies.md
similarity index 100%
rename from docs/api-reference/sticky/list-stickies.md
rename to docs/api-reference/v1/sticky/list-stickies.md
diff --git a/docs/api-reference/sticky/overview.md b/docs/api-reference/v1/sticky/overview.md
similarity index 100%
rename from docs/api-reference/sticky/overview.md
rename to docs/api-reference/v1/sticky/overview.md
diff --git a/docs/api-reference/sticky/update-sticky-detail.md b/docs/api-reference/v1/sticky/update-sticky-detail.md
similarity index 100%
rename from docs/api-reference/sticky/update-sticky-detail.md
rename to docs/api-reference/v1/sticky/update-sticky-detail.md
diff --git a/docs/api-reference/teamspace/add-projects-to-teamspace.md b/docs/api-reference/v1/teamspace/add-projects-to-teamspace.md
similarity index 100%
rename from docs/api-reference/teamspace/add-projects-to-teamspace.md
rename to docs/api-reference/v1/teamspace/add-projects-to-teamspace.md
diff --git a/docs/api-reference/teamspace/add-teamspace-members.md b/docs/api-reference/v1/teamspace/add-teamspace-members.md
similarity index 100%
rename from docs/api-reference/teamspace/add-teamspace-members.md
rename to docs/api-reference/v1/teamspace/add-teamspace-members.md
diff --git a/docs/api-reference/teamspace/add-teamspace.md b/docs/api-reference/v1/teamspace/add-teamspace.md
similarity index 100%
rename from docs/api-reference/teamspace/add-teamspace.md
rename to docs/api-reference/v1/teamspace/add-teamspace.md
diff --git a/docs/api-reference/teamspace/delete-teamspace.md b/docs/api-reference/v1/teamspace/delete-teamspace.md
similarity index 100%
rename from docs/api-reference/teamspace/delete-teamspace.md
rename to docs/api-reference/v1/teamspace/delete-teamspace.md
diff --git a/docs/api-reference/teamspace/get-teamspace-detail.md b/docs/api-reference/v1/teamspace/get-teamspace-detail.md
similarity index 100%
rename from docs/api-reference/teamspace/get-teamspace-detail.md
rename to docs/api-reference/v1/teamspace/get-teamspace-detail.md
diff --git a/docs/api-reference/teamspace/list-teamspace-members.md b/docs/api-reference/v1/teamspace/list-teamspace-members.md
similarity index 100%
rename from docs/api-reference/teamspace/list-teamspace-members.md
rename to docs/api-reference/v1/teamspace/list-teamspace-members.md
diff --git a/docs/api-reference/teamspace/list-teamspace-projects.md b/docs/api-reference/v1/teamspace/list-teamspace-projects.md
similarity index 100%
rename from docs/api-reference/teamspace/list-teamspace-projects.md
rename to docs/api-reference/v1/teamspace/list-teamspace-projects.md
diff --git a/docs/api-reference/teamspace/list-teamspaces.md b/docs/api-reference/v1/teamspace/list-teamspaces.md
similarity index 100%
rename from docs/api-reference/teamspace/list-teamspaces.md
rename to docs/api-reference/v1/teamspace/list-teamspaces.md
diff --git a/docs/api-reference/teamspace/overview.md b/docs/api-reference/v1/teamspace/overview.md
similarity index 100%
rename from docs/api-reference/teamspace/overview.md
rename to docs/api-reference/v1/teamspace/overview.md
diff --git a/docs/api-reference/teamspace/remove-projects-from-teamspace.md b/docs/api-reference/v1/teamspace/remove-projects-from-teamspace.md
similarity index 100%
rename from docs/api-reference/teamspace/remove-projects-from-teamspace.md
rename to docs/api-reference/v1/teamspace/remove-projects-from-teamspace.md
diff --git a/docs/api-reference/teamspace/remove-teamspace-members.md b/docs/api-reference/v1/teamspace/remove-teamspace-members.md
similarity index 100%
rename from docs/api-reference/teamspace/remove-teamspace-members.md
rename to docs/api-reference/v1/teamspace/remove-teamspace-members.md
diff --git a/docs/api-reference/teamspace/update-teamspace-detail.md b/docs/api-reference/v1/teamspace/update-teamspace-detail.md
similarity index 100%
rename from docs/api-reference/teamspace/update-teamspace-detail.md
rename to docs/api-reference/v1/teamspace/update-teamspace-detail.md
diff --git a/docs/api-reference/user/get-current-user.md b/docs/api-reference/v1/user/get-current-user.md
similarity index 100%
rename from docs/api-reference/user/get-current-user.md
rename to docs/api-reference/v1/user/get-current-user.md
diff --git a/docs/api-reference/user/overview.md b/docs/api-reference/v1/user/overview.md
similarity index 100%
rename from docs/api-reference/user/overview.md
rename to docs/api-reference/v1/user/overview.md
diff --git a/docs/api-reference/work-item-pages/add-work-item-page.md b/docs/api-reference/v1/work-item-pages/add-work-item-page.md
similarity index 100%
rename from docs/api-reference/work-item-pages/add-work-item-page.md
rename to docs/api-reference/v1/work-item-pages/add-work-item-page.md
diff --git a/docs/api-reference/work-item-pages/delete-work-item-page.md b/docs/api-reference/v1/work-item-pages/delete-work-item-page.md
similarity index 100%
rename from docs/api-reference/work-item-pages/delete-work-item-page.md
rename to docs/api-reference/v1/work-item-pages/delete-work-item-page.md
diff --git a/docs/api-reference/work-item-pages/get-work-item-page-detail.md b/docs/api-reference/v1/work-item-pages/get-work-item-page-detail.md
similarity index 100%
rename from docs/api-reference/work-item-pages/get-work-item-page-detail.md
rename to docs/api-reference/v1/work-item-pages/get-work-item-page-detail.md
diff --git a/docs/api-reference/work-item-pages/list-work-item-pages.md b/docs/api-reference/v1/work-item-pages/list-work-item-pages.md
similarity index 100%
rename from docs/api-reference/work-item-pages/list-work-item-pages.md
rename to docs/api-reference/v1/work-item-pages/list-work-item-pages.md
diff --git a/docs/api-reference/work-item-pages/overview.md b/docs/api-reference/v1/work-item-pages/overview.md
similarity index 100%
rename from docs/api-reference/work-item-pages/overview.md
rename to docs/api-reference/v1/work-item-pages/overview.md
diff --git a/docs/api-reference/work-item-relations/create-work-item-relation.md b/docs/api-reference/v1/work-item-relations/create-work-item-relation.md
similarity index 100%
rename from docs/api-reference/work-item-relations/create-work-item-relation.md
rename to docs/api-reference/v1/work-item-relations/create-work-item-relation.md
diff --git a/docs/api-reference/work-item-relations/list-work-item-relations.md b/docs/api-reference/v1/work-item-relations/list-work-item-relations.md
similarity index 100%
rename from docs/api-reference/work-item-relations/list-work-item-relations.md
rename to docs/api-reference/v1/work-item-relations/list-work-item-relations.md
diff --git a/docs/api-reference/work-item-relations/overview.md b/docs/api-reference/v1/work-item-relations/overview.md
similarity index 100%
rename from docs/api-reference/work-item-relations/overview.md
rename to docs/api-reference/v1/work-item-relations/overview.md
diff --git a/docs/api-reference/work-item-relations/remove-work-item-relation.md b/docs/api-reference/v1/work-item-relations/remove-work-item-relation.md
similarity index 100%
rename from docs/api-reference/work-item-relations/remove-work-item-relation.md
rename to docs/api-reference/v1/work-item-relations/remove-work-item-relation.md
diff --git a/docs/api-reference/worklogs/create-worklog.md b/docs/api-reference/v1/worklogs/create-worklog.md
similarity index 100%
rename from docs/api-reference/worklogs/create-worklog.md
rename to docs/api-reference/v1/worklogs/create-worklog.md
diff --git a/docs/api-reference/worklogs/delete-worklog.md b/docs/api-reference/v1/worklogs/delete-worklog.md
similarity index 100%
rename from docs/api-reference/worklogs/delete-worklog.md
rename to docs/api-reference/v1/worklogs/delete-worklog.md
diff --git a/docs/api-reference/worklogs/get-total-time.md b/docs/api-reference/v1/worklogs/get-total-time.md
similarity index 100%
rename from docs/api-reference/worklogs/get-total-time.md
rename to docs/api-reference/v1/worklogs/get-total-time.md
diff --git a/docs/api-reference/worklogs/get-worklogs-for-issue.md b/docs/api-reference/v1/worklogs/get-worklogs-for-issue.md
similarity index 100%
rename from docs/api-reference/worklogs/get-worklogs-for-issue.md
rename to docs/api-reference/v1/worklogs/get-worklogs-for-issue.md
diff --git a/docs/api-reference/worklogs/overview.md b/docs/api-reference/v1/worklogs/overview.md
similarity index 100%
rename from docs/api-reference/worklogs/overview.md
rename to docs/api-reference/v1/worklogs/overview.md
diff --git a/docs/api-reference/worklogs/update-worklog.md b/docs/api-reference/v1/worklogs/update-worklog.md
similarity index 100%
rename from docs/api-reference/worklogs/update-worklog.md
rename to docs/api-reference/v1/worklogs/update-worklog.md
diff --git a/docs/api-reference/workspace-features/get-workspace-features.md b/docs/api-reference/v1/workspace-features/get-workspace-features.md
similarity index 100%
rename from docs/api-reference/workspace-features/get-workspace-features.md
rename to docs/api-reference/v1/workspace-features/get-workspace-features.md
diff --git a/docs/api-reference/workspace-features/overview.md b/docs/api-reference/v1/workspace-features/overview.md
similarity index 96%
rename from docs/api-reference/workspace-features/overview.md
rename to docs/api-reference/v1/workspace-features/overview.md
index 079f5e38..fd599c48 100644
--- a/docs/api-reference/workspace-features/overview.md
+++ b/docs/api-reference/v1/workspace-features/overview.md
@@ -8,7 +8,7 @@ keywords: plane, plane api, rest api, api integration, workspace features, featu
Workspace features control which major Plane capabilities are enabled for a workspace.
-[Learn more about using the Plane API](https://developers.plane.so/api-reference/introduction)
+[Learn more about using the Plane API](https://developers.plane.so/api-reference/v1/introduction)
diff --git a/docs/api-reference/workspace-features/update-workspace-features.md b/docs/api-reference/v1/workspace-features/update-workspace-features.md
similarity index 100%
rename from docs/api-reference/workspace-features/update-workspace-features.md
rename to docs/api-reference/v1/workspace-features/update-workspace-features.md
diff --git a/docs/api-reference/workspace-invitations/add-workspace-invitation.md b/docs/api-reference/v1/workspace-invitations/add-workspace-invitation.md
similarity index 100%
rename from docs/api-reference/workspace-invitations/add-workspace-invitation.md
rename to docs/api-reference/v1/workspace-invitations/add-workspace-invitation.md
diff --git a/docs/api-reference/workspace-invitations/delete-workspace-invitation.md b/docs/api-reference/v1/workspace-invitations/delete-workspace-invitation.md
similarity index 100%
rename from docs/api-reference/workspace-invitations/delete-workspace-invitation.md
rename to docs/api-reference/v1/workspace-invitations/delete-workspace-invitation.md
diff --git a/docs/api-reference/workspace-invitations/get-workspace-invitation-detail.md b/docs/api-reference/v1/workspace-invitations/get-workspace-invitation-detail.md
similarity index 100%
rename from docs/api-reference/workspace-invitations/get-workspace-invitation-detail.md
rename to docs/api-reference/v1/workspace-invitations/get-workspace-invitation-detail.md
diff --git a/docs/api-reference/workspace-invitations/list-workspace-invitations.md b/docs/api-reference/v1/workspace-invitations/list-workspace-invitations.md
similarity index 100%
rename from docs/api-reference/workspace-invitations/list-workspace-invitations.md
rename to docs/api-reference/v1/workspace-invitations/list-workspace-invitations.md
diff --git a/docs/api-reference/workspace-invitations/overview.md b/docs/api-reference/v1/workspace-invitations/overview.md
similarity index 97%
rename from docs/api-reference/workspace-invitations/overview.md
rename to docs/api-reference/v1/workspace-invitations/overview.md
index f187290b..3cb3f2ec 100644
--- a/docs/api-reference/workspace-invitations/overview.md
+++ b/docs/api-reference/v1/workspace-invitations/overview.md
@@ -8,7 +8,7 @@ keywords: plane, plane api, rest api, api integration, workspace invitations, me
Workspace invitations let admins invite users to join a workspace with specific access settings.
-[Learn more about Members](https://developers.plane.so/api-reference/members/overview)
+[Learn more about Members](https://developers.plane.so/api-reference/v1/members/overview)
diff --git a/docs/api-reference/workspace-invitations/update-workspace-invitation.md b/docs/api-reference/v1/workspace-invitations/update-workspace-invitation.md
similarity index 100%
rename from docs/api-reference/workspace-invitations/update-workspace-invitation.md
rename to docs/api-reference/v1/workspace-invitations/update-workspace-invitation.md
diff --git a/docs/api-reference/v2/audit-logs/get-audit-log.md b/docs/api-reference/v2/audit-logs/get-audit-log.md
new file mode 100644
index 00000000..b457e997
--- /dev/null
+++ b/docs/api-reference/v2/audit-logs/get-audit-log.md
@@ -0,0 +1,187 @@
+---
+title: Get an audit log
+description: Retrieve a single Plane audit log entry by id with the v2 REST API. Path parameters, the full entry payload, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get audit log, audit entry by id, compliance evidence, security investigation, GET audit log
+---
+
+# Get an audit log
+
+
+ GET
+ /api/v2/workspaces/{slug}/audit-logs/{pk}/
+
+
+
+
+
+Retrieve one audit entry by id. Reach for this when an investigation has already narrowed to a single event — a ticket cites an entry id, an alert fired on one, or a list result needs to be cited as evidence.
+
+The payload is identical to a row in [List audit logs](/api-reference/v2/audit-logs/list-audit-logs), so use this endpoint for stable, linkable access to a single event rather than for bulk reading.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`. Audit entries never cross workspaces: an entry id from another workspace returns `404`.
+
+
+
+
+
+The id of the audit log entry to retrieve. This is the entry's `id`, not its `event_id`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+None. The detail route takes no filters and no `?expand=` — `actor_id` and `target_id` stay as ids, so resolve them yourself if you need names beyond the snapshots already in the payload.
+
+
+
+
+
+### Scopes
+
+`workspaces.audit_logs:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this workspace's audit logs. |
+| `404` | `resource_not_found` | No such entry or workspace, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Entries are immutable
+There is no `PATCH` or `DELETE` on this path. An audit entry is written once by Plane and cannot be edited or removed through the API, which is what makes a retrieved entry usable as evidence.
+:::
+
+::: info `id` versus `event_id`
+`id` addresses this record and is what belongs in the URL. `event_id` identifies the underlying event and is the better deduplication key when you are ingesting entries elsewhere. Passing an `event_id` here returns `404`.
+:::
+
+
+
+## Pivoting from one entry
+
+A single entry is usually the start of a thread, not the end of one. From the payload above:
+
+| Next question | Query on [List audit logs](/api-reference/v2/audit-logs/list-audit-logs) |
+| --------------------------------- | ------------------------------------------------------------------------------ |
+| What else did this actor do? | `?actor_id=16c61a3a-512a-48ac-b0be-b6b46fe6f430` |
+| What else came from that address? | `?ip_address=203.0.113.42` |
+| Full history of this target | `?target_type=workspace_member&target_id=3e8a5d17-9c40-4b2f-81d6-4a7f2b9e0c53` |
+| Everything around the same moment | `?created_after=2026-01-14T09:00:00Z&created_before=2026-01-14T10:00:00Z` |
+
+Resolve `actor_id` to a current person with [List workspace members](/api-reference/v2/members/list-workspace-members) — the names on the entry are snapshots from event time, and may no longer match.
+
+::: tip When the entry explains a `409`
+A `settings` entry that flipped `is_work_item_types_enabled` explains a wave of `409 work_item_types_managed_at_workspace` or `work_item_types_managed_at_project` responses: the workspace changed which surface accepts work item type writes. See [Work item type modes](/api-reference/v2/work-item-type-modes) and [Workspace features](/api-reference/v2/workspace-features/overview).
+:::
diff --git a/docs/api-reference/v2/audit-logs/list-audit-logs.md b/docs/api-reference/v2/audit-logs/list-audit-logs.md
new file mode 100644
index 00000000..b8719ac8
--- /dev/null
+++ b/docs/api-reference/v2/audit-logs/list-audit-logs.md
@@ -0,0 +1,394 @@
+---
+title: List audit logs
+description: Query a Plane workspace's audit trail with the v2 REST API. Date-range, actor, category, outcome and target filters, cursor pagination for exports, scopes, errors, and code examples.
+keywords: plane api v2, list audit logs, audit trail query, compliance export, created_after, created_before, outcome failure, SIEM, cursor pagination
+---
+
+# List audit logs
+
+
+ GET
+ /api/v2/workspaces/{slug}/audit-logs/
+
+
+
+
+
+Query the workspace's audit trail. This is the compliance and security-investigation endpoint: scope it to a date range for a review period, to an actor for an access certification, or to `outcome=failure` when you are looking for attempts that were turned away.
+
+It is also the export endpoint. With `?paginate=cursor` you can walk months of history into a SIEM or warehouse without paying for a `COUNT(*)` on every page.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`. The audit trail is per workspace; there is no cross-workspace query.
+
+
+
+
+
+
+
+
+### Query Parameters — time range
+
+Filters combine with `AND`. Check your spelling on `order_by` and `paginate` — neither is validated. An unrecognized `order_by` value falls back to the resource's default ordering, and anything other than `paginate=cursor` uses offset pagination. A typo shows up as an unexpected sort order or envelope, not as an error.
+
+
+
+
+
+Return entries recorded from this timestamp onward, for example `2026-01-01T00:00:00Z`. Pair it with `created_before` to bound a review period.
+
+
+
+
+
+Return entries recorded up to this timestamp. Send timestamps in UTC — `created_at` is returned in UTC, and mixing offsets is the usual reason a window looks empty.
+
+
+
+
+
+
+
+
+### Query Parameters — who and what
+
+
+
+
+
+Return only what one user did. This is the same id as `member_id` on the [member rosters](/api-reference/v2/members/overview), so an access certification is a roster read followed by one call per person.
+
+Entries with no actor — `system` and `anonymous` events — are excluded by any `actor_id` filter, so run a second unfiltered query if you are asked to account for everything in a window.
+
+
+
+
+
+Return only one specific event, for example `member.role_updated`. Use it when you already know the action you are hunting for; use `category` when you want a whole class of them.
+
+
+
+
+
+Return only one family of events: `auth`, `member`, `role`, `settings`, `integration`, `webhook`, `security`, or `instance`.
+
+`role` and `member` answer "who gained access and when". `settings` answers "what changed about this workspace". `auth` plus `outcome=failure` answers "who tried to get in".
+
+
+
+
+
+Return only entries with this outcome: `success` or `failure`. Failed attempts are recorded, which is what makes this filter the fastest way to spot probing.
+
+
+
+
+
+Return only entries acting on this kind of object, for example `workspace_member`. Combine it with `target_id` to build the history of a single record.
+
+
+
+
+
+Return only entries acting on this specific object. `?target_type=…&target_id=…` is the "everything that ever happened to this thing" query.
+
+
+
+
+
+Return only entries recorded from this client IP. Start from a suspicious entry, then pivot on its `ip_address` to see everything else that address did.
+
+
+
+
+
+A free-text search term across the entry. Reach for it when you have a name or a fragment rather than an id.
+
+
+
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `-created_at` , `created_at` — newest first, or oldest first
+- `id` , `-id`
+
+Use `-created_at` for an investigation, `created_at` for a replayable export. Two entries written in the same instant are separated by `sequence_number` in the payload.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Use 200 for exports.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000 — which an audit trail exceeds quickly, so use cursor pagination for anything deeper than a few pages.
+
+
+
+
+
+Set to `cursor` for the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. This is the mode to use for a full export: it has no offset ceiling and it does not skip or repeat rows as new entries land mid-walk.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted. Worth doing on every page of an offset walk — cursor pagination never runs a `COUNT` and ignores this parameter.
+
+
+
+
+
+
+
+
+### Scopes
+
+`workspaces.audit_logs:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this workspace's audit logs. |
+| `404` | `resource_not_found` | No such workspace, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning Audit reads are privileged
+`workspaces.audit_logs:read` exposes actor emails, IP addresses, and user agents for the whole workspace. Scope the tokens you issue for exports to exactly this, keep them out of user-facing clients, and treat the exported data as it deserves.
+:::
+
+
+
+## Exporting the trail
+
+Walk it with the cursor envelope, oldest first, and store `event_id` as the deduplication key:
+
+```python
+import requests
+
+BASE = "https://api.plane.so/api/v2/workspaces/my-team/audit-logs/"
+params = {"paginate": "cursor", "per_page": 200, "order_by": "created_at"}
+cursor = None
+
+while True:
+ if cursor:
+ params["cursor"] = cursor # the token carries position only — keep every other param
+ page = requests.get(BASE, headers={"X-Api-Key": "your-api-key"}, params=params).json()
+ for entry in page["data"]:
+ ingest(entry) # key on entry["event_id"]
+ if not page["has_more"]:
+ break
+ cursor = page["next_cursor"]
+```
+
+Resume an incremental export with `created_after` set to the `created_at` of the last entry you stored, and let `event_id` absorb the overlap at the boundary.
+
+::: tip Investigating a sudden wave of `409`s
+`409 work_item_types_managed_at_workspace` and `work_item_types_managed_at_project` mean a client is writing to the wrong surface, usually because an admin switched the workspace's work item type mode. Query `?category=settings` around the time the errors started to find the change and who made it. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+## Related
+
+- [Get an audit log](/api-reference/v2/audit-logs/get-audit-log)
+- [Audit logs overview](/api-reference/v2/audit-logs/overview) — every field and every enum value
+- [Pagination](/api-reference/v2/pagination)
diff --git a/docs/api-reference/v2/audit-logs/overview.md b/docs/api-reference/v2/audit-logs/overview.md
new file mode 100644
index 00000000..8c608c78
--- /dev/null
+++ b/docs/api-reference/v2/audit-logs/overview.md
@@ -0,0 +1,217 @@
+---
+title: Audit logs overview
+description: The Plane API v2 audit log object. Actor, target, category, outcome and source enums, old and new values, and how to query the workspace audit trail for compliance and security investigations.
+keywords: plane api v2, audit logs, audit trail, compliance, security investigation, actor_type, category, outcome, source, SIEM export
+---
+
+# Audit logs overview
+
+The audit log is the workspace's tamper-evident record of who did what, from where, and whether it worked. It is the endpoint you reach for during a compliance review, an access certification, or a security investigation — and the one you point a SIEM or warehouse at for continuous export.
+
+Every entry names an **actor** (who), a **target** (what they acted on), an **event** (what they did), an **outcome** (whether it succeeded), and a **source** (which surface the request came through). Where the event changed something, `old_value` and `new_value` carry the before and after.
+
+Audit logs are read-only. There is no way to write, edit, or delete an entry through the API.
+
+
+
+
+## The audit log object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for this audit log record. This is the id you pass to [Get an audit log](/api-reference/v2/audit-logs/get-audit-log).
+
+- `event_id` _string (uuid)_
+
+ Identifier of the event the record describes, distinct from `id`. Use it as the idempotency key when you ingest entries into another system so a re-read never double-counts.
+
+- `sequence_number` _integer_
+
+ An ordering number assigned to the entry as it was written. Useful as a stable tiebreaker when two entries share the same `created_at`.
+
+- `event_name` _string_
+
+ The specific event, for example `member.role_updated`. Filterable — this is the field to pin when you know exactly which action you are hunting for.
+
+- `category` _string_
+
+ The family the event belongs to. One of `auth`, `member`, `role`, `settings`, `integration`, `webhook`, `security`, or `instance`. Filter on `category` when you want a whole class of activity rather than one event name.
+
+- `outcome` _string_
+
+ Whether the attempt succeeded. One of `success` or `failure`. Failed attempts are recorded, which is what makes this useful for detecting probing.
+
+- `source` _string_
+
+ The surface the request came through. One of `platform`, `api`, `graphql`, `auth`, or `system`. `api` marks calls made with a token like yours; `system` marks actions Plane took on its own.
+
+- `actor_type` _string_
+
+ What kind of principal acted. One of `user`, `api_token`, `system`, or `anonymous`.
+
+- `actor_id` _string (uuid)_
+
+ The acting user. Nullable — `system` and `anonymous` actors have no id. It matches `member_id` on the [member](/api-reference/v2/members/overview) rosters.
+
+- `actor_display_name` , `actor_email` _string_
+
+ The actor's name and email captured at the time of the event. They are snapshots: a later rename or email change does not rewrite past entries.
+
+- `target_type` _string_
+
+ The kind of thing that was acted on, for example `workspace_member`.
+
+- `target_id` _string_
+
+ Identifier of the thing that was acted on.
+
+- `target_display_name` _string_
+
+ A human-readable label for the target, captured at the time of the event.
+
+- `old_value` , `new_value` _any_
+
+ The state before and after the change. Both are nullable — an event that changes nothing, such as a sign-in, carries `null` on both sides.
+
+- `reason` _string_
+
+ A short explanation recorded alongside the event. Most informative on `outcome: failure` entries.
+
+- `metadata` _any_
+
+ Additional structured context. The shape varies by `event_name`, so read it defensively rather than typing it strictly.
+
+- `ip_address` _string_
+
+ The client IP the request came from. Nullable — `system` events have none.
+
+- `user_agent` _string_
+
+ The client's user agent string.
+
+- `workspace_id` _string (uuid)_
+
+ The workspace the event belongs to.
+
+- `project_id` _string (uuid)_
+
+ The project the event is scoped to, when it is scoped to one.
+
+- `created_at` _string (date-time)_
+
+ When the event was recorded. This is the field the date-range filters work on.
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| ------ | -------------------------------------------- | ----------------- |
+| `GET` | `/api/v2/workspaces/{slug}/audit-logs/` | List audit logs |
+| `GET` | `/api/v2/workspaces/{slug}/audit-logs/{pk}/` | Get one audit log |
+
+Both require the `workspaces.audit_logs:read` scope. There is no `POST`, `PATCH`, or `DELETE` — entries are written by Plane and are not editable by any caller.
+
+## Querying for an investigation
+
+The list endpoint takes the filters an investigation actually needs. They combine with `AND`:
+
+| Question | Query |
+| ------------------------------------ | ------------------------------------------------------------------------------ |
+| What happened in this window? | `?created_after=2026-01-01T00:00:00Z&created_before=2026-02-01T00:00:00Z` |
+| What did this person do? | `?actor_id=16c61a3a-512a-48ac-b0be-b6b46fe6f430` |
+| Every permission change this quarter | `?category=role&created_after=2026-01-01T00:00:00Z` |
+| Failed attempts only | `?outcome=failure` |
+| Everything touching one object | `?target_type=workspace_member&target_id=3e8a5d17-9c40-4b2f-81d6-4a7f2b9e0c53` |
+| Activity from one address | `?ip_address=198.51.100.7` |
+| One specific action | `?event_name=member.role_updated` |
+
+For a full export, add `?paginate=cursor` and walk `next_cursor` — that skips the `COUNT(*)` on every page and stays stable as new entries arrive. Details on [List audit logs](/api-reference/v2/audit-logs/list-audit-logs).
+
+## Reading an entry
+
+- **Snapshots, not joins.** `actor_display_name`, `actor_email`, and `target_display_name` are recorded at event time. That is what makes the log evidential — the entry still names the person as they were, even after a rename or an offboarding.
+- **Failures are first-class.** `outcome: failure` entries are exactly what a security review is looking for. A burst of them from one `ip_address` with `category: auth` is the shape of a credential-stuffing attempt.
+- **`old_value` and `new_value` are free-form.** They describe the change for that `event_name` and are not a fixed schema. Render them, diff them, but do not require particular keys.
+- **`source` separates humans from tokens.** `platform` is the Plane app, `api` is a REST client, `system` is Plane acting by itself. Combining `source: api` with `actor_type: api_token` isolates automation.
+
+::: tip Correlate a `409` with a settings change
+If clients suddenly start getting `409 work_item_types_managed_at_workspace` or `work_item_types_managed_at_project`, someone toggled the work item type mode. Query `?category=settings` around the time the failures started to find the change and the actor behind it. See [Work item type modes](/api-reference/v2/work-item-type-modes) and [Update workspace features](/api-reference/v2/workspace-features/update-workspace-features).
+:::
+
+## Related
+
+- [List audit logs](/api-reference/v2/audit-logs/list-audit-logs)
+- [Get an audit log](/api-reference/v2/audit-logs/get-audit-log)
+- [Members overview](/api-reference/v2/members/overview) — resolve `actor_id` to a person
+- [Pagination](/api-reference/v2/pagination) — cursor traversal for exports
diff --git a/docs/api-reference/v2/authentication.md b/docs/api-reference/v2/authentication.md
new file mode 100644
index 00000000..7fc43b37
--- /dev/null
+++ b/docs/api-reference/v2/authentication.md
@@ -0,0 +1,255 @@
+---
+title: Authentication
+description: Authenticate Plane API v2 requests with an X-Api-Key personal access token or an OAuth 2.0 bearer token, and understand the global and fine-grained scope model.
+keywords: plane api v2 authentication, X-Api-Key, personal access token, plane oauth 2.0, bearer token, oauth scopes, users me endpoint
+---
+
+# Authentication
+
+Every v2 request must authenticate. There are two ways to do it, and which one you pick depends on whose data you are
+touching.
+
+| Method | Use it when | Header |
+| ----------------------- | ------------------------------------------------------------ | ------------------------------- |
+| [API key](#api-key) | A script, backend job, or integration acts as **you** | `X-Api-Key: ` |
+| [OAuth 2.0](#oauth-2-0) | An app acts on **another user's** behalf, with their consent | `Authorization: Bearer ` |
+
+Both are checked before anything else. A missing or invalid credential returns `401 unauthorized` — see
+[Errors](/api-reference/v2/errors).
+
+::: warning Header casing
+v2 prose and examples use `X-Api-Key`. The v1 docs write the same header as `X-API-Key`. HTTP header names are
+case-insensitive, so either spelling reaches the server, but match the v2 form when copying examples between versions.
+:::
+
+## API key
+
+An API key is a personal access token. It authenticates as the user who created it.
+
+### Generate a key
+
+1. Log in to Plane and open **Profile Settings**.
+2. Go to the **Personal Access Tokens** tab.
+3. Click **Add personal access token**.
+4. Give it a title and description so you can tell later what it was for and where it is deployed.
+5. Optionally set an expiry so it stops working after a date you choose.
+
+The token value is shown once. Store it somewhere your application can read it as a secret — never in client-side code
+or a committed file.
+
+### Use a key
+
+```bash
+curl "https://api.plane.so/api/v2/users/me/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+::: warning API keys are not scope-limited
+An API key acts with its owner's **full permissions**. It is not constrained by OAuth scopes — there is no way to mint
+a read-only key. It is still subject to the permission engine, so it can never do something the owner could not do in
+the UI, but within that boundary it can do everything.
+
+Treat a key as equivalent to the account it belongs to. If one leaks, delete it in **Personal Access Tokens** and issue
+a new one.
+:::
+
+## OAuth 2.0
+
+Use OAuth when a third-party app needs to act on behalf of a Plane user who is not you. v2 uses the standard
+authorization-code flow.
+
+| Step | Endpoint |
+| ----------------- | -------------------------------------- |
+| Authorization URL | `/auth/o/authorize/` |
+| Token URL | `/auth/o/token/` |
+| Request header | `Authorization: Bearer ` |
+
+1. Redirect the user to `/auth/o/authorize/` with your client id, redirect URI, and the scopes you need.
+2. The user approves the scopes. Plane redirects back with an authorization code.
+3. Exchange that code for an access token at `/auth/o/token/`.
+4. Send the token on every API call.
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/" \
+ -H "Authorization: Bearer $PLANE_ACCESS_TOKEN"
+```
+
+::: info OAuth tokens fail closed
+Unlike an API key, an OAuth token is **capped to the scopes the user granted**. If the token lacks a scope that
+satisfies the operation, the request is rejected before the permission engine runs — a user with permission to do
+something does not help if your token was never granted the scope for it.
+
+Request only the scopes you need. Adding one later requires the user to re-authorize.
+:::
+
+## The scope model
+
+Scopes come in two tiers, and an operation is satisfied by **either** one (OR logic):
+
+- **Global tier** — `read` grants read access to all resources; `write` grants write access to all resources.
+- **Fine-grained** — `:read` and `:write`, for example `projects.work_items:read` or
+ `projects.states:write`.
+
+So `GET …/states/` succeeds with **either** `read` **or** `projects.states:read`. You never need both, and a global
+tier scope is not a prerequisite for the fine-grained one.
+
+::: tip Prefer fine-grained scopes
+Ask for `read` or `write` only when your app genuinely spans the whole workspace surface. A consent screen listing
+three specific scopes converts better than one asking for everything, and it limits the blast radius if a token leaks.
+:::
+
+## Who am I
+
+`GET /api/v2/users/me/` echoes back whoever the credential you sent belongs to. It takes no workspace or project id,
+so it isolates an auth problem from a permission or tenancy problem — reach for it first when you are debugging a `401`
+or a `403`.
+
+
+ GET
+ /api/v2/users/me/
+
+
+No fine-grained scope gates it. An API key can always call it; an OAuth token needs the global `read` scope — a token
+holding only fine-grained scopes such as `projects.states:read` cannot call it.
+
+
+
+
+```bash
+curl "https://api.plane.so/api/v2/users/me/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+
+```python
+import requests
+
+response = requests.get(
+ "https://api.plane.so/api/v2/users/me/",
+ headers={"X-Api-Key": "your-api-key"},
+)
+print(response.json())
+```
+
+
+
+
+```javascript
+const response = await fetch("https://api.plane.so/api/v2/users/me/", {
+ headers: { "X-Api-Key": "your-api-key" },
+});
+const data = await response.json();
+```
+
+
+
+
+
+
+```json
+{
+ "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "email": "priya@my-team.io",
+ "display_name": "priya",
+ "principal_kind": "oauth",
+ "scopes": ["projects.work_items:read", "projects.states:read"]
+}
+```
+
+
+
+### Response fields
+
+- `id` _string (uuid)_
+
+ The authenticated user.
+
+- `email` _string (email)_
+
+ The user's email address.
+
+- `display_name` _string_
+
+ The user's display name.
+
+- `principal_kind` _string_
+
+ How the request authenticated. One of `oauth`, `api_key`, or `other`.
+
+- `scopes` _array of string_
+
+ The scopes this credential carries. Populated for OAuth tokens. Because API keys are not scope-limited, this tells
+ you nothing about an API key's reach — check `principal_kind` first.
+
+::: tip Debugging a 403
+Call `/api/v2/users/me/` and compare `scopes` against the scope listed on the endpoint page you are calling. If the
+scope is missing, the token needs re-authorization. If it is present and you still get `403`, the block is the user's
+role, not the token.
+:::
+
+## Fine-grained scopes for live endpoints
+
+These are the scopes that gate endpoints documented in this reference. Each is also satisfied by the matching global
+tier scope (`read` or `write`).
+
+| Scope | Grants |
+| --------------------------------------- | ---------------------------------------------- |
+| `projects.work_items:read` | Read access to project work items |
+| `projects.work_items:write` | Write access to project work items |
+| `projects.work_items.comments:read` | Read access to work item comments |
+| `projects.work_items.comments:write` | Write access to work item comments |
+| `projects.states:read` | Read access to project states |
+| `projects.states:write` | Write access to project states |
+| `projects.labels:read` | Read access to project labels |
+| `projects.labels:write` | Write access to project labels |
+| `projects.cycles:read` | Read access to project cycles |
+| `projects.cycles:write` | Write access to project cycles |
+| `projects.modules:read` | Read access to project modules |
+| `projects.modules:write` | Write access to project modules |
+| `projects.work_item_types:read` | Read access to work item types |
+| `projects.work_item_types:write` | Write access to work item types |
+| `projects.work_item_properties:read` | Read access to work item properties |
+| `projects.work_item_properties:write` | Write access to work item properties |
+| `projects.members:read` | Read access to project members |
+| `workspaces.members:read` | Read access to workspace members |
+| `workspaces.work_item_types:read` | Read access to workspace work item types |
+| `workspaces.work_item_types:write` | Write access to workspace work item types |
+| `workspaces.work_item_properties:read` | Read access to workspace work item properties |
+| `workspaces.work_item_properties:write` | Write access to workspace work item properties |
+| `workspaces.features:read` | Read access to workspace features |
+| `workspaces.features:write` | Write access to workspace features |
+| `workspaces.audit_logs:read` | Read access to workspace audit logs |
+| `read` | Read access to all resources |
+| `write` | Write access to all resources |
+
+::: info The catalog is larger than this table
+The OAuth scope catalog advertises the full planned resource surface — pages, customers, initiatives, releases,
+teamspaces, templates, webhooks, assets, and more. Those scopes exist and can be requested, but their endpoints are not
+live in v2 yet, so only the scopes above currently gate real requests.
+
+The complete catalog is served under `components.securitySchemes.oauth2` in
+[`GET /api/v2/schema/`](/api-reference/v2/introduction#machine-readable-schema).
+:::
+
+The scope required by a specific operation is listed in the **Scopes** section of each endpoint page — for example
+[Create a state](/api-reference/v2/states/create-state) requires `projects.states:write`.
+
+## Tenancy
+
+Authentication tells Plane who you are; tenancy decides what you can see. A workspace slug, project id, or record id
+outside your tenant returns **`404 resource_not_found`**, never `403`.
+
+::: warning A cross-tenant id looks identical to a deleted one
+This is deliberate — the API never confirms that a resource you cannot access exists. When a request that should work
+returns `404`, verify the workspace and project the id belongs to before assuming the record is gone.
+
+Reserve `403 forbidden` in your error handling for the case it actually means: you can see the resource, but your role
+or token scope cannot perform this action.
+:::
+
+## Next
+
+- [Errors](/api-reference/v2/errors) — how `401`, `403`, and `404` are reported.
+- [Introduction](/api-reference/v2/introduction) — base URL, trailing slashes, and request conventions.
diff --git a/docs/api-reference/v2/cycles/create-cycle.md b/docs/api-reference/v2/cycles/create-cycle.md
new file mode 100644
index 00000000..ddb2f3f6
--- /dev/null
+++ b/docs/api-reference/v2/cycles/create-cycle.md
@@ -0,0 +1,237 @@
+---
+title: Create a cycle
+description: Create a time-boxed cycle in a Plane project with the v2 REST API. Body parameters, dates and time zones, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create cycle, sprint, iteration, start_date, end_date, cycle timezone, POST cycles
+---
+
+# Create a cycle
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/cycles/
+
+
+
+
+
+Add a cycle to a project. Only `name` is required — leave the dates off to create an unscheduled cycle and fill them in later with [Update a cycle](/api-reference/v2/cycles/update-cycle).
+
+Cycle names must be unique within a project. Reusing one returns `409 conflict`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to add the cycle to.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Display name for the cycle, unique within the project. Maximum 255 characters.
+
+
+
+
+
+Free-form description of what the cycle covers.
+
+
+
+
+
+When the cycle opens, as an ISO 8601 date-time. Nullable — omit it or send `null` for an unscheduled cycle.
+
+
+
+
+
+When the cycle closes, as an ISO 8601 date-time. Nullable.
+
+
+
+
+
+The IANA time zone the cycle's dates are interpreted in, for example `America/New_York`, `Asia/Kolkata`, `Europe/London`, or `UTC`. Set it to the team's working zone so a cycle boundary lands at local midnight instead of UTC midnight. Any value outside the IANA list is rejected with `400 validation_error`.
+
+
+
+
+
+Ordering weight for the cycle within the project. Lower values sort first when you list with `?order_by=sort_order`.
+
+
+
+
+
+JSON blob holding the cycle's icon configuration, as written by Plane clients. Stored and returned unchanged.
+
+
+
+
+
+Your system's identifier for this cycle, for sync and import correlation. Maximum 255 characters. You can find the cycle again later with `?external_id=` on [List cycles](/api-reference/v2/cycles/list-cycles).
+
+
+
+
+
+The system `external_id` came from, for example `jira` or `linear`. Maximum 255 characters.
+
+
+
+
+
+
+::: info Owner is not settable
+`owned_by_id` is returned on the response but is not a body parameter — Plane assigns cycle ownership and the API does not accept an override.
+:::
+
+
+
+### Scopes
+
+`projects.cycles:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | Missing `name`, a name over 255 characters, an unparseable date, or a `timezone` outside the IANA list. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't create cycles in this project. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `conflict` | A cycle with this name already exists in the project. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+Remove a cycle from a project. The delete is a soft delete — the cycle stops appearing in reads and its name is freed for reuse.
+
+A successful call returns `204` with an empty body. There is nothing to parse, so branch on the status code.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the cycle belongs to.
+
+
+
+
+
+The cycle to delete.
+
+
+
+
+
+
+::: warning Deleting the cycle does not delete its work items
+Work items that were in the cycle survive the delete — they simply lose their cycle assignment. Move them to another cycle first if the assignment matters, using the v1 [cycle work-item endpoints](/api-reference/v1/cycle/overview).
+:::
+
+Deleting a cycle that is already deleted returns `404`, so the call is safe to retry: treat both `204` and `404` as "gone".
+
+
+
+### Scopes
+
+`projects.cycles:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------- |
+| `400` | `validation_error` | A malformed path parameter, for example a `pk` that isn't a UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't delete cycles in this project. |
+| `404` | `resource_not_found` | No such cycle, wrong project, or the record is outside your tenant. |
+| `409` | `conflict` | The cycle's current state blocks deletion. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/cycles/get-cycle.md b/docs/api-reference/v2/cycles/get-cycle.md
new file mode 100644
index 00000000..c5af43e6
--- /dev/null
+++ b/docs/api-reference/v2/cycles/get-cycle.md
@@ -0,0 +1,150 @@
+---
+title: Get a cycle
+description: Retrieve a single Plane cycle by id with the v2 REST API. Path parameters, response fields, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get cycle, retrieve cycle, cycle by id, GET cycles
+---
+
+# Get a cycle
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/
+
+
+
+
+
+Fetch one cycle by id. The response is the same cycle object the list endpoint returns for each row, so you only need this call when you already hold an id.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the cycle belongs to. A cycle id from a different project returns `404`, not the record.
+
+
+
+
+
+The cycle to retrieve.
+
+
+
+
+
+
+::: info No `?expand=` on cycles
+Cycles do not support `?expand=`. `owned_by_id` and `created_by_id` are always plain ids — resolve them against the members endpoints when you need display names.
+:::
+
+
+
+### Scopes
+
+`projects.cycles:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read cycles in this project. |
+| `404` | `resource_not_found` | No such cycle, wrong project, or the record is outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/cycles/list-cycles.md b/docs/api-reference/v2/cycles/list-cycles.md
new file mode 100644
index 00000000..e5e690a7
--- /dev/null
+++ b/docs/api-reference/v2/cycles/list-cycles.md
@@ -0,0 +1,278 @@
+---
+title: List cycles
+description: List the cycles in a Plane project with the v2 REST API. Filters, search, ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list cycles, cycles pagination, owned_by_id filter, order_by sort_order, GET cycles
+---
+
+# List cycles
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/cycles/
+
+
+
+
+
+Return the cycles in a project as a paginated page. Use it to build a sprint picker, to find the cycle covering a date range, or to reconcile cycles you imported from another tracker.
+
+Filters combine with `AND` — `?owned_by_id=…&search=sprint` returns only cycles that match both.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project whose cycles you want.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+**Filters**
+
+
+
+
+
+Return only cycles owned by this user. This is how you build a "my cycles" view — pass the authenticated user's id. Ownership is assigned by Plane, so this is a read-side filter only.
+
+
+
+
+
+Return only cycles whose `external_id` matches exactly. Pair it with `external_source` when the same identifier could come from more than one system.
+
+
+
+
+
+Return only cycles imported from this source, for example `jira`.
+
+
+
+
+
+**Search**
+
+
+
+
+
+Free-text search term matched against the cycle.
+
+
+
+
+
+**Ordering**
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` / `-sort_order` — the project's manual cycle ordering
+- `created_at` / `-created_at` — newest or oldest first
+- `id` / `-id`
+
+
+
+
+
+**Pagination**
+
+
+
+
+
+Page size. Defaults to 50, maximum 200.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000 — past that, switch to cursor pagination.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope instead of the default offset envelope. The response then carries `next_cursor` and `has_more` rather than `next`, `previous`, and `total_count`; pass `next_cursor` back as `cursor` to walk to the following page. Use it for deep traversal, where offset paging gets expensive.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count` — the field is then omitted from the envelope. Worth doing when you only need the rows.
+
+
+
+
+
+::: warning Bad enum values fail silently
+`order_by` and `paginate` are not validated against their allowed values. An unrecognized `order_by` falls back to the cycle default ordering, and anything other than `paginate=cursor` uses offset pagination — so check your spelling, because a typo shows up as an unexpected sort order or envelope rather than an error.
+:::
+
+
+
+
+
+### Scopes
+
+`projects.cycles:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ----------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read cycles in this project. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/cycles/overview.md b/docs/api-reference/v2/cycles/overview.md
new file mode 100644
index 00000000..05350265
--- /dev/null
+++ b/docs/api-reference/v2/cycles/overview.md
@@ -0,0 +1,143 @@
+---
+title: Cycles overview
+description: The Plane API v2 cycle object. Attributes, start and end dates, time zones, cycle ownership, and the endpoints for managing project cycles.
+keywords: plane api v2, cycles, sprints, iterations, start_date, end_date, cycle timezone, owned_by_id
+---
+
+# Cycles overview
+
+A cycle is a time-boxed iteration inside a project — a sprint, a release window, or any fixed period a team commits work to. A cycle belongs to exactly one project and is never shared across projects.
+
+Cycles are defined by a `start_date` and an `end_date`, interpreted in the cycle's own `timezone`. Both dates are optional, so a cycle can exist as an unscheduled container and be given dates later.
+
+[Learn more about cycles](https://docs.plane.so/core-concepts/cycles)
+
+
+
+
+## The cycle object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the cycle.
+
+- `name` _string_
+
+ Display name, unique within the project. Maximum 255 characters.
+
+- `description` _string_
+
+ Free-form description of what the cycle covers.
+
+- `start_date` _string (date-time)_
+
+ When the cycle opens. `null` if the cycle has not been scheduled.
+
+- `end_date` _string (date-time)_
+
+ When the cycle closes. `null` if the cycle has not been scheduled.
+
+- `timezone` _string_
+
+ The IANA time zone the cycle's dates are interpreted in, for example `America/New_York` or `UTC`. This is what makes a cycle boundary land at local midnight rather than UTC midnight for a distributed team.
+
+- `owned_by_id` _string (uuid)_
+
+ The user who owns the cycle. Read-only — assigned by Plane and not settable through the API. Use it to filter cycles on list.
+
+- `sort_order` _number_
+
+ Ordering weight for the cycle within the project. Lower values sort first when you order by `sort_order`.
+
+- `logo_props` _any_
+
+ JSON blob holding the cycle's icon configuration, as written by Plane clients. Passed through unchanged.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map a cycle to a record in another system and find it again later — both are exposed as list filters.
+
+- `created_at` _string (date-time)_
+
+ When the cycle was created.
+
+- `created_by_id` _string (uuid)_
+
+ The user who created the cycle.
+
+::: info Dates are timestamps, not calendar days
+`start_date` and `end_date` are full date-times. Send them as ISO 8601 timestamps and read `timezone` to interpret where the boundary falls.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | -------------------------------------------------------------- | -------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/cycles/` | List cycles |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/cycles/` | Create a cycle |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/` | Get a cycle |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/` | Update a cycle |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/` | Delete a cycle |
+
+## Names are unique per project
+
+Cycle names must be unique within a project. Creating a second cycle with a name already in use, or renaming a cycle onto an existing name, returns `409 conflict`. Uniqueness is scoped to the project, so two different projects can both have a `Sprint 24`.
+
+## Ownership is read-only
+
+`owned_by_id` is returned on every read but is not part of the write body — you cannot assign a cycle owner through the v2 API. You can still filter by owner on list with `?owned_by_id=`, which is the common case for building "my cycles" views.
+
+## No `?expand=` on cycles
+
+Cycles do not support `?expand=`. Related records are referenced by id only — resolve `owned_by_id` and `created_by_id` against the members endpoints when you need names.
+
+## Cycle work items are v1-only
+
+v2 does not yet expose the cycle work-item sub-resources. Adding work items to a cycle, listing a cycle's work items, removing one, or transferring work items between cycles are all still v1 operations.
+
+::: warning Use v1 for cycle membership
+Create and manage the cycle itself in v2, then use the v1 cycle work-item endpoints to populate it. See the
+[v1 cycle reference](/api-reference/v1/cycle/overview).
+:::
+
+Archiving is likewise not part of the v2 cycles surface — v2 exposes create, read, update, and delete only.
+
+## Changed from v1
+
+- `owned_by` is now **`owned_by_id`**, and `created_by` is now **`created_by_id`**.
+- `start_date` and `end_date` are **date-times**, not plain calendar dates.
+- Reads no longer return `updated_at`, `updated_by`, `project`, `workspace`, or `view_props`.
+- The list endpoint drops `cycle_view`, `expand`, and `fields`, and adds `search`, `owned_by_id`, `external_id`, and `external_source` filters.
+- Updates are `PATCH` only — v2 has no `PUT`.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
diff --git a/docs/api-reference/v2/cycles/update-cycle.md b/docs/api-reference/v2/cycles/update-cycle.md
new file mode 100644
index 00000000..a45e7c22
--- /dev/null
+++ b/docs/api-reference/v2/cycles/update-cycle.md
@@ -0,0 +1,240 @@
+---
+title: Update a cycle
+description: Partially update a Plane cycle with the v2 REST API. PATCH body parameters, date and time zone changes, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update cycle, patch cycle, reschedule cycle, start_date, end_date, PATCH cycles
+---
+
+# Update a cycle
+
+
+
+Change a cycle in place — rename it, reschedule it, or attach correlation ids after an import.
+
+The update is partial. Fields you omit are left untouched, and omitting a field is not the same as sending `null`: send `"end_date": null` to clear a date, omit `end_date` to keep it.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the cycle belongs to.
+
+
+
+
+
+The cycle to update.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+New display name, unique within the project. Maximum 255 characters. Renaming onto a name another cycle already holds returns `409 conflict`.
+
+
+
+
+
+Free-form description of what the cycle covers.
+
+
+
+
+
+New opening date-time, in ISO 8601. Send `null` to unschedule the start.
+
+
+
+
+
+New closing date-time, in ISO 8601. Send `null` to unschedule the end.
+
+
+
+
+
+The IANA time zone the cycle's dates are interpreted in, for example `America/New_York` or `UTC`. Changing it re-anchors where the existing boundaries fall locally, so send it together with the dates when you are moving a cycle between regions. Any value outside the IANA list is rejected with `400 validation_error`.
+
+
+
+
+
+Ordering weight for the cycle within the project. Lower values sort first when you list with `?order_by=sort_order`.
+
+
+
+
+
+JSON blob holding the cycle's icon configuration. Replaces the stored value outright — it is not merged key by key.
+
+
+
+
+
+Your system's identifier for this cycle. Maximum 255 characters, nullable.
+
+
+
+
+
+The system `external_id` came from, for example `jira`. Maximum 255 characters, nullable.
+
+
+
+
+
+
+::: warning There is no `PUT`
+v2 updates are `PATCH` only. A `PUT` to this path returns `405 method_not_allowed`. Audit fields such as `created_at`, `created_by_id`, and `owned_by_id` are read-only — including them has no effect.
+:::
+
+
+
+### Scopes
+
+`projects.cycles:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | --------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A name over 255 characters, an unparseable date, or a `timezone` outside the IANA list. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't update cycles in this project. |
+| `404` | `resource_not_found` | No such cycle, wrong project, or the record is outside your tenant. |
+| `409` | `conflict` | Another cycle in the project already uses this name. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/errors.md b/docs/api-reference/v2/errors.md
new file mode 100644
index 00000000..45a7c110
--- /dev/null
+++ b/docs/api-reference/v2/errors.md
@@ -0,0 +1,258 @@
+---
+title: Errors
+description: The Plane API v2 error model — RFC 9457 problem+json responses, the full error code table, validation error arrays, rate limiting, and mode conflicts.
+keywords: plane api v2 errors, rfc 9457, problem json, validation_error, resource_not_found, rate_limited, retry-after, plane api error codes
+---
+
+# Errors
+
+Every v2 error is an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem document, served as
+`application/problem+json`. The shape is the same whether the failure is a typo in a request body or a throttled
+client, so one error handler covers the whole API.
+
+## The problem shape
+
+
+
+
+### Fields
+
+- `type` _string (uri)_
+
+ A URI identifying the error class, for example `https://api.plane.so/errors/validation_error`. Stable, but meant for
+ humans following the link — do not parse it.
+
+- `title` _string_
+
+ A short human-readable summary of the error class, the same for every occurrence.
+
+- `status` _integer_
+
+ The HTTP status code, repeated in the body so a logged payload is self-contained.
+
+- `code` _string_
+
+ **The stable machine-readable identifier.** This is the field to branch on.
+
+- `detail` _string_
+
+ A human-readable explanation of this specific occurrence. Written for a developer reading a log — the wording can
+ change between releases.
+
+- `errors` _array_
+
+ Present **only** on `validation_error`. One entry per rejected field, each with a `field` and a `message`.
+
+::: warning Branch on `code`, not on status or prose
+Two different failures share status `403` (`forbidden` and `workflow_transition_denied`) and three share `400`. The
+HTTP status alone cannot tell them apart.
+
+`detail` is written for people and its wording is not part of the contract. Matching on its text will break.
+:::
+
+
+
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/validation_error",
+ "title": "Validation Error",
+ "status": 400,
+ "code": "validation_error",
+ "detail": "One or more fields failed validation.",
+ "errors": [
+ { "field": "name", "message": "This field is required." },
+ { "field": "group", "message": "\"in_review\" is not a valid choice." }
+ ]
+}
+```
+
+
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/resource_not_found",
+ "title": "Not Found",
+ "status": 404,
+ "code": "resource_not_found",
+ "detail": "The requested resource was not found."
+}
+```
+
+
+
+
+
+
+## Handling errors
+
+Read `code` first. Fall back to `status` only for codes you do not recognize yet — new codes can appear as v2 grows.
+
+
+
+
+```bash
+# -s keeps curl quiet, -w prints the status so you can see both parts
+curl -s -w "\n%{http_code}\n" \
+ "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"color": "#3f76ff"}'
+```
+
+
+
+
+```python
+import requests
+
+response = requests.post(
+ "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/",
+ headers={"X-Api-Key": "your-api-key"},
+ json={"color": "#3f76ff"},
+)
+
+if not response.ok:
+ problem = response.json()
+ if problem["code"] == "validation_error":
+ for item in problem["errors"]:
+ print(f"{item['field']}: {item['message']}")
+ elif problem["code"] == "rate_limited":
+ retry_after = int(response.headers.get("Retry-After", "1"))
+ print(f"Throttled, retry in {retry_after}s")
+ else:
+ print(problem["code"], problem["detail"])
+```
+
+
+
+
+```javascript
+const response = await fetch(
+ "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/",
+ {
+ method: "POST",
+ headers: {
+ "X-Api-Key": "your-api-key",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ color: "#3f76ff" }),
+ }
+);
+
+if (!response.ok) {
+ const problem = await response.json();
+ switch (problem.code) {
+ case "validation_error":
+ problem.errors.forEach((e) => console.error(`${e.field}: ${e.message}`));
+ break;
+ case "rate_limited":
+ console.error(`Throttled, retry in ${response.headers.get("Retry-After")}s`);
+ break;
+ default:
+ console.error(problem.code, problem.detail);
+ }
+}
+```
+
+
+
+
+## Error codes
+
+| Status | Code | Meaning |
+| ------ | -------------------------------------- | --------------------------------------------------------------------------- |
+| `400` | `validation_error` | Malformed or invalid body or query parameter. Includes an `errors[]` array. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Authenticated, but your role or token scope can't do this. |
+| `403` | `workflow_transition_denied` | A workflow rule blocked the create or state transition. |
+| `404` | `resource_not_found` | No such resource, or it's outside your tenant. |
+| `405` | `method_not_allowed` | Method not supported on this route — most often a `PUT`. |
+| `409` | `conflict` | Uniqueness or protected-state conflict. |
+| `409` | `work_item_types_managed_at_workspace` | Wrong mode — this workspace manages work item types at the workspace level. |
+| `409` | `work_item_types_managed_at_project` | Wrong mode — this workspace manages work item types at the project level. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header. |
+
+Two more `400` codes come from pagination:
+
+| Status | Code | Meaning |
+| ------ | ------------------------------ | ------------------------------------------------------------------------ |
+| `400` | `count_pagination_disabled` | Offset and COUNT are disabled for this resource. Use `?paginate=cursor`. |
+| `400` | `ordering_not_cursor_eligible` | This ordering can't be combined with cursor pagination. Use offset. |
+
+An unexpected server-side failure returns `500` with code `internal_error`. Retry it; if it persists, it is not
+something your request can fix.
+
+## Validation failures
+
+A `400 validation_error` is the only code that carries `errors[]`. Each entry names one rejected field, and a single
+response can carry several — the API validates the whole payload rather than stopping at the first problem, so one
+round trip tells you everything to fix.
+
+Query parameters are validated too. Enum-backed parameters such as `priority`, `state_group`, state `group`, and module
+`status` are checked against their allowed values, so a typo returns a clean `400` rather than a silently empty list.
+
+::: tip Surface `field` and `message` verbatim
+When you are relaying an error to a human — a CLI, a form, a Slack notification — the `field`/`message` pairs are
+already specific enough to act on. Passing them through beats collapsing them into "invalid request".
+:::
+
+## Rate limiting
+
+Requests are throttled per token, with a separate bucket per token class — API keys, OAuth tokens, workspace tokens,
+service tokens, and external tokens. Exceeding a bucket returns `429 rate_limited` with a `Retry-After` header.
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/rate_limited",
+ "title": "Rate Limited",
+ "status": 429,
+ "code": "rate_limited",
+ "detail": "Rate limit exceeded."
+}
+```
+
+
+
+Wait the number of seconds in `Retry-After` before retrying. Backing off on a fixed schedule instead — or retrying
+immediately — keeps you in the throttle. Because the buckets are per token, splitting a bulk job across several tokens
+does not merge their limits, but it does multiply the load on the workspace.
+
+## Mode conflicts
+
+A workspace manages work item types in exactly one mode: at the **project** level or at the **workspace** level. Both
+sets of endpoints exist at all times, so writing to the surface that is not currently in use returns `409` — not `404`
+or `403`. The capability is real, it just lives on the other surface.
+
+- Writing to a **project-mode** endpoint while workspace mode is on → `work_item_types_managed_at_workspace`.
+- Writing to a **workspace-mode** endpoint while project mode is on → `work_item_types_managed_at_project`.
+
+::: info Reads are never blocked by mode
+Only writes conflict. You can list and read types and properties on either surface regardless of the active mode — a
+project still surfaces its imported types while the workspace is in workspace mode.
+:::
+
+The code tells you where to send the write. See
+[Work item type modes](/api-reference/v2/work-item-type-modes) for the full picture.
+
+## Pagination errors
+
+Both pagination `400`s mean the same thing: the pagination style and the request do not fit together.
+
+- `ordering_not_cursor_eligible` — some orderings sort by meaning rather than by a stored column, for example
+ `?order_by=priority`. Those cannot back a keyset cursor. Drop `?paginate=cursor` and use offset for that ordering.
+- `count_pagination_disabled` — offset with a COUNT is unavailable for this resource. Switch to `?paginate=cursor`.
+
+See [Pagination](/api-reference/v2/pagination) for the two envelopes and when to prefer each.
+
+## Related
+
+- [Authentication](/api-reference/v2/authentication) — what separates `401`, `403`, and a tenant-safe `404`.
+- [Pagination](/api-reference/v2/pagination) — offset and cursor styles.
+- [Introduction](/api-reference/v2/introduction) — request conventions, including why a missing trailing slash bites.
diff --git a/docs/api-reference/v2/expanding-relations.md b/docs/api-reference/v2/expanding-relations.md
new file mode 100644
index 00000000..484ec8a6
--- /dev/null
+++ b/docs/api-reference/v2/expanding-relations.md
@@ -0,0 +1,247 @@
+---
+title: Expanding relations
+description: How ?expand= works in Plane API v2. Sparse reads, the separate-key expansion contract, which resources support expansion, and the exact shape of each expanded object.
+keywords: plane api v2, expand, sparse fields, state_id, assignee_ids, embedded relations, expand state, expand assignees, expand member
+---
+
+# Expanding relations
+
+v2 reads are **sparse by default**. A response gives you scalars plus identifiers for everything related to it — never an embedded object tree:
+
+- A to-one relation comes back as **`_id`**: `state_id`, `type_id`, `parent_id`, `created_by_id`.
+- A to-many relation comes back as **`_ids`**, an array: `assignee_ids`, `label_ids`.
+
+`?expand=` opts a single request into richer output for specific relations. It takes a comma-separated list:
+
+```bash
+GET .../work-items/?expand=state,assignees
+```
+
+## Expansion is separate-key
+
+This is the part that trips people up, so it is worth stating flatly:
+
+::: warning `?expand=` adds a key. It never replaces one.
+`?expand=state` keeps `state_id` exactly where it was **and** adds a separate `state` object beside it. The identifier is not swapped for an object, and it does not disappear. Both are present in the same response.
+:::
+
+Most APIs that offer expansion do the opposite — the `state` key holds a UUID string when you do not expand and an object when you do. That design forces every consumer to write `typeof x === "string" ? x : x.id` at each use site, and it means the shape of your response depends on a query parameter that some other part of your codebase set.
+
+v2 splits the two concerns into two keys with two stable types:
+
+| Key | Type | Present when |
+| ---------- | --------------- | ------------------------- |
+| `state_id` | `string (uuid)` | Always |
+| `state` | object | Only with `?expand=state` |
+
+### Why this matters for your client
+
+- **One type per field, forever.** `state_id` is a `string` in every response your code will ever see. You never union a string with an object, and you never write a type guard to tell them apart.
+- **Expansion is additive, so it is safe to change.** A caller can add or drop `?expand=` without invalidating any code that reads `state_id`. Your identifier-keyed caches, join tables, and foreign keys keep working untouched.
+- **Optional, not conditional.** In a typed client, the expanded key is simply optional — `state?: State`. Compare that with a discriminated union of `string | State`, which every consumer has to narrow.
+- **Nothing to reconcile.** Because both keys are present, `item.state.id` and `item.state_id` never disagree.
+
+The practical rule: **read identifiers from `*_id` / `*_ids`, and treat expanded objects purely as a display convenience** that saves you a round trip.
+
+## Where `?expand=` is supported
+
+`?expand=` is supported on exactly two resource families. This is the complete list.
+
+| Resource | Allowed `expand` values |
+| --------------------------------------------- | ------------------------------------------------ |
+| **Work items** | `state`, `type`, `parent`, `assignees`, `labels` |
+| **Workspace members** and **project members** | `member` |
+
+::: danger Every other v2 resource does not support `?expand=`
+States, labels, cycles, modules, comments, work item types, properties, property options, property contexts, workspace features, audit logs — none of them accept `?expand=`. There is no partial support and no silent ignoring: any value you pass to a resource with no expandable relations is an unknown value, and unknown values are rejected.
+:::
+
+Passing a value the resource does not declare returns a `400`:
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/validation_error",
+ "title": "Validation Error",
+ "status": 400,
+ "code": "validation_error",
+ "detail": "One or more fields failed validation.",
+ "errors": [{ "field": "expand", "message": "Unknown expand value(s): project." }]
+}
+```
+
+
+
+::: info `?expand=` is not in the published OpenAPI schema
+The API supports `?expand=` on the resources above, but the parameter is currently **absent from the served OpenAPI document** at `/api/v2/schema/`. Generated SDKs and schema-driven request validators will not know about it, and a strict client may strip it. This page is the reference until the schema catches up — the runtime behavior described here is what the API actually does.
+:::
+
+## Before and after
+
+Take the same work item list, once sparse and once expanded.
+
+**Sparse (default)**
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "data": [
+ {
+ "id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
+ "name": "Fix login redirect",
+ "identifier": "PROJ-118",
+ "sequence_id": 118,
+ "priority": "high",
+ "state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "type_id": null,
+ "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
+ "label_ids": [],
+ "parent_id": null,
+ "start_date": null,
+ "target_date": "2026-02-02",
+ "is_draft": false,
+ "archived_at": null,
+ "created_at": "2026-01-14T09:22:41.478363Z",
+ "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "custom_fields": null
+ }
+ ],
+ "next": null,
+ "previous": null,
+ "total_count": 1,
+ "pagination": { "style": "offset" }
+}
+```
+
+
+
+**Expanded**
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?expand=state,assignees" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "data": [
+ {
+ "id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
+ "name": "Fix login redirect",
+ "identifier": "PROJ-118",
+ "sequence_id": 118,
+ "priority": "high",
+ "state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "type_id": null,
+ "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
+ "label_ids": [],
+ "parent_id": null,
+ "start_date": null,
+ "target_date": "2026-02-02",
+ "is_draft": false,
+ "archived_at": null,
+ "created_at": "2026-01-14T09:22:41.478363Z",
+ "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "custom_fields": null,
+ "state": {
+ "id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "name": "In Progress",
+ "color": "#3f76ff",
+ "group": "started"
+ },
+ "assignees": [
+ {
+ "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "display_name": "Priya Raghavan",
+ "avatar_url": "https://assets.plane.so/avatars/16c61a3a.png",
+ "email": "priya@example.com"
+ }
+ ]
+ }
+ ],
+ "next": null,
+ "previous": null,
+ "total_count": 1,
+ "pagination": { "style": "offset" }
+}
+```
+
+
+
+Note what did **not** change: `state_id` is still `"f960d3c2-…"` and `assignee_ids` is still `["16c61a3a-…"]`. The two new keys sit alongside them.
+
+## Shape of each expanded object
+
+Expanded objects are deliberately minimal projections — enough to render a row without a second request, and nothing more. They are leaves: an expanded object never carries its own expansions or nested relations.
+
+### Work items
+
+| `expand` value | Adds key | Shape |
+| -------------- | ----------- | ---------------------------------------------------- |
+| `state` | `state` | `id`, `name`, `color`, `group` |
+| `type` | `type` | `id`, `name`, `logo_props`, `is_epic` |
+| `parent` | `parent` | `id`, `name`, `sequence_id` |
+| `assignees` | `assignees` | Array of `id`, `display_name`, `avatar_url`, `email` |
+| `labels` | `labels` | Array of `id`, `name`, `color` |
+
+### Members
+
+| `expand` value | Adds key | Shape |
+| -------------- | -------- | ------------------------------------------- |
+| `member` | `member` | `id`, `display_name`, `avatar_url`, `email` |
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/members/?expand=member" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "data": [
+ {
+ "id": "c1b7e4a9-2f66-4a1d-9c3b-7d5e2f8a1b40",
+ "member_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "role": "admin",
+ "member": {
+ "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "display_name": "Priya Raghavan",
+ "avatar_url": "https://assets.plane.so/avatars/16c61a3a.png",
+ "email": "priya@example.com"
+ }
+ }
+ ],
+ "next": null,
+ "previous": null,
+ "total_count": 1,
+ "pagination": { "style": "offset" }
+}
+```
+
+
+
+Here too the identifier survives: `member_id` is the user's id, and `member` is the object.
+
+## Behavior notes
+
+- **A to-many expansion is always an array, never `null`.** With `?expand=assignees`, an unassigned work item gets `"assignees": []`.
+- **A to-one expansion follows its id.** If `parent_id` is `null`, `?expand=parent` produces `"parent": null`.
+- **Expansion works on detail routes too**, not only lists — `GET .../work-items/{pk}/?expand=state,labels` behaves the same way.
+- **Expanding a list does not cost a query per row.** The relations are loaded in bulk, so `?expand=assignees` on a 200-row page is one additional query, not 200.
+- **Expansion is read-only.** Writes always take ids (`state_id`, `assignee_ids`); sending a nested object on a `POST` or `PATCH` does not create or link anything.
+
+## Related
+
+- [Filtering and ordering](/api-reference/v2/filtering-and-ordering) — filter on the same relations with `state_id`, `assignee_id`, and friends
+- [Pagination](/api-reference/v2/pagination) — the envelope that wraps every expanded list
+- [Migrating from v1](/api-reference/v2/migrating-from-v1) — what to do about v1 responses that embedded these objects by default
diff --git a/docs/api-reference/v2/filtering-and-ordering.md b/docs/api-reference/v2/filtering-and-ordering.md
new file mode 100644
index 00000000..3f86b4da
--- /dev/null
+++ b/docs/api-reference/v2/filtering-and-ordering.md
@@ -0,0 +1,204 @@
+---
+title: Filtering and ordering
+description: How Plane API v2 list queries work. Exact, membership, null and range filters, relation filters by id, free-text search, and semantic ordering with order_by.
+keywords: plane api v2, filtering, query parameters, order_by, search, state_group, priority, __in, __isnull, __gte, __lte
+---
+
+# Filtering and ordering
+
+v2 list endpoints draw their query parameters from the same four families: **filters** that narrow the set, **search** that matches free text, **ordering** that sorts what is left, and **pagination** that slices it. Every list endpoint supports ordering and pagination; each one declares its own filter and search surface, and a few smaller collections — work item types, properties, property options, property contexts — offer only ordering and pagination. Where they are available the four families compose, and every filter you add narrows the result further (they are combined with AND, never OR).
+
+Filters run after authorization, so a filter can only ever shrink the set of rows you were already allowed to see. There is no filter that widens access.
+
+## Filter shapes
+
+Public parameter names never contain `__`. The suffixes below are variants of a base name, and each list endpoint declares exactly which variants it supports.
+
+| Shape | Parameter form | Example | Matches |
+| -------------- | ----------------------------- | --------------------------------------- | --------------------------------------- |
+| **Exact** | `` | `?priority=urgent` | Rows equal to the value |
+| **Membership** | `__in` | `?priority__in=urgent,high` | Rows equal to any comma-separated value |
+| **Null** | `__isnull` | `?parent_id__isnull=true` | Rows where the field is (or is not) set |
+| **Range** | `__gte` / `__lte` | `?created_at__gte=2026-01-01T00:00:00Z` | Rows on or after / on or before a bound |
+
+A few things that are easy to get wrong:
+
+- `__in` takes a **comma-separated list in a single parameter** — `?priority__in=urgent,high`. Repeating the parameter is not the same thing.
+- `__gte` and `__lte` are inclusive on both ends. Pair them to express a window; send one alone for an open-ended bound.
+- Date-time ranges want an ISO 8601 timestamp (`2026-01-01T00:00:00Z`); date ranges want a plain date (`2026-01-01`). Which one a parameter takes follows the field it filters.
+- Not every base name offers every variant. `state_id` supports `__in` but not `__isnull`, because a work item always has a state. Check the endpoint's own parameter list rather than assuming the full set.
+
+## Filtering by a relation
+
+Filter a relation with its **`*_id` parameter**. There is no bare-name form — it is `state_id`, not `state`.
+
+| Resource | Relation filters |
+| ---------- | -------------------------------------------------------------------------------------- |
+| Work items | `state_id`, `assignee_id`, `label_id`, `parent_id`, `cycle_id`, `module_id`, `type_id` |
+| Cycles | `owned_by_id` |
+| Modules | `lead_id` |
+| Labels | `parent_id` |
+| Members | `member_id` |
+
+This mirrors how writes work — you set a relation with `state_id`, and you filter on it with `state_id` too. See [Migrating from v1](/api-reference/v2/migrating-from-v1) if you are used to v1's embedded relation objects.
+
+```bash
+# every cycle owned by one person
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/cycles/?owned_by_id=16c61a3a-512a-48ac-b0be-b6b46fe6f430" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+
+# modules a person leads that are underway
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/modules/?lead_id=16c61a3a-512a-48ac-b0be-b6b46fe6f430&status=in-progress" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+## Enum-backed filters are validated
+
+Parameters backed by an enum — work item `priority` and `state_group`, state `group`, module `status`, comment `access` — are checked against their allowed values before the query runs. A value outside the enum is a clean `400 validation_error` naming the offending field.
+
+This matters more than it sounds. In an API that ignores unknown filter values, `?state_group=in_progress` (a plausible-looking guess; the real value is `started`) returns `200` with an empty list, and you spend an afternoon deciding whether the project is genuinely empty. In v2 it fails immediately:
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/validation_error",
+ "title": "Validation Error",
+ "status": 400,
+ "code": "validation_error",
+ "detail": "One or more fields failed validation.",
+ "errors": [
+ { "field": "state_group", "message": "Select a valid choice. in_progress is not one of the available choices." }
+ ]
+}
+```
+
+
+
+The `__in` variants validate every element of the list, not just the first.
+
+## Search
+
+`?search=` is a case-insensitive partial match over the resource's searchable text. It is a convenience for lookups, not a query language — no operators, no field prefixes, no quoting.
+
+| Resource | Searches |
+| ------------------------------- | ---------------------------------------------------------------- |
+| Work items | Name |
+| States, labels, cycles, modules | Name |
+| Work item comments | Comment text |
+| Members | Display name, email, first name, last name |
+| Audit logs | Event name, actor display name, actor email, target display name |
+
+Search composes with filters, so `?search=login&priority=urgent` finds urgent work items whose name contains "login".
+
+## Ordering
+
+`?order_by=` sorts the list. Prefix the field with `-` for descending order:
+
+```bash
+?order_by=created_at # oldest first
+?order_by=-created_at # newest first
+```
+
+Each resource declares its own allowed fields. Unlike the enum-backed _filters_ above, an unrecognized `order_by` does not fail — it falls back to the resource's default ordering. If a sort looks wrong, check the spelling against the endpoint's allowed values before assuming the data is wrong. Every ordering carries a unique tiebreak internally, so a page boundary never drops or duplicates a row just because two rows share a sort key.
+
+### Semantic orderings
+
+Two work item orderings sort by **meaning rather than by string**:
+
+- `order_by=priority` sorts `urgent` → `high` → `medium` → `low` → `none`. Alphabetically that would be `high, low, medium, none, urgent`, which is useless.
+- `order_by=state_group` sorts `backlog` → `unstarted` → `started` → `completed` → `cancelled` → `triage`, following the workflow rather than the alphabet.
+
+`-priority` and `-state_group` reverse those sequences.
+
+::: warning Semantic orderings are offset-only
+`priority` and `state_group` cannot be combined with `?paginate=cursor` — a cursor needs a unique, monotonic sort key and these have a handful of distinct values. The pairing returns `400 ordering_not_cursor_eligible`. `sort_order` is not cursor-eligible either, for the same reason. On work items the cursor-eligible orderings are `created_at`, `updated_at`, `sequence_id`, and `id`. See [Pagination](/api-reference/v2/pagination#not-every-ordering-can-use-a-cursor).
+:::
+
+## Worked example: the work items list
+
+The work items list is the largest filter surface in v2. Its parameters, grouped by concept:
+
+**Relations**
+
+| Base name | Variants | Type |
+| ------------- | ------------------ | --------------- |
+| `state_id` | `__in` | `string (uuid)` |
+| `type_id` | `__in` | `string (uuid)` |
+| `assignee_id` | `__in`, `__isnull` | `string (uuid)` |
+| `label_id` | `__in`, `__isnull` | `string (uuid)` |
+| `parent_id` | `__in`, `__isnull` | `string (uuid)` |
+| `cycle_id` | `__in`, `__isnull` | `string (uuid)` |
+| `module_id` | `__in`, `__isnull` | `string (uuid)` |
+
+**Enums and scalars**
+
+| Name | Variants | Type | Values |
+| ----------------- | -------- | --------- | --------------------------------------------------------------------- |
+| `priority` | `__in` | `string` | `urgent`, `high`, `medium`, `low`, `none` |
+| `state_group` | `__in` | `string` | `backlog`, `unstarted`, `started`, `completed`, `cancelled`, `triage` |
+| `is_draft` | — | `boolean` | `true`, `false` |
+| `sequence_id` | — | `integer` | The number in a `PROJ-123` identifier |
+| `external_id` | — | `string` | Your system's id, for sync correlation |
+| `external_source` | — | `string` | The system `external_id` came from |
+
+**Ranges**
+
+| Base name | Variants | Type |
+| ------------- | ---------------- | -------------------- |
+| `created_at` | `__gte`, `__lte` | `string (date-time)` |
+| `updated_at` | `__gte`, `__lte` | `string (date-time)` |
+| `start_date` | `__gte`, `__lte` | `string (date)` |
+| `target_date` | `__gte`, `__lte` | `string (date)` |
+
+Date fields are range-only — there is no exact `?created_at=` filter. To match a single day, bracket it: `?created_at__gte=2026-01-14T00:00:00Z&created_at__lte=2026-01-14T23:59:59Z`.
+
+**Search** — `search`.
+
+**Ordering** — `order_by`, one of `created_at`, `updated_at`, `sequence_id`, `id`, `sort_order`, `priority`, `state_group`, each also available with a `-` prefix.
+
+**Pagination** — `per_page`, `offset`, `count`, `paginate`, `cursor`. See [Pagination](/api-reference/v2/pagination).
+
+## Combined filters
+
+```bash
+# urgent and high-priority items that are in flight or already done
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?priority__in=urgent,high&state_group__in=started,completed" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+```bash
+# one person's open work, most urgent first
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?assignee_id=16c61a3a-512a-48ac-b0be-b6b46fe6f430&state_group__in=backlog,unstarted,started&order_by=priority" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+```bash
+# an incremental sync: everything touched since the last run, oldest first
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?updated_at__gte=2026-01-14T09:22:41Z&order_by=updated_at&paginate=cursor&per_page=200" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+```bash
+# top-level items only — nothing that is a sub-item of something else
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?parent_id__isnull=true&is_draft=false" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+```bash
+# unassigned work due this quarter, with the state object attached
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?assignee_id__isnull=true&target_date__gte=2026-01-01&target_date__lte=2026-03-31&expand=state" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+```bash
+# find the item you imported from another system
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?external_source=jira&external_id=ENG-4417" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+## Related
+
+- [Pagination](/api-reference/v2/pagination) — the envelopes, and which orderings a cursor can use
+- [Expanding relations](/api-reference/v2/expanding-relations) — attaching related objects to filtered results
+- [Errors](/api-reference/v2/errors) — the `validation_error` body and its `errors[]` array
diff --git a/docs/api-reference/v2/introduction.md b/docs/api-reference/v2/introduction.md
new file mode 100644
index 00000000..3746434f
--- /dev/null
+++ b/docs/api-reference/v2/introduction.md
@@ -0,0 +1,208 @@
+---
+title: Plane API v2
+description: Introduction to the Plane REST API v2 — base URL, trailing slashes, PATCH-only writes, sparse reads, RFC 9457 errors, and a quickstart request.
+keywords: plane api v2, rest api, plane api introduction, api.plane.so, plane api quickstart, openapi schema, plane developer api
+---
+
+# Plane API v2
+
+API v2 is the current, recommended REST interface to Plane. It is resource-oriented, JSON in and JSON out, and
+described by a machine-readable OpenAPI document you can generate clients from.
+
+Everything here is stable and actively developed. [API v1](/api-reference/v1/introduction) remains available and
+supported for existing integrations, but it is in maintenance — build new integrations on v2.
+
+::: tip Already on v1?
+The two versions run side by side, so you can migrate one resource at a time. See
+[Migrating from v1](/api-reference/v2/migrating-from-v1) for the field-by-field differences.
+:::
+
+## Base URL
+
+| Deployment | Base URL |
+| ------------ | ---------------------------------------------------------- |
+| Plane Cloud | `https://api.plane.so` |
+| Self-managed | Your instance URL, for example `https://plane.example.com` |
+
+Every v2 path lives under `/api/v2/`, and **every URL ends with a trailing slash**. Dropping it is the single most
+common first-request mistake — `/api/v2/users/me` is not the same URL as `/api/v2/users/me/`.
+
+The OpenAPI document advertises one `servers` entry that matches your deployment, so a generated client points at the
+right host without further configuration.
+
+## Quickstart
+
+`GET /api/v2/users/me/` needs no workspace or project id, which makes it the fastest way to confirm your credentials
+work.
+
+
+
+
+```bash
+curl "https://api.plane.so/api/v2/users/me/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+
+```python
+import requests
+
+response = requests.get(
+ "https://api.plane.so/api/v2/users/me/",
+ headers={"X-Api-Key": "your-api-key"},
+)
+print(response.json())
+```
+
+
+
+
+```javascript
+const response = await fetch("https://api.plane.so/api/v2/users/me/", {
+ headers: { "X-Api-Key": "your-api-key" },
+});
+const data = await response.json();
+```
+
+
+
+
+
+
+```json
+{
+ "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "email": "priya@my-team.io",
+ "display_name": "priya",
+ "principal_kind": "api_key",
+ "scopes": []
+}
+```
+
+
+
+If that returns `200`, your key is valid. If it returns `401`, the header name or the key itself is wrong — see
+[Authentication](/api-reference/v2/authentication).
+
+## Requests
+
+- **JSON only.** Send `Content-Type: application/json` on requests with a body; responses are JSON.
+- **`POST` creates, `PATCH` updates.** There is no `PUT` — sending one returns `405 method_not_allowed`.
+- **`PATCH` is partial.** Only the fields you send are changed. Omitting a field leaves it untouched, which is not the
+ same as sending `null` to clear it.
+- **`DELETE` returns `204`** with an empty body. Deletes are soft.
+- **Writes take ids, not nested objects.** Set a relation with its `*_id` field (`state_id`, `parent_id`, `lead_id`) or
+ its `*_ids` array (`assignee_ids`, `label_ids`).
+- **Audit fields are read-only.** `created_at`, `created_by_id`, and their siblings are set server-side; sending them
+ has no effect.
+
+```bash
+# Create
+curl -X POST \
+ "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "In Review", "color": "#3f76ff", "group": "started"}'
+
+# Partial update — color changes, nothing else does
+curl -X PATCH \
+ "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/f960d3c2-8524-4a41-b8eb-055ce4be2a7f/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"color": "#ffffff"}'
+```
+
+## Design principles
+
+These five rules hold across every v2 endpoint. Learning them once means you rarely have to re-read a reference page.
+
+### Reads are sparse
+
+A read returns foreign keys as `*_id` fields and many-to-many relations as `*_ids` arrays. No related objects are
+embedded by default, so response size stays predictable no matter how many relations a resource has.
+
+```json
+{
+ "id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
+ "name": "Fix login redirect",
+ "state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"]
+}
+```
+
+### Expansion is separate-key
+
+`?expand=` **adds** an object beside the id — it never replaces it. `?expand=state` gives you `state_id` _and_ a
+`state` object, so code that reads `state_id` keeps working whether or not the caller asked for the expansion.
+
+Only work items and members support `?expand=`. See
+[Expanding relations](/api-reference/v2/expanding-relations) for the allowed values.
+
+### Errors are RFC 9457
+
+Every failure is `application/problem+json` with a stable machine-readable `code`. Branch on `code`, never on the HTTP
+status or the human-readable `detail`. See [Errors](/api-reference/v2/errors).
+
+### Cross-tenant ids return 404
+
+A workspace, project, or record id that belongs to someone else returns `404 resource_not_found`, never `403`. The API
+does not distinguish "does not exist" from "exists but is not yours", so it never leaks the existence of a resource you
+cannot see.
+
+::: warning A `404` is not always a typo
+If a `404` surprises you on an id you are confident is real, check which workspace or project the id belongs to before
+checking your spelling.
+:::
+
+### v2 evolves additively
+
+Inside `v2` the contract only grows: new optional fields, new enum values, new endpoints. Removals and renames are
+reserved for a future major version. Write clients that ignore unknown response fields and tolerate unrecognized enum
+values, and upgrades stay uneventful.
+
+## What's in v2
+
+88 operations across these resources.
+
+| Resource | Base path | Reference |
+| --------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| Work items | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/` | [Overview](/api-reference/v2/work-items/overview) |
+| Work item comments | `…/work-items/{work_item_id}/comments/` | [Overview](/api-reference/v2/work-item-comments/overview) |
+| States | `…/projects/{project_id}/states/` | [Overview](/api-reference/v2/states/overview) |
+| Labels | `…/projects/{project_id}/labels/` | [Overview](/api-reference/v2/labels/overview) |
+| Cycles | `…/projects/{project_id}/cycles/` | [Overview](/api-reference/v2/cycles/overview) |
+| Modules | `…/projects/{project_id}/modules/` | [Overview](/api-reference/v2/modules/overview) |
+| Work item types & properties, project | `…/projects/{project_id}/work-item-types/`, `…/work-item-properties/` | [Overview](/api-reference/v2/work-item-types/overview) |
+| Work item types & properties, workspace | `/api/v2/workspaces/{slug}/work-item-types/`, `…/work-item-properties/` | [Overview](/api-reference/v2/workspace-work-item-types/overview) |
+| Work item property contexts | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/` | [Overview](/api-reference/v2/work-item-property-contexts/overview) |
+| Members | `/api/v2/workspaces/{slug}/members/`, `…/projects/{project_id}/members/` | [Overview](/api-reference/v2/members/overview) |
+| Workspace features | `/api/v2/workspaces/{slug}/features/` | [Overview](/api-reference/v2/workspace-features/overview) |
+| Audit logs | `/api/v2/workspaces/{slug}/audit-logs/` | [Overview](/api-reference/v2/audit-logs/overview) |
+| Users | `/api/v2/users/me/` | [Overview](/api-reference/v2/users/overview) |
+
+::: info Two surfaces for work item types
+A workspace manages work item types either at the project level or at the workspace level — never both at once. Writing
+to the wrong surface returns a `409`. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+## Machine-readable schema
+
+The full OpenAPI document is served live at:
+
+```
+GET /api/v2/schema/
+```
+
+Point a generator at it to produce a typed client, or diff it between releases to see exactly what changed. It is the
+authority for every field, enum, and scope in this reference.
+
+## Where to go next
+
+- [Authentication](/api-reference/v2/authentication) — API keys, OAuth 2.0, and the scope model.
+- [Pagination](/api-reference/v2/pagination) — offset by default, cursor for deep traversal.
+- [Filtering and ordering](/api-reference/v2/filtering-and-ordering) — query parameters on list endpoints.
+- [Expanding relations](/api-reference/v2/expanding-relations) — where `?expand=` works and what it accepts.
+- [Errors](/api-reference/v2/errors) — the RFC 9457 problem shape and the full code table.
+- [Migrating from v1](/api-reference/v2/migrating-from-v1) — what changed and how to move.
diff --git a/docs/api-reference/v2/labels/create-label.md b/docs/api-reference/v2/labels/create-label.md
new file mode 100644
index 00000000..219d1aba
--- /dev/null
+++ b/docs/api-reference/v2/labels/create-label.md
@@ -0,0 +1,217 @@
+---
+title: Create a label
+description: Create a label in a Plane project with the v2 REST API. Body parameters, nesting with parent_id, name uniqueness, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create label, POST labels, parent_id, label color, external_id
+---
+
+# Create a label
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/labels/
+
+
+
+
+
+Add a label to a project. The new label is immediately available to every work item in that project, and to no other project.
+
+Label names must be unique within a project — reusing one returns `409 conflict`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to add the label to.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Display name for the label, unique within the project. Maximum 255 characters.
+
+
+
+
+
+Hex color used wherever the label is rendered, for example `#e5484d`. Maximum 255 characters. Send one if the label needs to be recognizable at a glance on a board.
+
+
+
+
+
+Free-form note about what the label is for and when to apply it.
+
+
+
+
+
+The label this one nests under, for grouping related tags such as `Billing` and `Search` beneath an `Area` label. The parent must be a label in the same project — an id from another project is a `400`, not a silent link.
+
+Omit it, or send `null`, for a top-level label.
+
+
+
+
+
+Ordering weight within the project. Lower values sort first. Assigned automatically when omitted, so send it only when you are recreating a specific order.
+
+
+
+
+
+Your system's identifier for this label, for sync and import correlation. Maximum 255 characters. Store it here and you can find the label later with `?external_id=`, without keeping a map of Plane ids.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.labels:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | `name` missing or over 255 characters, or a `parent_id` that isn't a label in this project. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't create labels. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `conflict` | A label with this name already exists in the project. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/labels/delete-label.md b/docs/api-reference/v2/labels/delete-label.md
new file mode 100644
index 00000000..be1504d1
--- /dev/null
+++ b/docs/api-reference/v2/labels/delete-label.md
@@ -0,0 +1,146 @@
+---
+title: Delete a label
+description: Delete a label from a Plane project with the v2 REST API. Path parameters, what happens to tagged work items, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete label, DELETE label, remove label, 204 no content
+---
+
+# Delete a label
+
+
+
+Remove a label from a project. The label disappears from every work item that carried it — the work items themselves are not touched, and nothing else about them changes.
+
+A successful delete returns `204` with an empty body. Don't parse the response.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the label belongs to.
+
+
+
+
+
+The label id to delete.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.labels:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A path parameter is malformed, for example a `pk` that isn't a UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't delete labels. |
+| `404` | `resource_not_found` | No such label in this project, or the workspace or project is outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+Labels have no delete protection. Unlike a project's default state, no label is pinned, and a label still applied to work items deletes normally.
+
+
+
+::: warning Deleting is not the same as unassigning
+If you only want the tag off a few work items, update those work items' `label_ids` instead. Deleting the label strips it from every work item in the project at once, and there is no undelete endpoint — you would have to recreate the label and reapply it everywhere.
+:::
+
+::: tip Deleting twice
+A repeated delete returns `404`, not `204`. Treat both as "the label is gone" when your job is idempotent.
+:::
+
+
diff --git a/docs/api-reference/v2/labels/get-label.md b/docs/api-reference/v2/labels/get-label.md
new file mode 100644
index 00000000..98496e69
--- /dev/null
+++ b/docs/api-reference/v2/labels/get-label.md
@@ -0,0 +1,152 @@
+---
+title: Get a label
+description: Retrieve a single label from a Plane project with the v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get label, retrieve label, GET label by id, label object
+---
+
+# Get a label
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/labels/{pk}/
+
+
+
+
+
+Retrieve one label by id. Reach for this when you already hold a label id — from a work item's `label_ids`, or from a webhook payload — and need its current name, color, or parent.
+
+If you only know the label by name or by your own identifier, use [List labels](/api-reference/v2/labels/list-labels) with `search` or `external_id` instead.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the label belongs to.
+
+
+
+
+
+The label id.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.labels:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read labels. |
+| `404` | `resource_not_found` | No such label in this project, or the workspace or project is outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info A real label under the wrong project is a 404
+Labels are project-scoped. Pairing a valid label id with a different project's `project_id` returns `404`, the same response you get for an id that doesn't exist anywhere — existence outside your reach is never leaked.
+:::
+
+
diff --git a/docs/api-reference/v2/labels/list-labels.md b/docs/api-reference/v2/labels/list-labels.md
new file mode 100644
index 00000000..32c27e32
--- /dev/null
+++ b/docs/api-reference/v2/labels/list-labels.md
@@ -0,0 +1,260 @@
+---
+title: List labels
+description: List the labels in a Plane project with the v2 REST API. Filters, search, ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list labels, label filters, parent_id, external_id, pagination, GET labels
+---
+
+# List labels
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/labels/
+
+
+
+
+
+Return the labels defined in a project. Use it to build a label picker, to resolve label names to ids before writing `label_ids` on a work item, or to look up a label you imported by its `external_id`.
+
+Results are paginated. Labels from other projects are never included — a project's labels are its own.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project whose labels you want.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+**Filters**
+
+
+
+
+
+Return only the labels nested under this label.
+
+Pair it with `parent_id__isnull=true` to get the opposite view — every top-level label in the project. Use `parent_id__isnull=false` for every label that has a parent.
+
+
+
+
+
+Return labels whose `external_id` matches. Combine with `external_source` to resolve a record from another system to its Plane label without keeping a local id map.
+
+
+
+
+
+Return labels that came from this system, for example `github` or `jira`.
+
+
+
+
+
+**Search**
+
+
+
+
+
+A search term matched against the label `name`. Use it to power type-ahead in a picker rather than downloading every page and filtering client-side.
+
+
+
+
+
+**Ordering**
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending. Defaults to `sort_order`.
+
+- `sort_order` , `-sort_order` — the project's own label ordering
+- `created_at` , `-created_at` — newest or oldest first
+- `id` , `-id`
+
+A value outside this list is not rejected — it silently falls back to the default `sort_order`. Check your spelling: a typo in `order_by` fails silently and shows up as a differently sorted page, not as an error.
+
+
+
+
+
+**Pagination**
+
+
+
+
+
+Page size. Defaults to 50, maximum 200.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response and send it back as `offset` to walk forward.
+
+
+
+
+
+Set to `cursor` to switch from the default offset envelope to the keyset cursor envelope, which skips the `COUNT(*)` and returns `next_cursor` and `has_more` instead of `next` and `total_count`. Send the returned `next_cursor` back as `cursor` to fetch the following page.
+
+Only `created_at` and `id` are cursor-eligible, because a keyset needs a strictly ordered column and `sort_order` is neither unique nor monotonic. Pair `paginate=cursor` with `order_by=created_at` or `order_by=id` — the default `sort_order` ordering is rejected with `400 ordering_not_cursor_eligible`.
+
+Most projects have few enough labels that the default offset envelope is all you need.
+
+
+
+
+
+Defaults to `true`. Set to `false` to omit `total_count` and skip the count query.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.labels:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read labels. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/labels/overview.md b/docs/api-reference/v2/labels/overview.md
new file mode 100644
index 00000000..8dad5fa1
--- /dev/null
+++ b/docs/api-reference/v2/labels/overview.md
@@ -0,0 +1,120 @@
+---
+title: Labels overview
+description: The Plane API v2 label object. Attributes, nested labels via parent_id, name uniqueness within a project, and the label endpoints.
+keywords: plane api v2, labels, label object, parent_id, nested labels, tags, work item labels
+---
+
+# Labels overview
+
+A label is a tag you attach to work items to slice a project by something the workflow doesn't already model — `bug`, `needs-design`, `customer-escalation`. A label belongs to exactly one project, so two projects can both have a `bug` label and they are separate records with separate ids.
+
+Work items reference labels by id through `label_ids`, and you filter work items with `label_id`. Nothing on the label object tells you which work items carry it — that relationship is read from the work item side.
+
+[Learn more about labels](https://docs.plane.so/core-concepts/work-items/labels)
+
+
+
+
+## The label object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the label. This is the value you send in a work item's `label_ids`.
+
+- `name` _string_
+
+ Display name, unique within the project. Maximum 255 characters.
+
+- `description` _string_
+
+ Free-form note about what the label is for. Useful when a team needs a convention written down next to the tag.
+
+- `color` _string_
+
+ Hex color used wherever the label is rendered, for example `#e5484d`.
+
+- `sort_order` _number_
+
+ Ordering weight within the project. Lower values sort first. Assigned automatically when you don't send one.
+
+- `parent_id` _string (uuid) or null_
+
+ The label this one nests under, letting you build groups such as an `Area` label with `Billing` and `Search` beneath it. `null` for a top-level label.
+
+- `external_id` , `external_source` _string or null_
+
+ Correlation fields for sync and import. Together they let you map a label to a record in another system and find it again later — list labels filtered by both to look one up without storing Plane ids yourself.
+
+- `created_at` _string (date-time)_
+
+ When the label was created.
+
+- `created_by_id` _string (uuid) or null_
+
+ The user who created the label. `null` for labels created by an integration or by the system.
+
+::: info Nesting is metadata, not inheritance
+`parent_id` groups labels for display and filtering. Applying a child label to a work item does not also apply its parent.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | -------------------------------------------------------------- | -------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/labels/` | List labels |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/labels/` | Create a label |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/labels/{pk}/` | Get a label |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/labels/{pk}/` | Update a label |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/labels/{pk}/` | Delete a label |
+
+## Names are unique per project
+
+A project cannot hold two labels with the same `name`. Creating a duplicate, or renaming a label onto a name already in use, returns `409 conflict`.
+
+If you are importing labels from another system, treat the `409` as "this already exists" rather than an error: list the project's labels filtered by `external_id` and `external_source` first, and update the match instead of creating it again.
+
+## Deleting labels
+
+`DELETE` returns `204` with an empty body. The label is removed from every work item that carried it — the work items themselves are untouched.
+
+::: warning Deletes are not reversible through the API
+There is no undelete endpoint. If you need the association back, recreate the label and reapply it with `label_ids` on each work item.
+:::
+
+## Changed from v1
+
+- `parent` is now **`parent_id`**, on both reads and writes.
+- `created_by` is now **`created_by_id`**.
+- Reads no longer return `updated_at`, `updated_by`, `project`, or `workspace`. The project is already in the request path.
+- Reads now return `external_id` and `external_source`, which v1 accepted on writes but did not surface on the label object.
+- Lists return the offset envelope — `data`, `next`, `previous`, `total_count` — instead of v1's `results` with cursor keys. See [List labels](/api-reference/v2/labels/list-labels).
+- The `expand` and `fields` query parameters are not available on labels.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
diff --git a/docs/api-reference/v2/labels/update-label.md b/docs/api-reference/v2/labels/update-label.md
new file mode 100644
index 00000000..5f84fd3e
--- /dev/null
+++ b/docs/api-reference/v2/labels/update-label.md
@@ -0,0 +1,219 @@
+---
+title: Update a label
+description: Update a label in a Plane project with the v2 REST API. Partial PATCH semantics, body parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update label, PATCH label, rename label, parent_id, label color
+---
+
+# Update a label
+
+
+
+Change a label's name, color, description, nesting, or ordering. Every work item already carrying the label picks up the change — the label id does not move, so no work item loses its tag when you rename it.
+
+The update is partial. Fields you omit are left untouched, and omitting a field is not the same as sending `null`: send `"parent_id": null` to lift a nested label to the top level.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the label belongs to.
+
+
+
+
+
+The label id.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+New display name, unique within the project. Maximum 255 characters. Renaming onto a name another label already holds returns `409 conflict`.
+
+
+
+
+
+Hex color used wherever the label is rendered, for example `#e5484d`. Maximum 255 characters.
+
+
+
+
+
+Free-form note about what the label is for.
+
+
+
+
+
+Move the label under a different parent. The new parent must be a label in the same project. Send `null` to make the label top-level again.
+
+
+
+
+
+Ordering weight within the project. Lower values sort first.
+
+
+
+
+
+Your system's identifier for this label, for sync and import correlation. Maximum 255 characters. Send `null` to clear it.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters. Send `null` to clear it.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.labels:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ----------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A field over its length limit, or a `parent_id` that isn't a label in this project. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't update labels. |
+| `404` | `resource_not_found` | No such label in this project, or the workspace or project is outside your tenant. |
+| `409` | `conflict` | Another label in the project already uses the name you sent. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning There is no PUT
+`PUT` on this path returns `405 method_not_allowed`. Send only the fields you want to change with `PATCH` — a full-object write is never required, and rebuilding the object client-side risks clobbering a field someone else just changed.
+:::
+
+
diff --git a/docs/api-reference/v2/members/list-project-members.md b/docs/api-reference/v2/members/list-project-members.md
new file mode 100644
index 00000000..c2c7d242
--- /dev/null
+++ b/docs/api-reference/v2/members/list-project-members.md
@@ -0,0 +1,276 @@
+---
+title: List project members
+description: List the members of a Plane project with the v2 REST API. Role and member filters, search, ordering, pagination, ?expand=member, OAuth scopes, and code examples.
+keywords: plane api v2, list project members, project roster, member_id filter, role filter, expand member, GET project members
+---
+
+# List project members
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/members/
+
+
+
+
+
+Return one project's active member roster as a paginated list. Reach for this when you need the people who can actually be assigned work in a project — the workspace roster is wider, and includes members with no access to this project.
+
+Roles here are project roles, so the same person can come back as `member` on the workspace roster and as `contributor` on this one.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project whose roster you want. A project id from another workspace returns `404`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+Filters combine with `AND`. Check your spelling on `order_by` and `paginate` — neither is validated. An unrecognized `order_by` value silently falls back to the default ordering, and anything other than `paginate=cursor` silently uses offset pagination. A typo shows up as an unexpected sort order or envelope, not as an error.
+
+
+
+
+
+Return the membership for one user. Use `member_id__in` to check several at once, comma-separated.
+
+Pairing `?member_id=` with `?per_page=1` is the cheapest membership check there is: an empty `data` array means that user is not on this project.
+
+
+
+
+
+Return only members holding this role slug, for example `?role=contributor`. Use `role__in` for several roles at once, comma-separated.
+
+The value is a plain string, not a fixed enum — a custom role is matched by its own slug.
+
+
+
+
+
+A search term matched against the member's user record, so you can find someone by name or email without expanding first.
+
+
+
+
+
+
+
+
+### Expansion
+
+
+
+
+
+Set to `member` to embed the user beside the id. `member_id` stays in place and a `member` object with `id`, `display_name`, `avatar_url`, and `email` is added next to it.
+
+`member` is the only accepted value; anything else returns `400`. See [Expanding relations](/api-reference/v2/expanding-relations).
+
+
+
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `created_at` , `-created_at` — when the person was added to the project
+- `id` , `-id`
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most project rosters fit in a single page.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read `next` from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` for the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.members:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's roster. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Validate assignees against this list, not the workspace one
+A `member_id` that is on the workspace roster but not on this project is not a valid assignee for the project's work items. Check membership here before writing `assignee_ids`.
+:::
+
+::: info A `409` is not a permission signal
+`403` means the caller's role or scope is too narrow. `409 work_item_types_managed_at_workspace` or `work_item_types_managed_at_project` means the write belongs on the other surface, whatever the caller's role is. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+::: tip Custom roles appear as their own slug
+`release-manager` above is a custom role defined by the workspace. `role` has no enum in the schema, so treat any slug you do not recognize as a role you have no rules for rather than as bad data.
+:::
diff --git a/docs/api-reference/v2/members/list-workspace-members.md b/docs/api-reference/v2/members/list-workspace-members.md
new file mode 100644
index 00000000..f869b118
--- /dev/null
+++ b/docs/api-reference/v2/members/list-workspace-members.md
@@ -0,0 +1,282 @@
+---
+title: List workspace members
+description: List the members of a Plane workspace with the v2 REST API. Role and member filters, search, ordering, pagination, ?expand=member, OAuth scopes, and code examples.
+keywords: plane api v2, list workspace members, workspace roster, member_id filter, role filter, expand member, GET members
+---
+
+# List workspace members
+
+
+ GET
+ /api/v2/workspaces/{slug}/members/
+
+
+
+
+
+Return the workspace's active member roster as a paginated list. This is the endpoint that resolves a person to the `member_id` you assign work to, and the one you poll to keep a user directory in sync with Plane.
+
+The workspace roster is the superset of every project roster — anyone on a project is on this list too.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+Filters combine with `AND`. Check your spelling on `order_by` and `paginate` — neither is validated. An unrecognized `order_by` value silently falls back to the default ordering, and anything other than `paginate=cursor` silently uses offset pagination. A typo shows up as an unexpected sort order or envelope, not as an error.
+
+
+
+
+
+Return the membership for one user. Use the `member_id__in` variant to look several users up at once, comma-separated — `?member_id__in=16c61a3a-512a-48ac-b0be-b6b46fe6f430,7f2b9e04-6c1d-4a58-9e3b-0d4c8a2f6b71`.
+
+This is the cheap way to answer "is this person still in the workspace, and what is their role now?" without paging the whole roster.
+
+
+
+
+
+Return only members holding this role slug, for example `?role=owner`. Use `role__in` for several roles at once, comma-separated.
+
+The value is a plain string, not a fixed enum — custom roles are matched by their own slug.
+
+
+
+
+
+A search term matched against the member's user record, so you can find someone by name or email without expanding first.
+
+
+
+
+
+
+
+
+### Expansion
+
+
+
+
+
+Set to `member` to embed the user beside the id. `member_id` stays in place and a `member` object with `id`, `display_name`, `avatar_url`, and `email` is added next to it.
+
+`member` is the only accepted value here; anything else returns `400`. See [Expanding relations](/api-reference/v2/expanding-relations).
+
+
+
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `created_at` , `-created_at` — when the person joined the workspace
+- `id` , `-id`
+
+There is no ordering by name or role; sort the page client-side, or expand and sort on `display_name`.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read `next` from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` for the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. Worth it when you are walking a large workspace end to end.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+
+
+
+### Scopes
+
+`workspaces.members:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this workspace roster. |
+| `404` | `resource_not_found` | No such workspace, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Membership id versus user id
+`id` identifies the membership row; `member_id` identifies the person. Key caches and joins on `member_id` — the same user has a different `id` on every roster they appear in. See [Members overview](/api-reference/v2/members/overview).
+:::
+
+::: info Roles gate writes, not this read
+A narrow role shows up as `403` on the write you attempted. A `409 work_item_types_managed_at_workspace` or `work_item_types_managed_at_project` is not a role problem at all — see [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+::: tip Expanding a page costs one query, not one per row
+`?expand=member` loads the users in bulk, so a 200-row page is a single extra query. Prefer one expanded page over 200 follow-up requests.
+:::
diff --git a/docs/api-reference/v2/members/overview.md b/docs/api-reference/v2/members/overview.md
new file mode 100644
index 00000000..621903ac
--- /dev/null
+++ b/docs/api-reference/v2/members/overview.md
@@ -0,0 +1,132 @@
+---
+title: Members overview
+description: The Plane API v2 member object. Workspace and project rosters, the role slug, membership ids versus user ids, and expanding the user with ?expand=member.
+keywords: plane api v2, members, workspace members, project members, member_id, role slug, expand member, roster
+---
+
+# Members overview
+
+A member record is the link between a user and a workspace or a project. Reading a roster answers two questions: who belongs here, and in what role.
+
+v2 returns the **membership**, not the user. Every row is three fields — the membership's own `id`, the `member_id` of the user it points at, and the `role` that user holds in this context. Names, emails, and avatars belong to the user, and you pull them into the same response with [`?expand=member`](#expanding-the-user).
+
+[Learn more about members](https://docs.plane.so/core-concepts/workspaces/members)
+
+
+
+
+## The member object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the membership row — not for the user. The same person has one membership in the workspace and another in each project they belong to, so they have several `id` values but one `member_id`.
+
+- `member_id` _string (uuid)_
+
+ The user this membership belongs to. This is the id that lines up with `created_by_id` on other objects, `assignee_ids` on work items, and `actor_id` in [audit logs](/api-reference/v2/audit-logs/overview). Join on `member_id`, never on `id`.
+
+- `role` _string_
+
+ The member's role in this workspace or project, expressed as a slug. It is what decides whether this person can write here at all.
+
+ The slug is context-correct: the same person can read as `member` on the workspace roster and as `contributor` on a project roster. Nullable — `null` appears only on legacy rows that predate role slugs.
+
+::: tip Roles are an open set, not an enum
+The schema types `role` as a plain string with no enum. Built-in slugs such as `owner`, `member`, and `contributor` are joined by whatever custom roles the workspace has defined, each under its own slug. Match the slugs you care about and fall through gracefully on anything else — do not write an exhaustive `switch`.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| ------ | ---------------------------------------------------------- | ---------------------- |
+| `GET` | `/api/v2/workspaces/{slug}/members/` | List workspace members |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/members/` | List project members |
+
+Both endpoints return the **active** roster for their scope. The workspace list is the superset: a project member is always a workspace member too, so a `member_id` on a project roster always appears on the workspace roster as well — usually with a different `role`.
+
+Members are read-only in v2. There are no v2 endpoints to invite, update, or remove a member; membership changes are made in Plane or through the [v1 members endpoints](/api-reference/v1/members/overview).
+
+## Membership ids are not user ids
+
+This is the single mistake worth designing against. Given the same person:
+
+| Roster | `id` | `member_id` |
+| ------------------------ | -------------------------------------- | -------------------------------------- |
+| Workspace `my-team` | `c1b7e4a9-2f66-4a1d-9c3b-7d5e2f8a1b40` | `16c61a3a-512a-48ac-b0be-b6b46fe6f430` |
+| Project `4af68566-…067b` | `9d2f5c81-4b73-4e60-8a1f-2c9b6d3e7f04` | `16c61a3a-512a-48ac-b0be-b6b46fe6f430` |
+
+Key your user cache on `member_id`. Use `id` only when you need to talk about the membership itself.
+
+## Expanding the user
+
+Members are one of only two v2 resources that accept `?expand=` — the other is work items. The single allowed value here is `member`.
+
+```bash
+GET /api/v2/workspaces/my-team/members/?expand=member
+```
+
+Expansion is **separate-key**: `member_id` stays exactly where it was and a `member` object is added beside it. The id is never swapped for an object, so code that reads `member_id` keeps working whether or not the caller expanded. The expanded object carries `id`, `display_name`, `avatar_url`, and `email`.
+
+Any other value — `user`, `project`, `role` — is rejected with `400 validation_error`. See [Expanding relations](/api-reference/v2/expanding-relations) for the full contract.
+
+## Roles and errors
+
+A role that is too narrow produces `403 forbidden` on the write you attempted. Roles never produce a `404`: a workspace or project outside your tenant returns `404 resource_not_found` whether or not you would have had the role for it.
+
+::: info A `409` is never a role problem
+If a write comes back `409 work_item_types_managed_at_workspace` or `work_item_types_managed_at_project`, the caller's role and scopes are fine. The workspace simply manages work item types on the other surface, and the same call succeeds there. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+## Changed from v1
+
+- A member row is now the **membership**, not a flattened user. v1 returned `first_name`, `last_name`, `email`, `avatar`, `avatar_url`, and `display_name` inline; v2 returns `id`, `member_id`, and `role`, and moves the user fields behind `?expand=member`.
+- `role` is a **slug string**, not the numeric level v1 returned. Compare `role == "member"`, not `role >= 15`.
+- Responses are paginated envelopes rather than bare arrays. See [Pagination](/api-reference/v2/pagination).
+- v2 exposes reads only. The v1 add, update, and remove member endpoints have no v2 equivalent yet.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
+
+## Related
+
+- [List workspace members](/api-reference/v2/members/list-workspace-members)
+- [List project members](/api-reference/v2/members/list-project-members)
+- [Expanding relations](/api-reference/v2/expanding-relations)
+- [Get current user](/api-reference/v2/users/get-current-user) — the `member_id` your own credentials map to
diff --git a/docs/api-reference/v2/migrating-from-v1.md b/docs/api-reference/v2/migrating-from-v1.md
new file mode 100644
index 00000000..ecbafe46
--- /dev/null
+++ b/docs/api-reference/v2/migrating-from-v1.md
@@ -0,0 +1,328 @@
+---
+title: Migrating from v1
+description: A practical guide to moving an integration from the Plane REST API v1 to v2. Path and header changes, PATCH-only writes, RFC 9457 errors, sparse reads, the new pagination envelope, renamed fields, and what v2 does not cover yet.
+keywords: plane api v2 migration, v1 to v2, plane rest api upgrade, is_default, description_html, custom_fields, RFC 9457, sparse reads, PATCH only
+---
+
+# Migrating from v1
+
+v2 is a separate, additive surface. v1 keeps working, nothing is being switched off underneath you, and you can move one call at a time — the two versions can run side by side in the same integration against the same workspace.
+
+This page is the checklist for moving a call across.
+
+## At a glance
+
+| | v1 | v2 |
+| ----------------- | -------------------------- | ----------------------------------------------------------- |
+| Path | `/api/v1/…` | `/api/v2/…` |
+| Auth header | `X-API-Key` | `X-Api-Key` |
+| Update verb | `PUT` or `PATCH` | `PATCH` only |
+| Errors | Ad-hoc JSON bodies | RFC 9457 `application/problem+json` with stable `code` |
+| Relations on read | Embedded objects | `*_id` / `*_ids`, plus `?expand=` on work items and members |
+| List envelope | `results` + cursor strings | `data` + `pagination.style` |
+| Sparse fieldsets | `?fields=` | Not needed — reads are already sparse |
+
+Unchanged: the base URL (`https://api.plane.so`), the trailing slash on every path, JSON in and out, and the fine-grained OAuth scope names (`projects.work_items:read`, `projects.states:write`, and so on).
+
+## 1. Change the path
+
+`/api/v1/` becomes `/api/v2/`. Everything else about the URL — workspace slug, project id, trailing slash — works the same way.
+
+```diff
+- https://api.plane.so/api/v1/workspaces/my-team/projects/{project_id}/work-items/
++ https://api.plane.so/api/v2/workspaces/my-team/projects/{project_id}/work-items/
+```
+
+## 2. Auth header casing
+
+v1 docs use `X-API-Key`; v2 standardizes on **`X-Api-Key`**.
+
+HTTP header names are case-insensitive, so this is cosmetic — an existing `X-API-Key` header keeps working against v2, and no client library will care. Adopt the new casing when you touch the code, not as a migration step of its own.
+
+OAuth is unchanged: `Authorization: Bearer `.
+
+## 3. `PUT` is gone
+
+v2 has no `PUT`. Every update is a `PATCH`, and a `PATCH` is partial — it changes only the fields you send.
+
+```diff
+- PUT /api/v1/workspaces/my-team/projects/{project_id}/states/{pk}/
++ PATCH /api/v2/workspaces/my-team/projects/{project_id}/states/{pk}/
+```
+
+Sending `PUT` returns `405` with the code `method_not_allowed`.
+
+Two things follow from partial updates:
+
+- **You no longer have to read-modify-write.** To change one field, send one field.
+- **Omitted is not the same as `null`.** Leaving a key out means "don't touch it"; sending `"target_date": null` means "clear it".
+
+## 4. Errors are RFC 9457
+
+Every v2 error is `application/problem+json` with the same five keys:
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/validation_error",
+ "title": "Validation Error",
+ "status": 400,
+ "code": "validation_error",
+ "detail": "One or more fields failed validation.",
+ "errors": [{ "field": "name", "message": "This field is required." }]
+}
+```
+
+
+
+**Branch on `code`.** It is the stable, machine-readable part. `status` is too coarse (several distinct conditions share `409`) and `detail` is prose that can be reworded at any time. `errors[]` appears only on `validation_error` and names the offending fields.
+
+Other behavior worth knowing while you port error handling:
+
+- A resource outside your tenant is `404 resource_not_found`, never `403`. v2 does not leak existence.
+- `DELETE` returns `204` with an empty body.
+- Uniqueness and protected-state conflicts are `409 conflict` — a duplicate state name, deleting a project's default state, deleting a state that still holds work items.
+- Throttling is `429 rate_limited`; honor `Retry-After`.
+
+See [Errors](/api-reference/v2/errors) for the full code list.
+
+## 5. Reads are sparse
+
+This is the largest behavioral change. v1 embedded related objects in the response; v2 returns identifiers.
+
+```diff
+- "state": { "id": "f960d3c2-…", "name": "In Progress", "group": "started" }
++ "state_id": "f960d3c2-…"
+
+- "assignees": [ { "id": "16c61a3a-…", "display_name": "Priya Raghavan" } ]
++ "assignee_ids": ["16c61a3a-…"]
+```
+
+To-one relations come back as `_id`; to-many relations as `_ids` arrays. That makes every response a predictable, flat shape and keeps list pages cheap.
+
+Where you genuinely need the object, add `?expand=`. It is **separate-key**: `?expand=state` keeps `state_id` _and_ adds a `state` object beside it, so nothing you already read stops working.
+
+`?expand=` is supported on **work items** (`state`, `type`, `parent`, `assignees`, `labels`) and on **workspace and project members** (`member`). No other v2 resource supports it, and an unknown value is a `400`. Full details in [Expanding relations](/api-reference/v2/expanding-relations).
+
+::: tip You usually need fewer objects than v1 gave you
+v1 embedded relations whether or not you used them. Before reaching for `?expand=`, check whether the id is all your code actually consumed — for cache keys, joins, and foreign keys it almost always is.
+:::
+
+There is no `?fields=` in v2. Reads are already sparse, so there is nothing to trim.
+
+## 6. Writes take ids
+
+v2 write payloads take identifiers, never nested objects: `state_id`, `parent_id`, `assignee_ids`, `label_ids`, modules' `lead_id`. An id belonging to another project or workspace is a clean `400` — it never silently links to nothing.
+
+Audit fields (`created_at`, `created_by_id`, and friends) are read-only. Sending them has no effect.
+
+## 7. The pagination envelope changed
+
+v1 returned `results` with cursor strings like `20:1:0`. v2 returns `data` inside an envelope that declares its own style.
+
+**v2 offset (the default):**
+
+```json
+{
+ "data": [],
+ "next": 50,
+ "previous": null,
+ "total_count": 327,
+ "pagination": { "style": "offset" }
+}
+```
+
+**v2 cursor (opt in with `?paginate=cursor`):**
+
+```json
+{
+ "data": [],
+ "next_cursor": "b3A9MTcx",
+ "has_more": true,
+ "pagination": { "style": "cursor" }
+}
+```
+
+Port your paginator to read `data`, then **branch on `pagination.style`** rather than sniffing which keys are present. On the offset path `next` and `previous` are integer offsets, not tokens and not URLs. `per_page` defaults to 50 and caps at 200; `offset` caps at 10000, which is the point at which you should switch to cursor. See [Pagination](/api-reference/v2/pagination).
+
+## 8. Renamed and removed fields
+
+These are the concrete differences to grep your codebase for.
+
+| Resource | v1 | v2 |
+| ---------- | -------------------------------------------------- | --------------------------------------------------------- |
+| States | `default` | **`is_default`** |
+| States | `updated_at`, `updated_by`, `project`, `workspace` | **Not returned** |
+| Work items | `updated_at`, `updated_by`, `project`, `workspace` | **Not returned** |
+| Work items | `description_html` on read | **Write-only** — accepted on write, not returned on read |
+| Work items | — | `custom_fields` — populated only on single-item responses |
+
+Details on the two that bite hardest:
+
+::: warning `description_html` is write-only on work items
+You can send `description_html` on a create or update and it is stored, but it does **not** come back on any read — not on the list, not on the detail route, not in the create/update response. Code that round-trips a description through the API needs to keep its own copy of what it wrote.
+:::
+
+::: warning `custom_fields` is `null` on list responses
+`custom_fields` carries a work item's custom property values, and it is populated **only on single-item responses** — retrieve, create, and update. On the **list** path it is always `null`, because resolving properties per row would mean a query per work item. If you need custom property values for many items, list first and then fetch the ones you care about individually.
+:::
+
+Also note: the state `group` enum gained `triage` alongside v1's five groups.
+
+## 9. Side by side: listing work items
+
+The same call in both versions.
+
+### v1
+
+```bash
+curl "https://api.plane.so/api/v1/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?per_page=20&expand=state,assignees" \
+ -H "X-API-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "total_count": 327,
+ "next_cursor": "20:1:0",
+ "prev_cursor": "20:0:0",
+ "next_page_results": true,
+ "prev_page_results": false,
+ "count": 20,
+ "total_pages": 17,
+ "results": [
+ {
+ "id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
+ "name": "Fix login redirect",
+ "description": "",
+ "priority": "high",
+ "sequence_id": 118,
+ "state": {
+ "id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "name": "In Progress",
+ "group": "started"
+ },
+ "assignees": [{ "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "display_name": "Priya Raghavan" }],
+ "labels": [],
+ "created_at": "2026-01-14T09:22:41.478363Z"
+ }
+ ]
+}
+```
+
+
+
+### v2
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?per_page=20&expand=state,assignees" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "data": [
+ {
+ "id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
+ "name": "Fix login redirect",
+ "identifier": "PROJ-118",
+ "sequence_id": 118,
+ "priority": "high",
+ "state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "type_id": null,
+ "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
+ "label_ids": [],
+ "parent_id": null,
+ "start_date": null,
+ "target_date": "2026-02-02",
+ "is_draft": false,
+ "archived_at": null,
+ "created_at": "2026-01-14T09:22:41.478363Z",
+ "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "custom_fields": null,
+ "state": {
+ "id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "name": "In Progress",
+ "color": "#3f76ff",
+ "group": "started"
+ },
+ "assignees": [
+ {
+ "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "display_name": "Priya Raghavan",
+ "avatar_url": "https://assets.plane.so/avatars/16c61a3a.png",
+ "email": "priya@example.com"
+ }
+ ]
+ }
+ ],
+ "next": 20,
+ "previous": null,
+ "total_count": 327,
+ "pagination": { "style": "offset" }
+}
+```
+
+
+
+Line up the differences:
+
+- `results` → `data`; the cursor strings are replaced by an integer `next` plus a `pagination.style` discriminator.
+- `state` → `state_id`, and `assignees` → `assignee_ids`. The expanded `state` and `assignees` objects are **added** by `?expand=`, not substituted for the ids.
+- `description` is gone from the read shape.
+- New on every work item: `identifier` (`PROJ-118`), `type_id`, `is_draft`, `archived_at`, `created_by_id`, and `custom_fields` (`null` here because this is a list).
+
+## 10. What is new in v2
+
+Capabilities that have no v1 equivalent and are worth designing around rather than porting:
+
+**Work item types and custom properties.** Define types (`Bug`, `Incident`) and typed custom properties (`TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `OPTION`, `RELATION`, `URL`, `EMAIL`, `FILE`, `FORMULA`), attach properties to types, and read a type's full writable schema in one call. A workspace runs these at either the project or the workspace level — start at [Work item type modes](/api-reference/v2/work-item-type-modes), because writing to the wrong surface is a `409`.
+
+**Property options and contexts.** `OPTION` properties carry their own managed choice lists. In workspace mode, a **property context** narrows where a workspace property applies and can override `is_required`, `is_multi`, or `default_value` for a specific slice of projects and types. See [Property options](/api-reference/v2/work-item-property-options/overview) and [Property contexts](/api-reference/v2/work-item-property-contexts/overview).
+
+**Workspace features.** Read and toggle workspace-level capabilities through the API instead of the UI: `GET` and `PATCH /api/v2/workspaces/{slug}/features/`. See [Workspace features](/api-reference/v2/workspace-features/overview).
+
+**Audit logs.** Query the workspace audit trail with filters for actor, category, event name, target, outcome, IP address, and time range — `GET /api/v2/workspaces/{slug}/audit-logs/`. See [Audit logs](/api-reference/v2/audit-logs/overview).
+
+**Fetch a work item by its `PROJ-123` identifier.** The human key that appears in the Plane UI is now directly addressable, without knowing the project id or resolving a UUID first:
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/work-items/PROJ-118/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+This is the fastest way to turn something a person typed — a ticket reference in a chat message, a commit trailer, a support email — into a work item. See [Get a work item by identifier](/api-reference/v2/work-items/get-work-item-by-identifier).
+
+## 11. Not yet in v2
+
+v2 does not cover the whole product surface. It currently covers work items and their comments, states, labels, cycles, modules, work item types and custom properties, members, workspace features, audit logs, and the current user.
+
+Everything else still lives in v1 — projects, pages and wiki pages, intake, teamspaces, initiatives, epics, estimates, customers, stickies, attachments and links, worklogs, work item relations, activity, milestones, and workspace invitations. So do the sub-resources and verbs of cycles and modules: adding or removing their work items, archiving, and unarchiving all remain v1-only.
+
+::: info Run both versions side by side
+There is no requirement to migrate wholesale. Point the calls that v2 covers at `/api/v2/` and leave the rest on `/api/v1/`. The same API key authenticates both. For the v1 surface, see [the v1 introduction](/api-reference/v1/introduction).
+:::
+
+## Migration checklist
+
+1. Swap `/api/v1/` for `/api/v2/` on the calls v2 covers.
+2. Replace every `PUT` with `PATCH`, and trim the payload to the fields that actually change.
+3. Rewrite error handling to read `code` from the problem+json body.
+4. Update your paginator: read `data`, branch on `pagination.style`, treat `next` as an integer offset.
+5. Replace embedded relation reads with `*_id` / `*_ids`, adding `?expand=` only where you need the object and only where it is supported.
+6. Rename state `default` to `is_default`, and stop reading `updated_at`, `updated_by`, `project`, and `workspace` from state and work item responses.
+7. Stop expecting `description_html` back from a work item read.
+8. Stop expecting `custom_fields` on list responses — fetch the item individually when you need its property values.
+9. Confirm nothing you depend on is in the "not yet in v2" list above.
+
+## Related
+
+- [Pagination](/api-reference/v2/pagination)
+- [Filtering and ordering](/api-reference/v2/filtering-and-ordering)
+- [Expanding relations](/api-reference/v2/expanding-relations)
+- [Work item type modes](/api-reference/v2/work-item-type-modes)
+- [Errors](/api-reference/v2/errors)
diff --git a/docs/api-reference/v2/modules/create-module.md b/docs/api-reference/v2/modules/create-module.md
new file mode 100644
index 00000000..91e0443d
--- /dev/null
+++ b/docs/api-reference/v2/modules/create-module.md
@@ -0,0 +1,262 @@
+---
+title: Create a module
+description: Create a module in a Plane project with the v2 REST API. Body parameters, the module status enum, lead validation, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create module, module status, lead_id, POST modules, project modules
+---
+
+# Create a module
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/modules/
+
+
+
+
+
+Create a module in a project. Only `name` is required — everything else can be filled in later with a `PATCH` as the
+work firms up.
+
+Module names must be unique within a project — reusing one returns `409 conflict`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to create the module in.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Display name for the module, unique within the project. Maximum 255 characters.
+
+
+
+
+
+Plain-text summary of what the module covers.
+
+
+
+
+
+Where the module sits in its lifecycle.
+
+- `backlog` — Captured, not yet committed to
+- `planned` — Committed to but not started
+- `in-progress` — Actively being worked on
+- `paused` — Started, then put on hold
+- `completed` — Delivered
+- `cancelled` — Dropped without delivering
+
+Defaults to `planned` when omitted. A value outside this list is a `400 validation_error`.
+
+
+
+
+
+Date the module is scheduled to begin, as `YYYY-MM-DD`. Nullable.
+
+
+
+
+
+Date the module is expected to land, as `YYYY-MM-DD`. Nullable, and must not be earlier than `start_date`.
+
+
+
+
+
+The user accountable for the module. Must be a member of this project — any other user id is rejected with a `400`
+naming `lead_id`, never linked silently. Nullable.
+
+
+
+
+
+Ordering weight used when modules are listed. Lower values sort first. In a project that already has modules, Plane
+positions the new module ahead of them on create, so send `sort_order` in a follow-up `PATCH` if you need a specific
+slot.
+
+
+
+
+
+Free-form JSON object holding the icon Plane renders for the module.
+
+
+
+
+
+Your system's identifier for this module, for sync and import correlation. Maximum 255 characters. Nullable.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters. Nullable.
+
+
+
+
+
+
+::: info Members aren't set here
+`member_ids` is read-only in v2, so a module is created with no members. Module membership is not yet writable through
+the v2 API.
+:::
+
+
+
+### Scopes
+
+`projects.modules:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | Missing `name`, a `status` outside the enum, a `lead_id` who isn't a project member, or a `target_date` before `start_date`. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't create modules in this project. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `conflict` | A module with this name already exists in the project. |
+| `429` | `rate_limited` | Throttled. Wait for the interval in `Retry-After` and retry. |
+
+
diff --git a/docs/api-reference/v2/modules/delete-module.md b/docs/api-reference/v2/modules/delete-module.md
new file mode 100644
index 00000000..09de18e3
--- /dev/null
+++ b/docs/api-reference/v2/modules/delete-module.md
@@ -0,0 +1,139 @@
+---
+title: Delete a module
+description: Delete a module from a Plane project with the v2 REST API. Path parameters, what happens to the module's work items, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete module, remove module, DELETE modules, soft delete
+---
+
+# Delete a module
+
+
+
+Delete a module. The work items that were grouped by it stay in the project — only the grouping goes away.
+
+This is a soft delete: the module stops appearing in reads and its name is freed up for reuse in the project.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the module belongs to.
+
+
+
+
+
+The module to delete.
+
+
+
+
+
+
+::: info 204, then 404
+A successful delete returns `204` with an empty body — there is nothing to parse. A second `DELETE` on the same id
+returns `404 resource_not_found`, so a retry after a dropped connection is safe to treat as success.
+:::
+
+
+
+### Scopes
+
+`projects.modules:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't delete this module. |
+| `404` | `resource_not_found` | No such module, workspace, or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Wait for the interval in `Retry-After` and retry. |
+
+
diff --git a/docs/api-reference/v2/modules/get-module.md b/docs/api-reference/v2/modules/get-module.md
new file mode 100644
index 00000000..00247ccd
--- /dev/null
+++ b/docs/api-reference/v2/modules/get-module.md
@@ -0,0 +1,155 @@
+---
+title: Get a module
+description: Retrieve a single module from a Plane project with the v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get module, retrieve module, module by id, GET modules
+---
+
+# Get a module
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/
+
+
+
+
+
+Retrieve one module by id. The response is the same shape a list row uses, so you can refresh a single module without
+re-reading the whole collection.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the module belongs to.
+
+
+
+
+
+The module to retrieve.
+
+
+
+
+
+
+::: info A module in another project is a 404
+`pk` is resolved inside `project_id`. A valid module id from a different project — or from another workspace — returns
+`404 resource_not_found`, not `403`. The API never reveals that a resource you can't see exists.
+:::
+
+
+
+### Scopes
+
+`projects.modules:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read modules in this project. |
+| `404` | `resource_not_found` | No such module, workspace, or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Wait for the interval in `Retry-After` and retry. |
+
+
diff --git a/docs/api-reference/v2/modules/list-modules.md b/docs/api-reference/v2/modules/list-modules.md
new file mode 100644
index 00000000..3bc2b64f
--- /dev/null
+++ b/docs/api-reference/v2/modules/list-modules.md
@@ -0,0 +1,290 @@
+---
+title: List modules
+description: List the modules in a Plane project with the v2 REST API. Filter by status, lead, and external id, search by name, order, paginate, and handle errors.
+keywords: plane api v2, list modules, module status filter, lead_id, GET modules, pagination
+---
+
+# List modules
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/modules/
+
+
+
+
+
+List the modules in a project. Use it to build a delivery board, to find the module a sync run created, or to pull
+everything a person leads.
+
+Results are paginated and, by default, ordered by `sort_order` — the order modules appear in the Plane UI.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project whose modules you want.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+
+
+**Filters**
+
+
+
+Return only modules in this lifecycle position. One of `backlog`, `planned`, `in-progress`, `paused`, `completed`, or
+`cancelled`.
+
+Use `status__in` with a comma-separated list to match several at once, for example
+`?status__in=planned,in-progress`.
+
+
+
+
+
+Return only modules led by this user. Match is exact on the module's `lead_id`.
+
+
+
+
+
+Return only modules carrying this external identifier. Pair it with `external_source` to resolve a record you imported
+from another system.
+
+
+
+
+
+Return only modules that came from this system, for example `github` or `jira`.
+
+
+
+**Search**
+
+
+
+Free-text match on the module name.
+
+
+
+**Ordering**
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` , `-sort_order`
+- `created_at` , `-created_at`
+- `id` , `-id`
+
+Defaults to `sort_order`.
+
+
+
+**Pagination**
+
+
+
+Page size. Defaults to 50, maximum 200.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000 — go deeper with cursor pagination.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, then follow `next_cursor` with `?cursor=`.
+
+
+
+
+
+Set to `false` to skip the count query and omit `total_count` from the offset envelope. Defaults to `true`.
+
+
+
+
+
+
+::: warning Cursor needs an explicit order_by
+The default `sort_order` is not unique, so it can't back a stable keyset. A bare `?paginate=cursor` returns
+`400 ordering_not_cursor_eligible`. Pair it with a cursor-eligible ordering — `?paginate=cursor&order_by=created_at`
+— or stay on offset.
+:::
+
+::: info Enum filters are validated
+`status` and `status__in` are checked against the allowed values. A typo like `?status=in_progress` is a clean
+`400 validation_error`, not an empty result set.
+:::
+
+Modules do not support `?expand=` — `lead_id` and `member_ids` are always returned as ids.
+
+
+
+### Scopes
+
+`projects.modules:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------ |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read modules in this project. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Wait for the interval in `Retry-After` and retry. |
+
+
diff --git a/docs/api-reference/v2/modules/overview.md b/docs/api-reference/v2/modules/overview.md
new file mode 100644
index 00000000..a5c0e407
--- /dev/null
+++ b/docs/api-reference/v2/modules/overview.md
@@ -0,0 +1,175 @@
+---
+title: Modules overview
+description: The Plane API v2 module object. Attributes, the module status enum, lead and member handling, name uniqueness, and how modules differ from v1.
+keywords: plane api v2, modules, module object, module status, module lead, project modules
+---
+
+# Modules overview
+
+A module groups work items into a single deliverable inside one project — a launch, a migration, a redesign. Modules
+belong to exactly one project and are never shared across projects.
+
+Where a cycle answers "what are we doing this sprint", a module answers "what does this piece of work consist of". A
+work item can sit in a module and a cycle at the same time.
+
+[Learn more about modules](https://docs.plane.so/core-concepts/modules)
+
+
+
+
+## The module object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the module.
+
+- `name` _string_
+
+ Display name, unique within the project. Maximum 255 characters.
+
+- `description` _string_
+
+ Plain-text summary of what the module covers.
+
+- `status` _string_
+
+ Where the module sits in its lifecycle. One of `backlog`, `planned`, `in-progress`, `paused`, `completed`, or
+ `cancelled`.
+
+- `start_date` _string (date)_
+
+ Date the module is scheduled to begin, as `YYYY-MM-DD`, or `null`.
+
+- `target_date` _string (date)_
+
+ Date the module is expected to land, as `YYYY-MM-DD`, or `null`.
+
+- `lead_id` _string (uuid)_
+
+ The project member accountable for the module, or `null`.
+
+- `member_ids` _array of string_
+
+ Project members assigned to the module. Read-only in v2 — module membership is not yet writable through the v2 API.
+
+- `sort_order` _number_
+
+ Ordering weight used when modules are listed. Lower values sort first.
+
+- `logo_props` _any_
+
+ Free-form JSON object holding the icon Plane renders for the module. `{}` when no icon is set.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map a module to a record in another system and find it
+ again later.
+
+- `archived_at` _string (date-time)_
+
+ When the module was archived, or `null` for an active module.
+
+- `created_at` _string (date-time)_
+
+ When the module was created.
+
+- `created_by_id` _string (uuid)_
+
+ The user who created the module.
+
+::: info Relations are always ids
+Modules do not support `?expand=`. `lead_id` and `member_ids` come back as ids — resolve them against
+[project members](/api-reference/v2/members/list-project-members).
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | --------------------------------------------------------------- | --------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/modules/` | List modules |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/modules/` | Create a module |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/` | Get a module |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/` | Update a module |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/` | Delete a module |
+
+## Status
+
+`status` is the module's lifecycle position and is validated on both writes and the `status` filter — a value outside
+the enum is a `400 validation_error`, never a silently ignored write or an empty list.
+
+| Value | Meaning |
+| ------------- | ------------------------------ |
+| `backlog` | Captured, not yet committed to |
+| `planned` | Committed to but not started |
+| `in-progress` | Actively being worked on |
+| `paused` | Started, then put on hold |
+| `completed` | Delivered |
+| `cancelled` | Dropped without delivering |
+
+A module created without a `status` starts as `planned`.
+
+## Lead and members
+
+`lead_id` must be a member of the module's project. Passing a user who isn't a project member is rejected with
+`400 validation_error` naming `lead_id` — the link is never made silently.
+
+`member_ids` is read-only. Reads return the module's current members, but there is no v2 write path for adding or
+removing them yet.
+
+## Dates
+
+`start_date` and `target_date` are plain dates (`YYYY-MM-DD`), and either can be `null`. A `target_date` earlier than
+`start_date` is rejected with `400 validation_error`.
+
+## Names are unique per project
+
+Two modules in the same project cannot share a name. A duplicate on create — or on a rename via `PATCH` — returns
+`409 conflict`.
+
+## Not yet in v2
+
+Module work-item management (listing the work items in a module, adding them, removing them) and archive/unarchive are
+still only available in v1. See the [v1 module reference](/api-reference/v1/module/overview) for those routes; the v2
+endpoints above cover module CRUD only.
+
+## Changed from v1
+
+- `lead` is now **`lead_id`**, `members` is now **`member_ids`**, and `created_by` is now **`created_by_id`**.
+- `member_ids` is read-only.
+- Reads no longer return `updated_at`, `updated_by`, `project`, `workspace`, `view_props`, `description_text`, or
+ `description_html`.
+- `PUT` is gone — updates are `PATCH` and partial.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
diff --git a/docs/api-reference/v2/modules/update-module.md b/docs/api-reference/v2/modules/update-module.md
new file mode 100644
index 00000000..83204162
--- /dev/null
+++ b/docs/api-reference/v2/modules/update-module.md
@@ -0,0 +1,259 @@
+---
+title: Update a module
+description: Partially update a module in a Plane project with the v2 REST API. PATCH body parameters, status transitions, lead validation, OAuth scopes, and error codes.
+keywords: plane api v2, update module, patch module, module status, module lead, PATCH modules
+---
+
+# Update a module
+
+
+
+Update a module in place — move it to `in-progress`, hand it to a new lead, push out its target date.
+
+The update is partial: fields you omit are untouched. Omitting a field is not the same as sending `null`, which clears
+a nullable field.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the module belongs to.
+
+
+
+
+
+The module to update.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+New display name, unique within the project. Maximum 255 characters. Renaming onto an existing module's name returns
+`409 conflict`.
+
+
+
+
+
+Plain-text summary of what the module covers.
+
+
+
+
+
+Move the module to a different lifecycle position.
+
+- `backlog` — Captured, not yet committed to
+- `planned` — Committed to but not started
+- `in-progress` — Actively being worked on
+- `paused` — Started, then put on hold
+- `completed` — Delivered
+- `cancelled` — Dropped without delivering
+
+Any value outside this list is a `400 validation_error`. Transitions are unrestricted — a `completed` module can be
+sent back to `in-progress`.
+
+
+
+
+
+Date the module is scheduled to begin, as `YYYY-MM-DD`. Send `null` to clear it.
+
+
+
+
+
+Date the module is expected to land, as `YYYY-MM-DD`. Send `null` to clear it. Must not be earlier than the module's
+`start_date` — including the `start_date` already stored when you only send `target_date`.
+
+
+
+
+
+Reassign the module. Must be a member of this project — any other user id is rejected with a `400` naming `lead_id`.
+Send `null` to leave the module without a lead.
+
+
+
+
+
+Ordering weight used when modules are listed. Lower values sort first.
+
+
+
+
+
+Free-form JSON object holding the icon Plane renders for the module. The value you send replaces the stored object.
+
+
+
+
+
+Your system's identifier for this module, for sync and import correlation. Maximum 255 characters. Nullable.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters. Nullable.
+
+
+
+
+
+
+::: warning No PUT
+v2 has no `PUT`. Sending one returns `405 method_not_allowed` — use `PATCH` with only the fields you want to change.
+:::
+
+`member_ids`, `archived_at`, and the audit fields are read-only. Sending them has no effect.
+
+
+
+### Scopes
+
+`projects.modules:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------------------------------------------------ |
+| `400` | `validation_error` | A `status` outside the enum, a `lead_id` who isn't a project member, or a `target_date` before `start_date`. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't edit this module. |
+| `404` | `resource_not_found` | No such module, workspace, or project, or it's outside your tenant. |
+| `409` | `conflict` | Another module in the project already uses this name. |
+| `429` | `rate_limited` | Throttled. Wait for the interval in `Retry-After` and retry. |
+
+
diff --git a/docs/api-reference/v2/pagination.md b/docs/api-reference/v2/pagination.md
new file mode 100644
index 00000000..80c028fe
--- /dev/null
+++ b/docs/api-reference/v2/pagination.md
@@ -0,0 +1,310 @@
+---
+title: Pagination
+description: How Plane API v2 paginates list endpoints. Offset pagination by default, opt-in cursor pagination, the response envelopes for each, and how to page through a full result set.
+keywords: plane api v2, pagination, offset pagination, cursor pagination, per_page, next_cursor, total_count, paginate cursor
+---
+
+# Pagination
+
+Every v2 list endpoint is paginated. A list response is always an **envelope object** — never a bare array — and the envelope tells you both what you got and how to get the rest.
+
+v2 has two pagination styles:
+
+| Style | How you get it | Envelope keys | Cost | Best for |
+| ---------- | -------------------- | ------------------------------------------------------- | -------------------------- | ------------------------------------------------ |
+| **Offset** | Default — do nothing | `data`, `next`, `previous`, `total_count`, `pagination` | Runs a `COUNT` per request | UI paging, jumping to a page, showing a total |
+| **Cursor** | `?paginate=cursor` | `data`, `next_cursor`, `has_more`, `pagination` | No `COUNT` | Full exports, deep traversal, changing data sets |
+
+Both envelopes carry a `pagination.style` discriminator. Branch your client on that key rather than on which fields happen to be present — see [Write one loop for both styles](#write-one-loop-for-both-styles).
+
+## Offset pagination (default)
+
+Offset is what you get when you send nothing special. You control the window with `per_page` and `offset`.
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?per_page=50&offset=0" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "data": [
+ {
+ "id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
+ "name": "Fix login redirect",
+ "identifier": "PROJ-118",
+ "sequence_id": 118,
+ "priority": "high",
+ "state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "type_id": null,
+ "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
+ "label_ids": [],
+ "parent_id": null,
+ "start_date": null,
+ "target_date": "2026-02-02",
+ "is_draft": false,
+ "archived_at": null,
+ "created_at": "2026-01-14T09:22:41.478363Z",
+ "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "custom_fields": null
+ }
+ ],
+ "next": 50,
+ "previous": null,
+ "total_count": 327,
+ "pagination": { "style": "offset" }
+}
+```
+
+
+
+### Reading the envelope
+
+- `data` — the page of results.
+- `next` — an **integer offset**, not a URL and not a token. Send it back as `?offset=` to get the following page. It is `null` on the last page.
+- `previous` — the integer offset of the preceding page, or `null` when you are already at the start.
+- `total_count` — how many rows match your filters across all pages. Omitted entirely when you pass `?count=false`.
+- `pagination.style` — always `"offset"` here.
+
+Because `next` and `previous` are plain integers, you can also compute them yourself and jump straight to an arbitrary page — `?offset=200&per_page=50` is the fifth page, no traversal required.
+
+### Query parameters
+
+- `per_page` — page size. Defaults to `50`, capped at `200`. Values above the cap are clamped down rather than rejected; a non-integer value is a `400 validation_error`.
+- `offset` — rows to skip from the start of the result set. Defaults to `0`, maximum `10000`. A negative, non-integer, or over-cap `offset` is a `400 validation_error`, and the over-cap message points you at cursor pagination.
+- `count` — set `?count=false` to skip the `COUNT` query. `total_count` is then omitted from the envelope; `next` and `previous` still work.
+
+::: tip Turn off the count on hot paths
+`total_count` costs a second database query on every request. If you are streaming a list into a job rather than rendering "327 results" in a UI, `?count=false` makes each page cheaper without changing how you traverse.
+:::
+
+::: warning Offset stops at 10000
+`offset` is hard-capped at 10000 rows. If your result set is larger than that — a full export, a backfill, a nightly sync — switch to cursor pagination instead of trying to walk past the cap.
+:::
+
+## Cursor pagination (opt-in)
+
+Cursor pagination is a keyset walk. Instead of counting rows to skip, each page hands you an opaque token that encodes where the previous page stopped.
+
+Opt in with `?paginate=cursor`, then follow `next_cursor` until `has_more` is `false`.
+
+```bash
+# first page
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?paginate=cursor&per_page=100" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+
+# every page after that — same parameters, plus the token
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?per_page=100&cursor=b3A9MTcx" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "data": [
+ {
+ "id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
+ "name": "Fix login redirect",
+ "identifier": "PROJ-118",
+ "sequence_id": 118,
+ "priority": "high",
+ "state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
+ "type_id": null,
+ "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
+ "label_ids": [],
+ "parent_id": null,
+ "start_date": null,
+ "target_date": "2026-02-02",
+ "is_draft": false,
+ "archived_at": null,
+ "created_at": "2026-01-14T09:22:41.478363Z",
+ "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "custom_fields": null
+ }
+ ],
+ "next_cursor": "b3A9MTcx",
+ "has_more": true,
+ "pagination": { "style": "cursor" }
+}
+```
+
+
+
+### What is different
+
+- There is **no `total_count`** and no `count` parameter — cursor pagination never runs a `COUNT`. That is the point of it.
+- There is **no `previous`**. Cursor traversal is forward-only.
+- `next_cursor` is `null` and `has_more` is `false` on the last page.
+- `per_page` behaves exactly as it does on the offset path: default `50`, max `200`.
+
+Passing `?cursor=` is enough to stay on the cursor path — you do not need to repeat `paginate=cursor` on follow-up requests. You **do** need to repeat everything else. The token encodes only the position in the walk; filters, `order_by`, and `per_page` are re-read from each request, so dropping them mid-traversal silently changes the result set you are walking.
+
+The token is opaque. Treat it as a string to hand straight back to the API: do not parse it, do not build one yourself, and do not persist it as a long-lived bookmark. A malformed or truncated token is a `400 validation_error`.
+
+::: info Why cursor is stable
+Offset pages are computed by skipping rows. If someone creates a work item while you are on page 3, everything shifts by one and you can see a row twice or miss it entirely. A cursor encodes the last row's sort key, so inserts and deletes elsewhere in the list cannot shift your window.
+:::
+
+### Not every ordering can use a cursor
+
+A keyset walk needs an ordering whose key is unique and monotonic, so each resource declares an explicit allowlist of cursor-eligible `order_by` values rather than accepting any stored column. On work items the eligible values are `created_at`, `updated_at`, `sequence_id`, and `id`.
+
+Everything else is rejected on the cursor path. `sort_order` is a plain stored column but is not unique, so it cannot anchor a keyset. The **semantic orderings** cannot either — `order_by=priority` sorts `urgent` before `none` and `order_by=state_group` sorts by workflow meaning, and neither produces a key a cursor can resume from.
+
+Pairing one of those with cursor pagination returns a `400` with the stable code `ordering_not_cursor_eligible`:
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/ordering_not_cursor_eligible",
+ "title": "Ordering Not Cursor Eligible",
+ "status": 400,
+ "code": "ordering_not_cursor_eligible",
+ "detail": "This ordering can't be used with cursor pagination. Pass a cursor-safe ordering (e.g. order_by=created_at), or use the default offset pagination."
+}
+```
+
+
+
+Two ways out, depending on what you actually need:
+
+- You need the semantic sort — drop `paginate=cursor` and use offset.
+- You need the deep traversal — sort by a cursor-eligible column such as `created_at`, and sort the results yourself once you have them all.
+
+::: warning The check applies to the default ordering too
+Eligibility is evaluated against the ordering that will actually run. If you omit `order_by`, that is the resource's own default — which is not always a cursor-eligible column. When a cursor request 400s even though you passed no `order_by`, pass an explicit cursor-eligible one such as `?order_by=created_at`.
+:::
+
+See [Filtering and ordering](/api-reference/v2/filtering-and-ordering) for the full ordering contract and each resource's allowed `order_by` values.
+
+## Write one loop for both styles
+
+The two envelopes share the `data` key and the `pagination.style` discriminator, so a single auto-paginator can serve every list endpoint. Branch on `pagination.style` — not on `"next" in body`, which breaks the moment someone flips a call to cursor.
+
+
+
+
+```bash
+# Cursor traversal in a shell loop: follow next_cursor until has_more is false.
+URL="https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/"
+CURSOR=""
+
+while :; do
+ BODY=$(curl -s -G "$URL" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ --data-urlencode "paginate=cursor" \
+ --data-urlencode "per_page=100" \
+ ${CURSOR:+--data-urlencode "cursor=$CURSOR"})
+
+ echo "$BODY" | jq -r '.data[] | "\(.identifier) \(.name)"'
+
+ [ "$(echo "$BODY" | jq -r '.has_more')" = "true" ] || break
+ CURSOR=$(echo "$BODY" | jq -r '.next_cursor')
+done
+```
+
+
+
+
+```python
+import requests
+
+BASE = "https://api.plane.so/api/v2"
+HEADERS = {"X-Api-Key": "your-api-key"}
+
+
+def paginate(url, params=None):
+ """Yield every item from a v2 list endpoint, whichever style it returns."""
+ params = dict(params or {})
+
+ while True:
+ response = requests.get(url, headers=HEADERS, params=params)
+ response.raise_for_status()
+ body = response.json()
+
+ yield from body["data"]
+
+ if body["pagination"]["style"] == "cursor":
+ if not body["has_more"]:
+ return
+ # Keep every other parameter. The cursor encodes only the position —
+ # filters, order_by, and per_page are re-read from each request.
+ params["cursor"] = body["next_cursor"]
+ else:
+ if body["next"] is None:
+ return
+ params["offset"] = body["next"]
+
+
+work_items = paginate(
+ f"{BASE}/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/",
+ # count=false skips the COUNT on every page; swap it for paginate=cursor to
+ # skip the 10000-row offset cap entirely.
+ {"per_page": 100, "count": "false"},
+)
+
+for item in work_items:
+ print(item["identifier"], item["name"])
+```
+
+
+
+
+```javascript
+const BASE = "https://api.plane.so/api/v2";
+const HEADERS = { "X-Api-Key": "your-api-key" };
+
+async function* paginate(url, params = {}) {
+ let query = { ...params };
+
+ while (true) {
+ const response = await fetch(`${url}?${new URLSearchParams(query)}`, {
+ headers: HEADERS,
+ });
+ if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
+ const body = await response.json();
+
+ yield* body.data;
+
+ if (body.pagination.style === "cursor") {
+ if (!body.has_more) return;
+ // Keep every other parameter. The cursor encodes only the position —
+ // filters, order_by, and per_page are re-read from each request.
+ query.cursor = body.next_cursor;
+ } else {
+ if (body.next === null) return;
+ query.offset = body.next;
+ }
+ }
+}
+
+const workItems = paginate(
+ `${BASE}/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/`,
+ // count=false skips the COUNT on every page; swap it for paginate=cursor to
+ // skip the 10000-row offset cap entirely.
+ { per_page: "100", count: "false" }
+);
+
+for await (const item of workItems) {
+ console.log(item.identifier, item.name);
+}
+```
+
+
+
+
+## Choosing a style
+
+Reach for **offset** when a human is looking at the result: you want a total to display, you want to jump to page 7, and the list is small enough that the 10000-row cap is irrelevant.
+
+Reach for **cursor** when a machine is looking at the result: exports, backfills, incremental syncs, anything that walks a whole project's work items. It skips the `COUNT`, has no depth cap, and will not double-count rows if the data changes while you are traversing.
+
+## Related
+
+- [Filtering and ordering](/api-reference/v2/filtering-and-ordering) — narrowing a list before you page through it
+- [Errors](/api-reference/v2/errors) — the full RFC 9457 error contract and code list
+- [Expanding relations](/api-reference/v2/expanding-relations) — `?expand=` on paginated work item and member lists
diff --git a/docs/api-reference/v2/states/create-state.md b/docs/api-reference/v2/states/create-state.md
new file mode 100644
index 00000000..452c2eac
--- /dev/null
+++ b/docs/api-reference/v2/states/create-state.md
@@ -0,0 +1,230 @@
+---
+title: Create a state
+description: Create a workflow state in a Plane project with the v2 REST API. Body parameters, workflow groups, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create state, workflow state, state group, POST states
+---
+
+# Create a state
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/states/
+
+
+
+
+
+Add a workflow state to a project. The new state is appended to the project's workflow and becomes immediately selectable on work items.
+
+State names must be unique within a project — reusing one returns `409 conflict`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to add the state to.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Display name for the state, unique within the project. Maximum 255 characters.
+
+
+
+
+
+Hex color used wherever the state is rendered, for example `#3f76ff`. Maximum 255 characters.
+
+
+
+
+
+The workflow group the state belongs to. Determines how the state is treated by boards, charts, and cycle progress.
+
+- `backlog` — Not yet scheduled
+- `unstarted` — Scheduled but not begun
+- `started` — Actively in progress
+- `completed` — Finished successfully
+- `cancelled` — Closed without completion
+- `triage` — Awaiting intake review
+
+Defaults to `backlog` when omitted.
+
+
+
+
+
+Free-form description of what the state means in this workflow.
+
+
+
+
+
+Ordering weight within the project. Lower values sort first. Assigned automatically when omitted.
+
+
+
+
+
+Make this the project's default state — where work items land when no `state_id` is supplied. Setting it clears the flag on the previous default.
+
+
+
+
+
+Your system's identifier for this state, for sync and import correlation. Maximum 255 characters.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.states:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------- |
+| `400` | `validation_error` | Missing `name`/`color`, or a `group` outside the enum. |
+| `403` | `forbidden` | Your role or token scope can't create states. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `conflict` | A state with this name already exists in the project. |
+
+
diff --git a/docs/api-reference/v2/states/delete-state.md b/docs/api-reference/v2/states/delete-state.md
new file mode 100644
index 00000000..f0908487
--- /dev/null
+++ b/docs/api-reference/v2/states/delete-state.md
@@ -0,0 +1,164 @@
+---
+title: Delete a state
+description: Delete a workflow state from a Plane project with the v2 REST API. Protected default states, states holding work items, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete state, remove workflow state, default state conflict, 409 conflict, DELETE states
+---
+
+# Delete a state
+
+
+
+Remove a state from a project's workflow. A successful delete returns `204` with an empty body.
+
+Two conditions block a delete, and you have to clear the condition before the state will go — see [Before you delete](#before-you-delete).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the state belongs to.
+
+
+
+
+
+The id of the state to delete.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.states:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't delete states. |
+| `404` | `resource_not_found` | No such state, project, or workspace — or it's outside your tenant. |
+| `409` | `conflict` | This is the project's default state. |
+| `409` | `conflict` | The state still holds work items. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## Before you delete
+
+Both protected cases return `409 conflict`, so branch on the `detail` only for messaging — the fix differs:
+
+- **The project's default state.** Every project needs somewhere for work items to land when no `state_id` is supplied. Promote another state with [Update a state](/api-reference/v2/states/update-state) and `"is_default": true`, which demotes the current default, then delete it.
+- **A state that still holds work items.** Deleting it would leave those work items without a status. Move them to another state first — filter the project's work items by this state, `PATCH` each one to the replacement state, then retry the delete.
+
+A safe teardown is therefore: reassign work items, hand off the default flag if this state has it, delete.
+
+::: tip Deletes are soft
+The state stops appearing in the API and in Plane, but the row is retained. Treat the `204` as final for integration purposes — the states API has no restore operation.
+:::
diff --git a/docs/api-reference/v2/states/get-state.md b/docs/api-reference/v2/states/get-state.md
new file mode 100644
index 00000000..c1ed4ffd
--- /dev/null
+++ b/docs/api-reference/v2/states/get-state.md
@@ -0,0 +1,157 @@
+---
+title: Get a state
+description: Retrieve a single workflow state from a Plane project with the v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get state, retrieve state, workflow state, state group, GET state by id
+---
+
+# Get a state
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/states/{pk}/
+
+
+
+
+
+Retrieve one state by id. Reach for this when you already hold a `state_id` — from a work item, a webhook payload, or a stored mapping — and need its current name, color, or group.
+
+Checking `group` before you act is the reliable way to tell whether a work item is finished: a state named `Done` in one project and `Shipped` in another both report `group: "completed"`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the state belongs to. A state id from a different project returns `404`, even inside the same workspace.
+
+
+
+
+
+The id of the state to retrieve.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.states:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's states. |
+| `404` | `resource_not_found` | No such state, project, or workspace — or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info The response is flat
+The state object has no nested records. `created_by_id` is returned as an id, so fetch the user separately if you need their name.
+:::
+
+::: info Existence is never leaked
+A state outside your tenant returns `404`, not `403`. If you are resolving a cached id and get a `404`, re-read the project's workflow with [List states](/api-reference/v2/states/list-states) instead of retrying.
+:::
+
+
diff --git a/docs/api-reference/v2/states/list-states.md b/docs/api-reference/v2/states/list-states.md
new file mode 100644
index 00000000..2e4a1ab7
--- /dev/null
+++ b/docs/api-reference/v2/states/list-states.md
@@ -0,0 +1,300 @@
+---
+title: List states
+description: List the workflow states in a Plane project with the v2 REST API. Filters, search, ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list states, workflow states, state group filter, pagination, GET states
+---
+
+# List states
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/states/
+
+
+
+
+
+Return the states in a project as a paginated list. This is how you resolve a state name to the `state_id` you need when creating or moving a work item, and how you build a state picker that stays in sync with the project's workflow.
+
+Results are scoped to a single project — states are never shared across projects, so there is no workspace-wide state list.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project whose states you want to list.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+Filters combine with `AND`. The enum-backed _filters_ `group` and `group__in` are validated: an unrecognized value is rejected with a `400 validation_error` instead of quietly returning an empty list, so a typo surfaces immediately rather than looking like "no results". `order_by` and `paginate` are not validated — an unrecognized `order_by` silently falls back to the default ordering and anything other than `paginate=cursor` silently uses offset pagination, so check your spelling there.
+
+
+
+
+
+Return only states in this workflow group. One of `backlog`, `unstarted`, `started`, `completed`, `cancelled`, or `triage`.
+
+Use the `group__in` variant to match several groups at once, passing them comma-separated — `?group__in=started,completed`. Filtering by group is the portable way to ask "what counts as in progress here", because every project names its states differently but the groups are fixed.
+
+
+
+
+
+Return only the project's default state (`true`) or only the non-default states (`false`). Pairing `?is_default=true` with `?per_page=1` is the cheapest way to find where new work items will land.
+
+
+
+
+
+Return states whose `external_id` matches exactly. Use it to find the state you previously created for a record in another system.
+
+
+
+
+
+Return states that came from a particular system, for example `github` or `jira`. Combine it with `external_id` — an `external_id` is only unique within its source.
+
+
+
+
+
+A search term matched against the state name.
+
+
+
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sequence` , `-sequence` — the project's own workflow order
+- `created_at` , `-created_at` — when each state was added
+- `id` , `-id`
+
+Order by `sequence` when you are rendering the workflow to a user; it is the order the project itself uses.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most projects have fewer than 20 states, so one page is usually the whole workflow.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. Omit it for the default offset envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.states:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's states. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Read the group, not the name
+A project can rename `In Progress` to `Building` at any time, and reporting keeps working because boards and burndowns read `group`. Write integrations the same way: match on `group`, and treat `name` as a label for humans.
+:::
diff --git a/docs/api-reference/v2/states/overview.md b/docs/api-reference/v2/states/overview.md
new file mode 100644
index 00000000..46444f01
--- /dev/null
+++ b/docs/api-reference/v2/states/overview.md
@@ -0,0 +1,120 @@
+---
+title: States overview
+description: The Plane API v2 state object. Attributes, workflow groups, the default and triage states, and how states gate work item transitions.
+keywords: plane api v2, states, workflow states, state groups, backlog, triage, work item status
+---
+
+# States overview
+
+A state is a step in a project's workflow. Every work item sits in exactly one state, and a state belongs to exactly one project — states are never shared across projects.
+
+Each state is assigned to a **group**, which is what Plane uses to reason about progress. Boards, charts, and cycle burndowns read the group, not the state name, so a project can rename `In Progress` to `Building` without breaking any reporting.
+
+[Learn more about states](https://docs.plane.so/core-concepts/work-items/states)
+
+
+
+
+## The state object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the state.
+
+- `name` _string_
+
+ Display name, unique within the project. Maximum 255 characters.
+
+- `description` _string_
+
+ Free-form description of what the state means in this workflow.
+
+- `color` _string_
+
+ Hex color used wherever the state is rendered, for example `#3f76ff`.
+
+- `group` _string_
+
+ The workflow group this state belongs to. One of `backlog`, `unstarted`, `started`, `completed`, `cancelled`, or `triage`.
+
+- `sequence` _number_
+
+ Ordering weight within the project. Lower values sort first.
+
+- `is_default` _boolean_
+
+ Whether new work items land in this state when no `state_id` is supplied. Exactly one state per project is the default.
+
+- `is_triage` _boolean_
+
+ Whether this is the project's triage state, used by intake. Read-only — a triage state is created and managed by Plane.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map a state to a record in another system and find it again later.
+
+- `created_at` _string (date-time)_
+
+ When the state was created.
+
+- `created_by_id` _string (uuid)_
+
+ The user who created the state.
+
+::: tip Group drives behavior, name does not
+Filtering work items by `state_group=started` is stable across projects even when each project names its in-progress state differently. Filtering by `state_id` is exact but project-specific.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | -------------------------------------------------------------- | -------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/states/` | List states |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/states/` | Create a state |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/states/{pk}/` | Get a state |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/states/{pk}/` | Update a state |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/states/{pk}/` | Delete a state |
+
+## Deleting states
+
+Two deletes are rejected with `409 conflict`:
+
+- The project's **default state** cannot be deleted. Mark another state as default first.
+- A state that **still holds work items** cannot be deleted. Move those work items to another state first.
+
+## Changed from v1
+
+- `default` is now **`is_default`**.
+- Reads no longer return `updated_at`, `updated_by`, `project`, or `workspace`.
+- `group` accepts `triage` in addition to the five v1 groups.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
diff --git a/docs/api-reference/v2/states/update-state.md b/docs/api-reference/v2/states/update-state.md
new file mode 100644
index 00000000..295401b1
--- /dev/null
+++ b/docs/api-reference/v2/states/update-state.md
@@ -0,0 +1,246 @@
+---
+title: Update a state
+description: Update a workflow state in a Plane project with the v2 REST API. Partial PATCH semantics, body parameters, the default state flag, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update state, patch state, workflow state, is_default, state group, PATCH states
+---
+
+# Update a state
+
+
+
+Change a state's name, color, description, group, position, or default flag. Work items already sitting in the state stay where they are — you are editing the state itself, not moving anything.
+
+`PATCH` is partial. Send only the fields you want to change; anything you omit keeps its current value. Omitting a field is not the same as sending `null`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the state belongs to.
+
+
+
+
+
+The id of the state to update.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional — send the subset you are changing.
+
+
+
+
+
+New display name, unique within the project. Maximum 255 characters. Renaming is safe for reporting: boards and charts key off `group`, not `name`.
+
+
+
+
+
+Hex color used wherever the state is rendered, for example `#3f76ff`. Maximum 255 characters.
+
+
+
+
+
+Free-form description of what the state means in this workflow.
+
+
+
+
+
+Move the state to a different workflow group. This changes how every work item in the state is counted by boards, charts, and cycle progress, so switching a state from `started` to `completed` retroactively changes what those work items report as.
+
+- `backlog` — Not yet scheduled
+- `unstarted` — Scheduled but not begun
+- `started` — Actively in progress
+- `completed` — Finished successfully
+- `cancelled` — Closed without completion
+- `triage` — Awaiting intake review
+
+
+
+
+
+Ordering weight within the project. Lower values sort first. Set it to reposition the state in the workflow.
+
+
+
+
+
+Make this the project's default state — where work items land when no `state_id` is supplied. A project has exactly one default, so setting this to `true` clears the flag on the state that held it.
+
+
+
+
+
+Your system's identifier for this state, for sync and import correlation. Maximum 255 characters.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters.
+
+
+
+
+
+
+
+
+### Scopes
+
+`projects.states:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------- |
+| `400` | `validation_error` | A `group` outside the enum, or a field over its length limit. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't update states. |
+| `404` | `resource_not_found` | No such state, project, or workspace — or it's outside your tenant. |
+| `409` | `conflict` | Another state in the project already uses this name. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning Setting is_default moves the flag
+A project has exactly one default state, so promoting a state with `"is_default": true` demotes whichever state held the flag before. To change which state is the default, promote the new one — that single request is the whole operation.
+:::
+
+::: info is_triage is not writable
+`is_triage` is read-only. Plane creates and manages the triage state used by intake, so it isn't accepted in the request body. You can still assign the `triage` group to a state you own.
+:::
+
+
diff --git a/docs/api-reference/v2/users/get-current-user.md b/docs/api-reference/v2/users/get-current-user.md
new file mode 100644
index 00000000..664e7a56
--- /dev/null
+++ b/docs/api-reference/v2/users/get-current-user.md
@@ -0,0 +1,176 @@
+---
+title: Get current user
+description: Verify Plane API v2 credentials with GET /api/v2/users/me/. Returns the calling principal — id, display name, email, principal_kind, and granted OAuth scopes. Errors and code examples.
+keywords: plane api v2, get current user, users me, whoami, verify api key, principal_kind, oauth scopes, debug 401, debug 403
+---
+
+# Get current user
+
+
+ GET
+ /api/v2/users/me/
+
+
+
+
+
+Return the principal behind the credentials you just sent: which user it acts as, whether it is an OAuth token or an API key, and which scopes it carries.
+
+Make this your first call against a new key or a freshly authorized OAuth app. A `200` proves the credential is accepted and shows you exactly what it can do, before you spend a debugging session on an endpoint that was never in scope.
+
+
+
+### Path Parameters
+
+None. The path is fixed — there is no `{slug}` and no user id, because the endpoint describes the credential rather than a workspace or a person you choose.
+
+
+
+
+
+### Query Parameters
+
+None. No filters, no pagination, and no `?expand=`.
+
+
+
+
+
+### Scopes
+
+No fine-grained scope is required — the endpoint declares none, so there is no `users:*` scope to ask for.
+
+An API key reaches it unconditionally. An OAuth token still needs to carry a read scope; a token granted only write scopes is rejected with `403`. In practice any token issued for real work already qualifies, which is what makes this a reliable probe when you are unsure what your credential can reach.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ----------------------------------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials — a wrong key, the wrong header, or an expired or revoked OAuth token. |
+| `403` | `forbidden` | The credential is valid but carries no read scope — an OAuth token granted write scopes only. |
+| `404` | `resource_not_found` | The calling principal could not be resolved to a user record. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Split `401` from `403` before you debug anything else
+A `401` here means the credential is wrong, so no other endpoint will work either. A `200` here plus a `403` elsewhere means the credential is fine and the **scope or role** is too narrow for that specific endpoint — compare its documented scope against the `scopes` array in this response.
+:::
+
+::: info Send the key as `X-Api-Key`
+v2 reads the API key from `X-Api-Key`, or an OAuth token from `Authorization: Bearer `. v1's `X-API-Key` casing is a common cause of a `401` here when porting a v1 client. See [Authentication](/api-reference/v2/authentication).
+:::
+
+::: info A `409` is never about this endpoint's answer
+If your scopes look right and a write still returns `409 work_item_types_managed_at_workspace` or `work_item_types_managed_at_project`, stop looking at credentials. The workspace manages work item types on the other surface and the same call succeeds there. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+## Reading the response
+
+- **`principal_kind` tells you which credential model you are in.** `oauth` means an application token acting for a user, `api_key` means a personal key, `other` covers anything else the server authenticates.
+- **`scopes` is the OAuth grant.** For an OAuth token it lists the fine-grained scopes the user consented to; check it against the scope named on the endpoint you are calling. An empty array means no fine-grained scopes are attached to this credential.
+- **`id` is the identity your writes will carry.** It shows up later as `created_by_id` on objects you create and as `actor_id` in [audit logs](/api-reference/v2/audit-logs/overview).
+- **The response says nothing about roles.** Scopes cap what a token may attempt; the user's role in each workspace and project decides whether it succeeds. A `403` with the right scope listed here is a role problem.
+
+## Related
+
+- [Users overview](/api-reference/v2/users/overview)
+- [Authentication](/api-reference/v2/authentication)
+- [Members overview](/api-reference/v2/members/overview) — the roster where this `id` appears as `member_id`
+- [Errors](/api-reference/v2/errors) — the full code list to branch on
diff --git a/docs/api-reference/v2/users/overview.md b/docs/api-reference/v2/users/overview.md
new file mode 100644
index 00000000..12de37d7
--- /dev/null
+++ b/docs/api-reference/v2/users/overview.md
@@ -0,0 +1,101 @@
+---
+title: Users overview
+description: The Plane API v2 users resource. GET /api/v2/users/me/ echoes the calling principal — its kind, identity, and granted OAuth scopes — and is the first call to make when verifying credentials.
+keywords: plane api v2, users, users me, whoami, principal_kind, oauth scopes, api key, auth debugging, verify credentials
+---
+
+# Users overview
+
+The users resource has exactly one endpoint, and it answers one question: **who am I to this API right now?**
+
+`GET /api/v2/users/me/` echoes the calling principal back at you — the kind of credential you presented, the identity it resolves to, and, for OAuth tokens, the scopes it was actually granted. It is the natural first call after wiring up credentials, and the first thing to run when a request is failing and you are not sure why.
+
+
+
+
+## The current user object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ The user this credential acts as. This is the same id you see as `member_id` on the [member rosters](/api-reference/v2/members/overview), `created_by_id` on objects you create, and `actor_id` in [audit logs](/api-reference/v2/audit-logs/overview) — so it is how you confirm that the trail you are looking at is yours.
+
+- `display_name` _string_
+
+ The user's display name.
+
+- `email` _string (email)_
+
+ The user's email address. Useful for confirming you are on the account you meant to be on, especially across staging and production keys.
+
+- `principal_kind` _string_
+
+ What kind of credential is calling. One of:
+ - `oauth` — an OAuth access token issued to an application
+ - `api_key` — a personal API key sent in `X-Api-Key`
+ - `other` — anything else the server recognizes as authenticated
+
+- `scopes` _array of string_
+
+ The scopes this credential carries. For an OAuth token these are the fine-grained scopes the user consented to, for example `projects.work_items:read`. Compare this list against the scope named on the endpoint you are calling before you conclude that a `403` is a role problem.
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| ------ | ------------------- | ---------------------------- |
+| `GET` | `/api/v2/users/me/` | Return the calling principal |
+
+The path has no workspace slug: it describes the credential, not a workspace. There is no `/users/{id}/` in v2 — to look other people up, read a workspace or project roster with [`?expand=member`](/api-reference/v2/members/list-workspace-members).
+
+## Debugging auth with it
+
+This endpoint declares no fine-grained scope requirement — an API key reaches it unconditionally, and an OAuth token needs only a read scope. That makes it a clean way to split failures apart:
+
+| Symptom | What `GET /users/me/` tells you |
+| ------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
+| Every call returns `401` | If this returns `401` too, the credential itself is wrong — bad key, wrong header, expired token. |
+| One call returns `403`, others work | Compare `scopes` against the scope that endpoint documents. A missing scope is the usual cause. |
+| Writes land under an unexpected author | Check `id` and `email` — you are probably holding a different account's key than you think. |
+| Not sure whether an integration is OAuth or a key | Read `principal_kind`. |
+
+::: tip A `409` is not an auth failure
+If `scopes` contains the write scope for the endpoint and the call still fails with `409 work_item_types_managed_at_workspace` or `work_item_types_managed_at_project`, the credential is fine. The workspace manages work item types on the other surface, and the same request succeeds there. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+::: info Scopes are a ceiling, not a grant
+A token can only do what its scopes allow **and** what the underlying user's role allows. A broad scope on a member-level account is still limited by that member's role, so an empty-handed `403` with the right scope listed here points at the role, not the token.
+:::
+
+## Changed from v1
+
+- The response is now about the **principal**, not just the person. `principal_kind` and `scopes` are new, and they are what make the endpoint useful for debugging OAuth apps.
+- `first_name`, `last_name`, `avatar`, and `avatar_url` are no longer returned. `display_name` and `email` remain.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
+
+## Related
+
+- [Get current user](/api-reference/v2/users/get-current-user)
+- [Authentication](/api-reference/v2/authentication) — how to present an API key or OAuth token
+- [Members overview](/api-reference/v2/members/overview) — resolve other people from a roster
diff --git a/docs/api-reference/v2/work-item-comments/create-comment.md b/docs/api-reference/v2/work-item-comments/create-comment.md
new file mode 100644
index 00000000..3488bebd
--- /dev/null
+++ b/docs/api-reference/v2/work-item-comments/create-comment.md
@@ -0,0 +1,215 @@
+---
+title: Create a comment
+description: Post a comment on a Plane work item with the v2 REST API. HTML bodies, INTERNAL and EXTERNAL access, external ids, OAuth scopes, errors, and code examples.
+keywords: plane api v2, create work item comment, POST comments, comment_html, comment access, append-only comments
+---
+
+# Create a comment
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/
+
+
+
+
+
+Post a comment on a work item. The comment is attached to the work item in the path and appears in its
+discussion immediately.
+
+The author is taken from the credentials on the request and returned as `actor_id` — you cannot post a comment
+on behalf of another user.
+
+::: info Comments are not deduplicated
+Nothing about a comment's body or `external_id` identifies it uniquely, so sending the same `comment_html`
+twice creates two comments. If your integration must post a comment at most once, send an `external_id` and
+look for it first with `GET …/comments/?external_id=…&external_source=…`.
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item to comment on.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+The comment body, as HTML — for example `
Deployed the fix to staging.
`. Plane derives the plain-text
+`comment_stripped` from it server-side, which is what search matches.
+
+
+
+
+
+Visibility of the comment.
+
+- `INTERNAL` — visible to the project team
+- `EXTERNAL` — marked as visible outside the team, for example on a published project
+
+Omit it to take the server's default. Change it later with a `PATCH`.
+
+
+
+
+
+Your system's identifier for this comment, for sync and import correlation. Maximum 255 characters. Accepts
+`null`.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `zendesk`. Maximum 255 characters. Accepts `null`.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------- |
+| `400` | `validation_error` | Missing `comment_html`, or an `access` value outside the enum. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't comment on this work item. |
+| `404` | `resource_not_found` | No such workspace, project, or work item, or it's outside your tenant. |
+| `409` | `conflict` | The write conflicts with the current state of the work item. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/work-item-comments/delete-comment.md b/docs/api-reference/v2/work-item-comments/delete-comment.md
new file mode 100644
index 00000000..9ffb6343
--- /dev/null
+++ b/docs/api-reference/v2/work-item-comments/delete-comment.md
@@ -0,0 +1,150 @@
+---
+title: Delete a comment
+description: Delete a comment from a Plane work item with the v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete work item comment, DELETE comment, remove comment, 204 no content
+---
+
+# Delete a comment
+
+
+
+Remove a comment from a work item. On success the response is `204 No Content` with an empty body — there is
+nothing to parse, so branch on the status code.
+
+The delete is a soft delete: the comment stops appearing in every read, including lists and its own detail
+route, and a follow-up `GET` on the same id returns `404 resource_not_found`.
+
+::: warning Deleting a comment is not reversible through the API
+There is no restore endpoint, so confirm with the user before deleting. Deleting the parent work item is the
+usual alternative when you want the whole thread to disappear at once.
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item the comment is attached to.
+
+
+
+
+
+The comment to delete.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | -------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't delete this comment. |
+| `404` | `resource_not_found` | No such comment, it belongs to another work item, or it's already deleted. |
+| `409` | `conflict` | The delete conflicts with the current state of the comment. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/work-item-comments/get-comment.md b/docs/api-reference/v2/work-item-comments/get-comment.md
new file mode 100644
index 00000000..2af6ad83
--- /dev/null
+++ b/docs/api-reference/v2/work-item-comments/get-comment.md
@@ -0,0 +1,157 @@
+---
+title: Get a comment
+description: Retrieve a single comment on a Plane work item with the v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get work item comment, GET comment, comment by id, comment_html
+---
+
+# Get a comment
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/
+
+
+
+
+
+Retrieve one comment by id. Use it to re-read a comment you created or updated, or to check `edited_at` before
+overwriting a body you did not write.
+
+The comment is looked up **within the work item in the path**. A comment id that belongs to a different work
+item is not reachable here and returns `404 resource_not_found`, the same as an id that does not exist.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item the comment is attached to.
+
+
+
+
+
+The comment to retrieve.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------------------ |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this work item. |
+| `404` | `resource_not_found` | No such comment, it belongs to another work item, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/work-item-comments/list-comments.md b/docs/api-reference/v2/work-item-comments/list-comments.md
new file mode 100644
index 00000000..193e9797
--- /dev/null
+++ b/docs/api-reference/v2/work-item-comments/list-comments.md
@@ -0,0 +1,293 @@
+---
+title: List comments
+description: List the comments on a Plane work item with the v2 REST API. Access filter, external id lookup, search, ordering, pagination, scopes, and code examples.
+keywords: plane api v2, list work item comments, GET comments, comment access filter, comment search, comment pagination
+---
+
+# List comments
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/
+
+
+
+
+
+Return the comments on one work item. Comments are scoped to their parent work item, so this is
+the only way to read a work item's discussion — reach for it when rendering a work item detail view or mirroring
+a thread into another system.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item whose comments you want. Comments never span work items, so this narrows the result set by
+itself.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+**Filters**
+
+
+
+
+
+Return only comments with this visibility.
+
+- `INTERNAL` — visible to the project team
+- `EXTERNAL` — marked as visible outside the team
+
+
+
+
+
+Return comments carrying this identifier from your system. `external_id` is not a unique key, so this can match
+more than one comment — pair it with `external_source` and handle a multi-row result.
+
+
+
+
+
+Return comments that came from this system, for example `github` or `zendesk`.
+
+
+
+
+
+**Search**
+
+
+
+
+
+Match comments against their plain-text body (`comment_stripped`), so HTML markup in `comment_html` never
+affects whether a term hits.
+
+
+
+
+
+**Ordering**
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending. Send it explicitly whenever order matters — a thread you
+render should not depend on the server's unstated default.
+
+- `created_at` — oldest first
+- `-created_at` — newest first
+- `id`
+- `-id`
+
+
+
+
+
+**Pagination**
+
+
+
+
+
+Page size. Defaults to 50, maximum 200.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000 — for deeper traversal switch to cursor
+pagination.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope instead of the default offset envelope. The response
+then carries `next_cursor` and `has_more`; send the value of `next_cursor` back as `?cursor=` to fetch the next
+page. See [Pagination](/api-reference/v2/pagination) for the full envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` and omit `total_count` from the offset envelope.
+
+
+
+
+
+::: info `access` is validated, `order_by` is not
+`access` is checked against its allowed values — an unrecognized value is rejected as a `400 validation_error`, so you
+never get a silently empty list back. `order_by` is not checked: an unrecognized value falls back to the default
+ordering, so check your spelling there because a typo shows up as an unexpected sort order rather than an error.
+:::
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this work item. |
+| `404` | `resource_not_found` | No such workspace, project, or work item, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
diff --git a/docs/api-reference/v2/work-item-comments/overview.md b/docs/api-reference/v2/work-item-comments/overview.md
new file mode 100644
index 00000000..30ae4ea7
--- /dev/null
+++ b/docs/api-reference/v2/work-item-comments/overview.md
@@ -0,0 +1,148 @@
+---
+title: Work item comments overview
+description: The Plane API v2 work item comment object. HTML bodies, INTERNAL and EXTERNAL access, the sub-resource path, and why comments never conflict.
+keywords: plane api v2, work item comments, issue comments, comment_html, comment access, internal comment, external comment
+---
+
+# Work item comments overview
+
+A comment is a note posted on a single work item. Comments are a **sub-resource of a work item**, not a
+project-level collection: every route is mounted under the work item, so a comment can only be read or written
+through the work item it belongs to.
+
+```text
+/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/
+```
+
+That means the collection routes carry three path parameters (`slug`, `project_id`, `work_item_id`) and the
+detail routes carry four (plus `pk`). There is no workspace-wide or project-wide comment feed — to gather
+comments across work items, list them per work item.
+
+[Learn more about work item comments](https://docs.plane.so/core-concepts/issues/overview#comment-on-work-items)
+
+
+
+
+## The comment object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the comment.
+
+- `work_item_id` _string (uuid)_
+
+ The work item this comment is attached to. Always matches the `work_item_id` in the request path.
+
+- `comment_html` _string_
+
+ The comment body, as HTML. This is the field you write.
+
+- `comment_stripped` _string_
+
+ The plain-text version of `comment_html`, derived server-side. It is what `?search=` matches, so markup never
+ affects a search hit. Read-only — you never send it.
+
+- `access` _string_
+
+ Visibility of the comment. One of `INTERNAL` or `EXTERNAL`. `INTERNAL` keeps the comment inside the project
+ team; `EXTERNAL` marks it as visible outside the team, for example on a published project.
+
+- `actor_id` _string (uuid)_ or _null_
+
+ The author of the comment — the principal that posted it. Set server-side from the authenticated caller, so
+ you cannot post a comment on someone else's behalf.
+
+- `edited_at` _string (date-time)_ or _null_
+
+ When the comment was last updated through a `PATCH`. `null` on a comment that has never been edited, which is
+ how clients render an "edited" marker.
+
+- `external_id` , `external_source` _string_ or _null_
+
+ Correlation fields for sync and import. Together they let you map a comment to a record in another system and
+ find it again with `?external_id=` and `?external_source=`.
+
+- `created_at` _string (date-time)_
+
+ When the comment was created.
+
+- `created_by_id` _string (uuid)_ or _null_
+
+ The user record that created the comment. For comments posted by a person this is the same user as
+ `actor_id`; `actor_id` is the domain field that Plane renders as the comment author.
+
+::: tip Write `comment_html`, read `comment_stripped`
+`comment_html` is the only body field you can set. Plane derives `comment_stripped` from it on every write, so
+the two never drift apart.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | ------------------------------------------------------------------------------------------ | -------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/` | List comments |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/` | Create comment |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/` | Get comment |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/` | Update comment |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/` | Delete comment |
+
+## Comments are not deduplicated
+
+::: info The API will not collapse duplicates for you
+Nothing about a comment's body or `external_id` identifies it uniquely, so posting the same `comment_html`
+twice creates two comments. If your integration must post a comment at most once, send an `external_id` and
+look for it first with `GET …/comments/?external_id=…&external_source=…` before creating.
+
+Writes can still fail with `409 conflict`, so branch on the `code` in the problem response instead of assuming
+a create always succeeds.
+:::
+
+## Access and visibility
+
+`access` is set per comment and can be changed later with a `PATCH`. Filter a list down to one visibility with
+`?access=INTERNAL` or `?access=EXTERNAL` when you need only the comments a particular audience can see.
+
+## Reads follow the parent work item
+
+Comment visibility is inherited from the work item in the path — there is no per-comment read permission. A
+`pk` that belongs to a different work item is not visible on this route and returns `404 resource_not_found`,
+the same as a comment id that does not exist. Existence is never leaked.
+
+## Changed from v1
+
+- The path segment is `/work-items/{work_item_id}/comments/` and the parent id is reported as **`work_item_id`**
+ instead of `issue`.
+- `actor` is now **`actor_id`**, and `created_by` is now **`created_by_id`**.
+- Reads no longer return `updated_at`, `updated_by`, `deleted_at`, `project`, `workspace`, `comment_json`,
+ `attachments`, or `is_member`.
+- `edited_at` is stamped by Plane on every `PATCH`.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
diff --git a/docs/api-reference/v2/work-item-comments/update-comment.md b/docs/api-reference/v2/work-item-comments/update-comment.md
new file mode 100644
index 00000000..42e69052
--- /dev/null
+++ b/docs/api-reference/v2/work-item-comments/update-comment.md
@@ -0,0 +1,217 @@
+---
+title: Update a comment
+description: Edit a comment on a Plane work item with the v2 REST API. Partial PATCH body, access changes, edited_at, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update work item comment, PATCH comment, edit comment_html, comment access, edited_at
+---
+
+# Update a comment
+
+
+
+Edit an existing comment. Updates are partial — send only the fields you want to change, and everything you omit
+is left untouched. There is no `PUT`.
+
+Plane stamps `edited_at` on the comment when the update succeeds, which is what clients render as an "edited"
+marker. When you send `comment_html`, the plain-text `comment_stripped` is re-derived from it in the same write,
+so search stays in sync with the visible body.
+
+::: tip Editing is not restricted to your own comment id
+Whether you can edit a given comment is decided by your role and token scope, not by a body field. A comment you
+are not allowed to edit returns `403 forbidden`; a comment on another work item returns `404`.
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item the comment is attached to.
+
+
+
+
+
+The comment to update.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Replacement body for the comment, as HTML. It overwrites the previous body — there is no append mode, so send
+the full new text.
+
+
+
+
+
+Change the comment's visibility.
+
+- `INTERNAL` — visible to the project team
+- `EXTERNAL` — marked as visible outside the team, for example on a published project
+
+
+
+
+
+Your system's identifier for this comment. Maximum 255 characters. Accepts `null` to clear the correlation.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `zendesk`. Maximum 255 characters. Accepts `null`.
+
+
+
+
+
+`external_id` is a correlation field, not a key: nothing stops two comments on the same work item from carrying
+the same value, so keep your own side of the mapping authoritative.
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------------------ |
+| `400` | `validation_error` | An `access` value outside the enum, or an `external_id` over 255 characters. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't edit this comment. |
+| `404` | `resource_not_found` | No such comment, it belongs to another work item, or it's outside your tenant. |
+| `409` | `conflict` | The update conflicts with the current state of the comment. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
",
+ "comment_stripped": "Deployed the fix to staging and production.",
+ "access": "INTERNAL",
+ "actor_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
+ "external_id": null,
+ "external_source": null,
+ "edited_at": "2026-01-15T11:04:02.115740Z",
+ "created_at": "2026-01-14T09:22:41.478363Z",
+ "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
+}
+```
+
+
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/validation_error",
+ "title": "Validation Error",
+ "status": 400,
+ "code": "validation_error",
+ "detail": "The request body failed validation.",
+ "errors": [{ "field": "access", "message": "\"PUBLIC\" is not a valid choice." }]
+}
+```
+
+
+
+
+
diff --git a/docs/api-reference/v2/work-item-properties/create-work-item-property.md b/docs/api-reference/v2/work-item-properties/create-work-item-property.md
new file mode 100644
index 00000000..46651bc3
--- /dev/null
+++ b/docs/api-reference/v2/work-item-properties/create-work-item-property.md
@@ -0,0 +1,379 @@
+---
+title: Create a work item property
+description: Define a custom work item property in a Plane project with the v2 REST API. property_type and relation_type enums, inline options, default values, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create work item property, custom field, property_type, relation_type, inline options, default_value, POST work item properties
+---
+
+# Create a work item property
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/
+
+
+
+
+
+Define a new custom field in a project. You choose a label and a `property_type`, and Plane returns the property definition — including the derived `name` slug you can key off in your own storage.
+
+Two things to know before you send this request:
+
+- **Creating a property does not put it on any work item.** The definition exists, but nothing renders it until you attach it to a work item type with [Attach a type property](/api-reference/v2/work-item-type-properties/attach-type-property).
+- **You write `display_name`, not `name`.** `name` is the read-side slug Plane derives; it is not a body field.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to define the property in. Project-level properties belong to this project alone.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+The human-readable label for the field, for example `Severity`. Maximum 255 characters. Plane derives the read-only `name` slug from this value and returns both.
+
+
+
+
+
+What kind of data the field holds. This is the decision that shapes everything else about the property.
+
+- `TEXT` — Free-form text
+- `DATETIME` — A point in time, written as an ISO 8601 timestamp
+- `DECIMAL` — A number, carried as a string in the value array
+- `BOOLEAN` — A yes/no flag
+- `OPTION` — A choice from a fixed set you define
+- `RELATION` — A reference to another record; pair it with `relation_type`
+- `URL` — A link
+- `EMAIL` — An email address
+- `FILE` — An uploaded file
+- `FORMULA` — A value Plane computes rather than one a person enters
+
+The full reference, including what each type means for the value you send on a work item, is on the [properties overview](/api-reference/v2/work-item-properties/overview#property-type-reference).
+
+
+
+
+
+What a `RELATION` property points at. Only meaningful when `property_type` is `RELATION` — leave it out otherwise.
+
+- `ISSUE` — A work item
+- `USER` — A member
+- `RELEASE` — A release
+- `RICH_TEXT` — Rich text content
+
+
+
+
+
+Free-form explanation of what the field is for. Worth filling in — it is the helper text people read when deciding what to type.
+
+
+
+
+
+Write-only. Define the choices for an `OPTION` property inline, in the same request that creates it, instead of a second round-trip per choice.
+
+Each entry is a property option object — `name` is required, and `description`, `is_default`, `external_id`, and `external_source` are accepted. See [Create a property option](/api-reference/v2/work-item-property-options/create-property-option) for the full field list.
+
+You never get `options` back in the shape you sent it. The response carries the resolved `options` array, with each choice's generated `id` and `sort_order`.
+
+
+
+
+
+Allow more than one value on a work item — a multi-select list of severities, a set of reviewers, several linked work items. Leave it off for a single-valued field.
+
+
+
+
+
+Force a value to be present. Give a required property a `default_value` so existing flows have something to fall back on.
+
+
+
+
+
+Whether the property is offered. Create it inactive if you want to define the field and its options now but roll it out later.
+
+
+
+
+
+The value applied when none is supplied. **Always an array**, even for a single-valued property — a `DECIMAL` field that defaults to `3` is sent as `["3"]`, not `3`. A property with no default sends `[]` or omits the field.
+
+For an `OPTION` property, mark the default choice with `is_default` on the option itself rather than repeating it here.
+
+
+
+
+
+Free-form object holding type-specific configuration. What belongs in it depends entirely on `property_type`, so there is no single schema — send the whole object rather than assuming keys.
+
+
+
+
+
+Free-form object holding type-specific validation constraints. Same shape caveat as `settings`.
+
+
+
+
+
+Your system's identifier for this field, for sync and import correlation. Maximum 255 characters.
+
+
+
+
+
+The system `external_id` came from, for example `jira` or `linear`. Maximum 255 characters.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | Missing `display_name` or `property_type`, or an enum value outside the list. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't create properties. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_workspace` | This workspace manages work item types at the workspace level. Create the property on the workspace endpoint instead. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning A 409 means wrong surface, not missing permission
+If the workspace manages work item types at the workspace level, this project endpoint returns `409 work_item_types_managed_at_workspace`. Nothing is broken and no permission is missing — the same property is created through [Create a workspace work item property](/api-reference/v2/workspace-work-item-properties/create-workspace-work-item-property). See [Work item type modes](/api-reference/v2/work-item-type-modes) for how to detect which mode you are in.
+:::
+
+
+
+## Creating a relation property
+
+A `RELATION` property needs `relation_type` to say what it points at. This one lets a work item name several reviewers:
+
+```json
+{
+ "display_name": "Reviewer",
+ "description": "Who signs this off",
+ "property_type": "RELATION",
+ "relation_type": "USER",
+ "is_multi": true
+}
+```
+
+`relation_type` stays `null` on every non-`RELATION` property.
+
+## After you create
+
+1. **Attach it to a type.** [Attach a type property](/api-reference/v2/work-item-type-properties/attach-type-property) is what makes the field appear on work items. One property can be attached to several types.
+2. **Add or adjust options later.** Inline `options` covers the initial set; [Property options](/api-reference/v2/work-item-property-options/list-property-options) handles changes over the property's life.
diff --git a/docs/api-reference/v2/work-item-properties/delete-work-item-property.md b/docs/api-reference/v2/work-item-properties/delete-work-item-property.md
new file mode 100644
index 00000000..f5b8887a
--- /dev/null
+++ b/docs/api-reference/v2/work-item-properties/delete-work-item-property.md
@@ -0,0 +1,170 @@
+---
+title: Delete a work item property
+description: Delete a custom work item property from a Plane project with the v2 REST API. Path parameters, the deactivate alternative, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete work item property, remove custom field, is_active, 204 no content, DELETE work item property
+---
+
+# Delete a work item property
+
+
+
+Remove a custom property definition from a project. The response is `204` with an empty body — there is nothing to parse, so branch on the status code.
+
+Deleting a property removes the field itself, along with the choices defined for it and its attachments to work item types. If you only want the field to stop being offered, set `is_active: false` with [Update a work item property](/api-reference/v2/work-item-properties/update-work-item-property) instead — that keeps the definition and everything recorded against it, and it is reversible.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the property belongs to. A property id from a different project returns `404`, even inside the same workspace.
+
+
+
+
+
+The id of the property to delete.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A malformed identifier in the path. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't delete properties. |
+| `404` | `resource_not_found` | No such property, project, or workspace — or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_workspace` | This workspace manages work item types at the workspace level. Delete the property on the workspace endpoint instead. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning A 409 means wrong surface, not missing permission
+If the workspace manages work item types at the workspace level, this project endpoint returns `409 work_item_types_managed_at_workspace`. The delete belongs on [Delete a workspace work item property](/api-reference/v2/workspace-work-item-properties/delete-workspace-work-item-property). Reading the same property through this path still works — only writes are mode-specific. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+::: info 204 has no body
+A successful delete returns `204 No Content`. Calling `.json()` on the response throws. Check `response.status === 204` (or `response.ok`) instead.
+:::
+
+
+
+## Delete or deactivate?
+
+| You want to… | Do this |
+| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
+| Stop offering the field but keep its history | `PATCH` with `is_active: false` |
+| Take a choice out of an `OPTION` field, keep the field | [Delete a property option](/api-reference/v2/work-item-property-options/delete-property-option) |
+| Remove the field from one type but keep it elsewhere | [Detach a type property](/api-reference/v2/work-item-type-properties/detach-type-property) |
+| Remove the definition from the project entirely | `DELETE` this endpoint |
+
+Deleting is the only one of these you cannot undo by flipping a flag back.
diff --git a/docs/api-reference/v2/work-item-properties/get-work-item-property.md b/docs/api-reference/v2/work-item-properties/get-work-item-property.md
new file mode 100644
index 00000000..1d11df50
--- /dev/null
+++ b/docs/api-reference/v2/work-item-properties/get-work-item-property.md
@@ -0,0 +1,198 @@
+---
+title: Get a work item property
+description: Retrieve a single custom work item property from a Plane project with the v2 REST API. Path parameters, the resolved options array, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get work item property, retrieve custom field, property_type, options, GET work item property by id
+---
+
+# Get a work item property
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{pk}/
+
+
+
+
+
+Retrieve one property definition by id. Reach for this when you already hold a property `id` — from a type's property list, a stored mapping, or a create response — and need its current shape before reading or writing values.
+
+The response includes the resolved `options` array, so a single request tells you both what the field is and which choices it offers. Check `property_type` before you build a value: it decides what the field can hold, and `relation_type` narrows it further for `RELATION` properties.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the property belongs to. A property id from a different project returns `404`, even inside the same workspace.
+
+
+
+
+
+The id of the property to retrieve.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's properties. |
+| `404` | `resource_not_found` | No such property, project, or workspace — or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Reads ignore the mode
+This endpoint works whether the workspace manages work item types at the project or workspace level — only writes are mode-specific, and only writes return a `409`. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+::: info No expansion on properties
+Work item properties don't accept `?expand=`. The response is the object shown here, with `options` already resolved inline.
+:::
+
+::: info Existence is never leaked
+A property outside your tenant returns `404`, not `403`. If a cached id starts returning `404`, re-read the catalog with [List work item properties](/api-reference/v2/work-item-properties/list-work-item-properties) rather than retrying.
+:::
+
+
+
+::: tip `name` is the stable key, `display_name` is the label
+People rename fields. If you are storing a mapping between a Plane property and a field in your own system, key it on `id` — and use `name` when you need something human-legible that changes less often than the label. `display_name` is the one that shows up in the UI, and the one you send when you want to change it.
+:::
diff --git a/docs/api-reference/v2/work-item-properties/list-work-item-properties.md b/docs/api-reference/v2/work-item-properties/list-work-item-properties.md
new file mode 100644
index 00000000..93956a1b
--- /dev/null
+++ b/docs/api-reference/v2/work-item-properties/list-work-item-properties.md
@@ -0,0 +1,308 @@
+---
+title: List work item properties
+description: List the custom work item properties defined in a Plane project with the v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list work item properties, custom fields, custom properties, project properties, pagination, GET work item properties
+---
+
+# List work item properties
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/
+
+
+
+
+
+Return every custom property defined in a project as a paginated list. This is how you discover which fields exist before writing values, and how you resolve a field's label to the property `id` you need for options and type attachments.
+
+The list is the project's full catalog of definitions — including properties with `is_active: false`, and including properties not yet attached to any work item type. To see what a specific type actually offers, use [List type properties](/api-reference/v2/work-item-type-properties/list-type-properties) instead.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project whose properties you want to list.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+There are no filter or search parameters on this endpoint — a project's property catalog is small enough to read whole and filter client-side. Check your spelling on `order_by` and `paginate`: neither is validated, so an unrecognized `order_by` silently falls back to the default ordering and anything other than `paginate=cursor` silently uses offset pagination. A typo shows up as an unexpected sort order or envelope, not as an error.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` , `-sort_order` — the configured display order of the fields
+- `created_at` , `-created_at` — when each property was defined
+- `id` , `-id`
+
+Order by `sort_order` when you are rendering the fields to a person; it is the order the project itself uses.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most projects define far fewer than 50 properties, so one page is usually the whole catalog.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. Omit it for the default offset envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | -------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's properties. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Reads work in either mode
+Listing properties never returns a mode conflict. A project still surfaces its properties when the workspace manages work item types at the workspace level — only **writes** are mode-specific. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+::: tip Inactive properties still appear
+`is_active: false` means the field is no longer offered on new edits, not that it is gone. The list returns it. If you are rendering a form, filter to `is_active: true` yourself — and check the type's own property list, since a property in this catalog may not be attached to the type you are editing.
+:::
diff --git a/docs/api-reference/v2/work-item-properties/overview.md b/docs/api-reference/v2/work-item-properties/overview.md
new file mode 100644
index 00000000..8c7e0072
--- /dev/null
+++ b/docs/api-reference/v2/work-item-properties/overview.md
@@ -0,0 +1,201 @@
+---
+title: Work item properties overview
+description: The Plane API v2 project work item property object. Attributes, the property_type and relation_type reference, default values, inline options, and the endpoints that manage custom fields.
+keywords: plane api v2, work item properties, custom fields, custom properties, property_type, relation_type, OPTION property, project properties
+---
+
+# Work item properties overview
+
+A work item property is a custom field you define on a project. Once a property is attached to a work item type, every work item of that type can carry a value for it — a severity, a customer name, a due-by timestamp, a link to a design doc.
+
+A property is a **definition**, not a value. It says what the field is called, what kind of data it holds, whether it is required, and what choices are available. The values themselves live on individual work items.
+
+Properties in this group are **project-level**: they are defined inside one project and belong to that project alone. The workspace equivalent lives at [Work item properties (workspace)](/api-reference/v2/workspace-work-item-properties/list-workspace-work-item-properties).
+
+::: warning One mode per workspace
+A workspace manages work item types — and therefore their properties — in exactly one mode: **project-level** or **workspace-level**. Writing to the endpoints on this page while the workspace is in workspace mode returns `409 work_item_types_managed_at_workspace`, not a `404` or `403`. The capability exists; it just lives on the other surface. Reads are unaffected by mode. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+
+## The work item property object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the property. This is the id you pass when attaching the property to a type or managing its options.
+
+- `name` _string_
+
+ The machine-readable slug for the property, derived by Plane. Read-only — it is not a create or update body field. Use it when you need a stable key in your own storage.
+
+- `display_name` _string_
+
+ The human-readable label shown wherever the property is rendered. This is the field you **write** on create and update. Maximum 255 characters.
+
+- `description` _string_
+
+ Free-form explanation of what the field is for. Surfaced as helper text next to the field.
+
+- `property_type` _string_
+
+ What kind of data the property holds. One of `TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `OPTION`, `RELATION`, `URL`, `EMAIL`, `FILE`, or `FORMULA`. See the [property type reference](#property-type-reference) below.
+
+- `relation_type` _string_
+
+ What a `RELATION` property points at. One of `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`. `null` for every other property type.
+
+- `is_multi` _boolean_
+
+ Whether the property accepts more than one value. A multi-select severity list, a set of assignable reviewers, several linked work items.
+
+- `is_required` _boolean_
+
+ Whether a value must be present. Turning this on for an existing property affects work items edited from then on — it does not retroactively fill in the blanks.
+
+- `is_active` _boolean_
+
+ Whether the property is currently offered. Deactivating is the reversible alternative to deleting: the definition and its recorded values stay, the field stops being offered on new edits.
+
+- `default_value` _array of string_
+
+ The value applied when none is supplied. Always an array, even for a single-valued property — a decimal that defaults to `3` is `["3"]`, not `3`.
+
+- `options` _array of any_
+
+ For `OPTION` properties, the resolved list of choices. Each entry is a property option object with its own `id`, `name`, `is_default`, and `sort_order`. Manage them through [Property options](/api-reference/v2/work-item-property-options/list-property-options), or define them inline when you [create the property](/api-reference/v2/work-item-properties/create-work-item-property).
+
+- `settings` _any_
+
+ Free-form object holding type-specific configuration. What it contains depends entirely on `property_type` — treat it as opaque unless you wrote the value yourself.
+
+- `validation_rules` _any_
+
+ Free-form object holding type-specific validation constraints. Same caveat as `settings`: the shape is per type, so read and write it as a whole rather than assuming keys.
+
+- `logo_props` _any_
+
+ Icon configuration used when rendering the property. Read-only through this API.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map a property to a field in another system and find it again later.
+
+- `created_at` _string (date-time)_
+
+ When the property was created.
+
+::: tip `display_name` writes, `name` reads
+These are two different fields and mixing them up is the most common first error. You send `display_name` on create; Plane derives `name` from it and returns both. There is no `name` field in a request body.
+:::
+
+
+
+## Property type reference
+
+`property_type` is required on create and fixes what the field can hold. The **Value shape** column describes the value the property carries — `default_value` uses exactly that shape, always wrapped in an array.
+
+| `property_type` | What it is | Value shape |
+| --------------- | ------------------------- | -------------------------------------------------------------------------------- |
+| `TEXT` | Free-form text | A string, for example `["Needs a repro"]`. |
+| `DATETIME` | A point in time | An ISO 8601 timestamp, for example `["2026-03-01T00:00:00Z"]`. |
+| `DECIMAL` | A number | The number expressed as a string, for example `["3"]` or `["12.5"]`. |
+| `BOOLEAN` | A yes/no flag | `["true"]` or `["false"]`. |
+| `OPTION` | A choice from a fixed set | One of the property's defined options. With `is_multi`, several of them. |
+| `RELATION` | A reference to a record | The referenced record, with `relation_type` deciding what kind. See below. |
+| `URL` | A link | A URL string, for example `["https://example.com/spec"]`. |
+| `EMAIL` | An email address | An address string, for example `["ana@example.com"]`. |
+| `FILE` | An uploaded file | A reference to the stored file. |
+| `FORMULA` | A computed value | Derived by Plane rather than entered by a person. Its configuration is per type. |
+
+### `relation_type`
+
+`relation_type` is only meaningful when `property_type` is `RELATION`. It is `null` on every other property.
+
+| `relation_type` | Points at |
+| --------------- | ----------------- |
+| `ISSUE` | A work item |
+| `USER` | A member |
+| `RELEASE` | A release |
+| `RICH_TEXT` | Rich text content |
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | ---------------------------------------------------------------------------- | --------------------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/` | List work item properties |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/` | Create a work item property |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{pk}/` | Get a work item property |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{pk}/` | Update a work item property |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{pk}/` | Delete a work item property |
+
+## Creating a property is only half the job
+
+A property that exists is not yet a field anyone can fill in. Creating it defines the field; **attaching it to a work item type** is what makes it appear on work items.
+
+1. Create the property with [Create a work item property](/api-reference/v2/work-item-properties/create-work-item-property).
+2. Attach it to a type with [Attach a type property](/api-reference/v2/work-item-type-properties/attach-type-property).
+
+The same property can be attached to more than one type, which is why the two steps are separate.
+
+## Defining options
+
+`OPTION` properties need choices. You have two paths:
+
+- **Inline on create** — pass an `options` array in the create or update body and define the choices in the same request. `options` is write-only: you send it, and the response comes back with the resolved `options` array instead.
+- **Separately** — create the property first, then add choices one at a time through [Property options](/api-reference/v2/work-item-property-options/create-property-option). Use this when options change over the life of the property.
+
+## Deactivate before you delete
+
+`is_active: false` stops a property from being offered without removing it. Deleting is permanent for the definition and takes its options with it. If you are retiring a field but still need to read historical values, deactivate it.
diff --git a/docs/api-reference/v2/work-item-properties/update-work-item-property.md b/docs/api-reference/v2/work-item-properties/update-work-item-property.md
new file mode 100644
index 00000000..9d49ab68
--- /dev/null
+++ b/docs/api-reference/v2/work-item-properties/update-work-item-property.md
@@ -0,0 +1,287 @@
+---
+title: Update a work item property
+description: Update a custom work item property in a Plane project with the v2 REST API. Partial PATCH semantics, default values, inline options, is_active, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update work item property, patch custom field, default_value, is_active, is_required, options, PATCH work item property
+---
+
+# Update a work item property
+
+
+
+Change a property's label, description, default, requirement, or availability. Values already recorded on work items stay where they are — you are editing the field definition, not rewriting anyone's data.
+
+`PATCH` is partial. Send only the fields you want to change; anything you omit keeps its current value. Omitting a field is not the same as sending `null`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the property belongs to.
+
+
+
+
+
+The id of the property to update.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional — send the subset you are changing. There is no `name` field: `name` is the read-side slug, and `display_name` is what you write.
+
+
+
+
+
+New human-readable label for the field. Maximum 255 characters.
+
+
+
+
+
+Free-form explanation of what the field is for, shown as helper text.
+
+
+
+
+
+The kind of data the field holds: `TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `OPTION`, `RELATION`, `URL`, `EMAIL`, `FILE`, or `FORMULA`.
+
+Changing the type of a property that already holds values changes what those values mean, so treat this as a migration rather than an edit — in most cases creating a new property and retiring the old one with `is_active: false` is the safer move. The full reference is on the [properties overview](/api-reference/v2/work-item-properties/overview#property-type-reference).
+
+
+
+
+
+What a `RELATION` property points at: `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`. Only meaningful when `property_type` is `RELATION`.
+
+
+
+
+
+Write-only. Define `OPTION` choices inline, the same way you can on create. Each entry is a property option object with `name` required.
+
+For adding, editing, or removing a single choice on a live property, prefer the dedicated endpoints under [Property options](/api-reference/v2/work-item-property-options/list-property-options) — they let you address one option by id instead of restating the set. The response always returns the resolved `options` array, never the payload you sent.
+
+
+
+
+
+Whether the field accepts more than one value. Turning it off on a property that already holds several values per work item is a narrowing change — check your data first.
+
+
+
+
+
+Whether a value must be present. This applies going forward: existing work items with the field empty are not rejected retroactively, but the next edit will ask for a value. Pair it with a `default_value` so automated flows have something to fall back on.
+
+
+
+
+
+Whether the property is offered. Setting `false` is the reversible way to retire a field — the definition, its options, and its recorded values all survive, and the field simply stops being offered.
+
+
+
+
+
+The value applied when none is supplied. **Always an array**, even for a single-valued property — a `DECIMAL` field that defaults to `3` is sent as `["3"]`, not `3`. Send `[]` to clear the default.
+
+
+
+
+
+Free-form object holding type-specific configuration. The shape depends on `property_type`, so send the whole object rather than assuming keys — a partial `settings` object replaces the stored one.
+
+
+
+
+
+Free-form object holding type-specific validation constraints. Same whole-object caveat as `settings`.
+
+
+
+
+
+Your system's identifier for this field, for sync and import correlation. Maximum 255 characters.
+
+
+
+
+
+The system `external_id` came from, for example `jira` or `linear`. Maximum 255 characters.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | An enum value outside the list, or a field over its length limit. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't update properties. |
+| `404` | `resource_not_found` | No such property, project, or workspace — or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_workspace` | This workspace manages work item types at the workspace level. Update the property on the workspace endpoint instead. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning A 409 means wrong surface, not missing permission
+If the workspace manages work item types at the workspace level, this project endpoint returns `409 work_item_types_managed_at_workspace` — the write belongs on [Update a workspace work item property](/api-reference/v2/workspace-work-item-properties/update-workspace-work-item-property) instead. Reading the same property through this path still works. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+::: info `name` is not writable
+`name` is derived by Plane and returned on reads only. Renaming the field means sending a new `display_name`.
+:::
+
+
+
+## Retiring a field without losing data
+
+Deleting a property is permanent. When you want a field to stop appearing but its history to stay readable, deactivate it:
+
+```json
+{
+ "is_active": false
+}
+```
+
+The definition, its options, and every value already recorded stay in place, and setting `is_active` back to `true` brings the field back exactly as it was.
diff --git a/docs/api-reference/v2/work-item-property-contexts/create-property-context.md b/docs/api-reference/v2/work-item-property-contexts/create-property-context.md
new file mode 100644
index 00000000..b06459ea
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-contexts/create-property-context.md
@@ -0,0 +1,379 @@
+---
+title: Create a property context
+description: Create a context that scopes a workspace-level work item property to specific projects and work item types with the Plane v2 REST API. Wildcard flags, id lists, overrides, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create property context, applies_to_all_projects, applies_to_all_work_item_types, project_ids, issue_type_ids, property override, POST contexts
+---
+
+# Create a property context
+
+
+ POST
+ /api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/
+
+
+
+
+
+Add a context to a workspace-level property. The context names the projects and work item types it covers, and carries the `is_required`, `is_multi`, `default_value`, `settings`, and option list that apply inside that scope.
+
+Reach for this when one property definition needs different rules in different places — for example a `Severity` property that is required on `Bug` work items in two projects and optional everywhere else.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property the context belongs to. A project-level property id is not addressable here and returns `404 resource_not_found`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+No single field is required by the schema, but the **scope is**: each of the two axes must be pinned down, either by setting its wildcard flag to `true` or by supplying a non-empty id list. Both flags default to `false`, so a body that omits `project_ids` and `issue_type_ids` entirely is a `400`, not a context that covers everything.
+
+
+
+
+
+Display name, unique among this property's contexts. Maximum 255 characters. Always send one — the context Plane seeded with the property already occupies the name `Default`.
+
+
+
+
+
+Set to `true` to cover every project in the workspace instead of listing them. Defaults to `false`, which means `project_ids` is required.
+
+Only one context per property may set this to `true`, and the seeded context normally holds that slot, so a second all-projects context returns `400`.
+
+
+
+
+
+The exact projects this context covers. Required when `applies_to_all_projects` is `false`, and rejected with a `400` when it is `true` — the flag and the list are alternatives, never a combination.
+
+Every id must belong to this workspace, and duplicates are rejected. This field is write-only as an input, but the resulting links are echoed back in the response's read-only `project_ids`.
+
+
+
+
+
+Set to `true` to cover every work item type in the workspace instead of listing them. Defaults to `false`, which means `issue_type_ids` is required.
+
+
+
+
+
+The exact work item types this context covers. Required when `applies_to_all_work_item_types` is `false`, and rejected with a `400` when it is `true`.
+
+Every id must belong to this workspace, and duplicates are rejected. Write-only as an input; the response echoes the stored links in `issue_type_ids`.
+
+
+
+
+
+Whether the property must be filled in on work items inside this scope. This replaces the property's own `is_required` here — it does not combine with it.
+
+
+
+
+
+Whether the property accepts several values inside this scope. Replaces the property's own `is_multi` here.
+
+
+
+
+
+Values applied when a work item in this scope has nothing set. For `OPTION` properties, any option in this context flagged `is_default` takes precedence over this array.
+
+
+
+
+
+The choices this context offers for an `OPTION` property. Options belong to a context, so this list — not the property's full option list — is what work items in this scope can pick from.
+
+Each entry either references an existing option of the property or creates a new one:
+
+- `id` _string (uuid)_ — an existing option of this property. Duplicate ids and ids from another property are rejected.
+- `name` _string_ — creates a new option. Maximum 255 characters. Names must be unique within the payload, case-insensitively.
+- `description` _string_ — free-form description of the option.
+- `is_default` _boolean_ — preselect this option for new work items in this scope. Defaults to `false`.
+
+Send either `id` or `name` on each entry — an entry with neither is a `400`. Write-only as an input; the response returns the resulting options with their ids.
+
+An option's own `sort_order` is read-only: Plane assigns it from the order of the entries you send, and it is returned on each option in the response.
+
+
+
+
+
+Type-specific configuration for this scope, as a JSON object. Replaces the property's own `settings` here.
+
+
+
+
+
+Ordering weight among this property's contexts. Lower values sort first. Omit it and Plane places the new context after the ones that already exist. Ordering is presentational — it does not affect which context wins.
+
+
+
+
+
+Your system's identifier for this context, for sync and import correlation. Maximum 255 characters. Must be sent together with `external_source`, and the pair must be unique within the property.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters. Must be sent together with `external_id`.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | An axis left unpinned, a list sent alongside its wildcard flag, an id outside the workspace, a duplicate name, a half-filled external pair, or an overlap with a same-tier context. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't change workspace property settings. |
+| `404` | `resource_not_found` | No such workspace or property, the property is project-level, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types per project, so workspace-level property writes are refused. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning Same-tier overlap is rejected
+Two contexts on the same property may overlap only when one is strictly more specific than the other — that is what makes narrow rules beat broad ones. Two contexts at the same level of specificity that share a project return `400`: two listed-project/listed-type contexts sharing a project **and** a type, or two listed-project/all-type contexts sharing a project.
+:::
+
+::: info Workspace mode only
+Creating a context requires the workspace to manage work item types at the workspace level. In project mode the request returns `409 work_item_types_managed_at_project` — the capability exists, it just lives on the project surface. Reads are unaffected. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+## Pinning each axis
+
+A context covers the intersection of a project set and a work item type set, and each set is expressed either by its wildcard flag or by its id list. Four shapes are valid, and they map one-to-one onto the precedence tiers Plane uses when it resolves a work item:
+
+| Projects | Work item types | Covers |
+| ------------------------------- | -------------------------------------- | --------------------------------------------------------- |
+| `project_ids: [...]` | `issue_type_ids: [...]` | Those types, in those projects. Most specific. |
+| `project_ids: [...]` | `applies_to_all_work_item_types: true` | Every type, in those projects |
+| `applies_to_all_projects: true` | `issue_type_ids: [...]` | Those types, in every project |
+| `applies_to_all_projects: true` | `applies_to_all_work_item_types: true` | Everything. Least specific — normally the seeded context. |
+
+Two mistakes produce a `400` rather than a surprising context:
+
+- **Neither the flag nor the list.** Omitting `project_ids` while `applies_to_all_projects` is `false` does not quietly mean "all projects"; it means the context would cover nothing, so it is rejected.
+- **Both the flag and the list.** Sending `project_ids` with `applies_to_all_projects: true` is rejected because the list could never take effect. Omit it.
+
+The same two rules apply on the work item type axis.
+
+::: tip Verify with a read-back
+The response echoes `project_ids` and `issue_type_ids` as stored. A wildcard context always reads back with the matching list empty — `"applies_to_all_projects": true` with `"project_ids": []` means every project, not none. Read the flag first.
+:::
+
+## What the new context changes
+
+Inside its scope, the context's `is_required`, `is_multi`, `default_value`, `settings`, and options replace the property's own values wholesale. Nothing is merged, and option lists are not unioned: a `Bug` in the Platform project can only choose from `S1`, `S2`, and `S3` above, even if the property's other contexts offer more.
+
+Outside its scope, nothing changes — work items keep resolving to whichever context was already the most specific match for them.
+
+See [Property contexts overview](/api-reference/v2/work-item-property-contexts/overview) for the full precedence rules, and [Update a context](/api-reference/v2/work-item-property-contexts/update-property-context) for how scope changes affect values that were already recorded.
diff --git a/docs/api-reference/v2/work-item-property-contexts/delete-property-context.md b/docs/api-reference/v2/work-item-property-contexts/delete-property-context.md
new file mode 100644
index 00000000..4d5fd6dc
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-contexts/delete-property-context.md
@@ -0,0 +1,166 @@
+---
+title: Delete a property context
+description: Delete a context from a workspace-level work item property with the Plane v2 REST API. What happens to the context's options and recorded values, mode conflicts, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete property context, remove context, property scope, 204, work item type modes, DELETE contexts
+---
+
+# Delete a property context
+
+
+
+Remove a context from a property. A successful delete returns `204` with an empty body.
+
+Deleting a context does not just remove a rule — it removes coverage. Every work item that resolved to this context falls through to the next most specific context, and if none matches, the property stops appearing on those work items entirely. Read [What a delete takes with it](#what-a-delete-takes-with-it) before calling this on a context that is in use.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property the context belongs to. A project-level property id is not addressable here and returns `404 resource_not_found`.
+
+
+
+
+
+The id of the context to delete.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | --------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A path segment is not a valid UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't change workspace property settings. |
+| `404` | `resource_not_found` | No such context, property, or workspace — or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types per project, so workspace-level property writes are refused. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Workspace mode only
+Deleting a context requires the workspace to manage work item types at the workspace level. In project mode the request returns `409 work_item_types_managed_at_project` — the capability exists, it just lives on the project surface. Reads are unaffected. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+## What a delete takes with it
+
+The delete is not limited to the context row:
+
+- **The context's options go with it.** Options belong to a context, not to the property, so deleting the context deletes the choices it offered — and any value recorded against one of those options is cleared.
+- **Cells that lose their last covering context lose their values.** For each project and work item type combination that this context covered, Plane re-resolves the property. If a less specific context still matches, work items there switch to its rules. If nothing matches, the property no longer applies in that combination and the values recorded there are removed.
+
+There is no restore operation. Treat the `204` as final.
+
+## Deleting the all-projects context
+
+The context Plane seeds with a property covers all projects and all work item types, which makes it the fallback that every uncovered work item lands on. Deleting it does not delete the property, but it does leave the property invisible everywhere your narrower contexts do not reach.
+
+That slot also carries a constraint: only one context per property may set `applies_to_all_projects`. So deleting this context is how you free the slot when you want a different context to be the catch-all — create the replacement first if you would rather not have a gap, then move the flag with [Update a context](/api-reference/v2/work-item-property-contexts/update-property-context).
+
+## A safer teardown
+
+When a context is in active use, retire it in this order:
+
+1. [List the property's contexts](/api-reference/v2/work-item-property-contexts/list-property-contexts) and work out which context each covered project and work item type will fall back to.
+2. If the fallback is the wrong rule, [update](/api-reference/v2/work-item-property-contexts/update-property-context) it — or create the replacement context — before deleting anything.
+3. Delete the context you are retiring.
+
+Doing it in the other order leaves a window where the property is unreachable in those projects, and the values recorded there are cleaned up rather than parked.
diff --git a/docs/api-reference/v2/work-item-property-contexts/get-property-context.md b/docs/api-reference/v2/work-item-property-contexts/get-property-context.md
new file mode 100644
index 00000000..3f3d74f6
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-contexts/get-property-context.md
@@ -0,0 +1,187 @@
+---
+title: Get a property context
+description: Retrieve a single context of a workspace-level work item property with the Plane v2 REST API. Scope flags, overrides, options, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get property context, retrieve context, applies_to_all_projects, project_ids, issue_type_ids, GET contexts
+---
+
+# Get a property context
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/{pk}/
+
+
+
+
+
+Retrieve one context by id. Use it to confirm the scope and overrides you just wrote, or to read the current state before sending a `PATCH`.
+
+The response is the full context: the two wildcard flags, the resolved `project_ids` and `issue_type_ids`, the values this context imposes on the property, and the options it offers.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property the context belongs to. A project-level property id is not addressable here and returns `404 resource_not_found`.
+
+
+
+
+
+The id of the context to retrieve. A context that belongs to a different property returns `404`, even if the id exists.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read workspace property settings. |
+| `404` | `resource_not_found` | No such context, property, or workspace — or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Reads work in either mode
+Retrieving a context does not depend on how the workspace manages work item types. Only `POST`, `PATCH`, and `DELETE` are mode-gated — see [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+## Reading the response
+
+The scope is two pairs of fields, and each pair has to be read in order:
+
+- `applies_to_all_projects`, then `project_ids`. The context above lists two projects. Had the flag been `true`, `project_ids` would be `[]` and the context would cover **every** project — an empty list is never "no projects".
+- `applies_to_all_work_item_types`, then `issue_type_ids`, on the same principle.
+
+`is_required`, `is_multi`, `default_value`, `settings`, and `options` are what this context imposes on the property inside that scope. They replace the property's own values rather than combining with them, so reading the property alone will not tell you how it behaves on a given work item — you need the context that resolves for that work item's project and type.
+
+A single context is also not enough to predict behavior on its own, because a more specific context can outrank it. [List property contexts](/api-reference/v2/work-item-property-contexts/list-property-contexts) returns every context on the property, which is what you need to rank them.
diff --git a/docs/api-reference/v2/work-item-property-contexts/list-property-contexts.md b/docs/api-reference/v2/work-item-property-contexts/list-property-contexts.md
new file mode 100644
index 00000000..ba6182b1
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-contexts/list-property-contexts.md
@@ -0,0 +1,302 @@
+---
+title: List property contexts
+description: List the contexts of a workspace-level work item property with the Plane v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list property contexts, work item property scope, applies_to_all_projects, project_ids, pagination, GET contexts
+---
+
+# List property contexts
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/
+
+
+
+
+
+Return every context defined on a workspace-level property, as a paginated list. This is how you see the full picture of where a property applies and what it does in each place, before deciding whether to add a new context or edit an existing one.
+
+Read the whole list rather than a single context when you need to reason about precedence: a context's effect depends on which other contexts exist. Contexts are returned in `sort_order` by default.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property whose contexts you want. A project-level property id is not addressable here and returns `404 resource_not_found`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+Check your spelling on `order_by` and `paginate` — neither is validated, so both fail silently. An unrecognized `order_by` value falls back to the default ordering, and anything other than `paginate=cursor` uses offset pagination. A typo shows up as an unexpected sort order or envelope, not as an error.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` , `-sort_order` — the property's own context order. This is the default.
+- `created_at` , `-created_at` — when each context was added
+- `id` , `-id`
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. A property usually has a handful of contexts, so one page is normally the whole set.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. Pair it with `order_by=created_at` or `order_by=id` — the default `sort_order` ordering is not unique, so it is not cursor-eligible and the request is rejected with `ordering_not_cursor_eligible`.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read workspace property settings. |
+| `404` | `resource_not_found` | No such workspace or property, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Reads work in either mode
+Listing contexts does not depend on how the workspace manages work item types. Only `POST`, `PATCH`, and `DELETE` are mode-gated — see [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+## Reading the scope of each row
+
+Two fields decide coverage on each axis, and the list is only legible if you read them in the right order:
+
+- `applies_to_all_projects` first, `project_ids` second. The first row above covers **every** project — its empty `project_ids` is a consequence of the wildcard, not an empty scope.
+- `applies_to_all_work_item_types` first, `issue_type_ids` second, on the same principle.
+
+Ranking the rows you get back tells you which one a given work item will actually use. Most specific wins: listed projects and listed types beat listed projects and all types, which beat all projects and listed types, which beat all projects and all types. See [Property contexts overview](/api-reference/v2/work-item-property-contexts/overview) for the full precedence rules.
diff --git a/docs/api-reference/v2/work-item-property-contexts/overview.md b/docs/api-reference/v2/work-item-property-contexts/overview.md
new file mode 100644
index 00000000..70ab07f2
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-contexts/overview.md
@@ -0,0 +1,246 @@
+---
+title: Property contexts overview
+description: The Plane API v2 work item property context object. How a workspace-level property is scoped to projects and work item types, how the applies_to_all wildcards interact with explicit id lists, and how a context overrides the property it belongs to.
+keywords: plane api v2, property contexts, work item property scope, applies_to_all_projects, applies_to_all_work_item_types, project_ids, issue_type_ids, property override
+---
+
+# Property contexts overview
+
+A context scopes a **workspace-level** work item property to a set of projects and a set of work item types, and overrides how that property behaves inside that scope.
+
+A property is defined once for the whole workspace, so it carries exactly one `is_required` flag, one `is_multi` flag, one default value, and one list of options. That is rarely what a real workspace wants. Contexts are how one property definition expresses different rules in different places.
+
+## The problem a context solves
+
+Your workspace has a `Severity` property. You want it to be **required** on `Bug` work items in the Platform and Mobile projects, with a full `S1`/`S2`/`S3` scale — and **optional**, with a shorter list, everywhere else.
+
+That is two sets of rules for one property, so it takes two contexts:
+
+- The context Plane seeds with the property covers all projects and all work item types. Leave it at `is_required: false`. This is the "everywhere else" rule.
+- A second context names the two projects and the `Bug` type, and sets `is_required: true` with its own options. This is the narrow rule.
+
+The narrow context is the one that wins on a Bug in Platform, because Plane always resolves the **most specific** matching context. On a Task in Platform, or on a Bug in a third project, nothing narrow matches and the seeded context applies instead.
+
+Creating the narrow rule is a single request:
+
+```json
+{
+ "name": "Bug severity",
+ "applies_to_all_projects": false,
+ "project_ids": ["4af68566-94a4-4eb3-94aa-50dc9427067b", "9c3d7e21-6b45-4a80-8f19-2e0c7b5d3a64"],
+ "applies_to_all_work_item_types": false,
+ "issue_type_ids": ["b7e04a95-2f18-4c63-9d57-8a12e6b0f439"],
+ "is_required": true,
+ "is_multi": false,
+ "options": [{ "name": "S1 — Critical" }, { "name": "S2 — Major", "is_default": true }, { "name": "S3 — Minor" }]
+}
+```
+
+::: info Every property already has one context
+When you create a workspace-level property, Plane seeds a context named `Default` that covers all projects and all work item types and mirrors the property's own `is_required`, `is_multi`, `default_value`, and `settings`. It is a normal context — you can read it, update it, and delete it through these endpoints. Its `is_default` flag is a historical marker and does not affect resolution.
+:::
+
+
+
+
+## The property context object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the context.
+
+- `name` _string_
+
+ Display name, unique among the contexts of this property. Maximum 255 characters.
+
+- `is_required` _boolean_
+
+ Whether the property must be filled in inside this scope. Overrides the property's own `is_required`.
+
+- `is_multi` _boolean_
+
+ Whether the property accepts several values inside this scope. Overrides the property's own `is_multi`.
+
+- `is_default` _boolean_
+
+ `true` on the context Plane seeded with the property. It is a marker only — precedence is decided by the scope flags, not by this field.
+
+- `default_value` _array of string_
+
+ Values applied when a work item in this scope has nothing set. For `OPTION` properties, options in this context flagged `is_default` take precedence over this array.
+
+- `settings` _any_
+
+ Type-specific configuration for this scope, as a JSON object. Overrides the property's own `settings`.
+
+- `sort_order` _number_
+
+ Ordering weight among this property's contexts. Lower values sort first, and lists are returned in this order by default.
+
+- `applies_to_all_projects` _boolean_
+
+ When `true`, the context covers every project in the workspace and `project_ids` is empty. When `false`, `project_ids` is the exact list of projects covered.
+
+- `applies_to_all_work_item_types` _boolean_
+
+ When `true`, the context covers every work item type and `issue_type_ids` is empty. When `false`, `issue_type_ids` is the exact list of types covered.
+
+- `project_ids` _array of any_
+
+ The ids of the projects this context covers. Empty when `applies_to_all_projects` is `true` — read the flag before reading this list.
+
+- `issue_type_ids` _array of any_
+
+ The ids of the work item types this context covers. Empty when `applies_to_all_work_item_types` is `true`.
+
+- `options` _array of any_
+
+ The choices this context offers for an `OPTION` property. Each entry has `id`, `name`, `is_default`, and `sort_order`. Options belong to a context, not to the property, so two contexts on the same property can offer different lists.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map a context to a record in another system and find it again later. A pair is unique within a property.
+
+- `created_at` _string (date-time)_
+
+ When the context was created.
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | ----------------------------------------------------------------------------- | ---------------- |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/` | List contexts |
+| `POST` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/` | Create a context |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/{pk}/` | Get a context |
+| `PATCH` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/{pk}/` | Update a context |
+| `DELETE` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/contexts/{pk}/` | Delete a context |
+
+`{property_id}` must be a workspace-level property. A project-level property id is not addressable here and returns `404 resource_not_found`.
+
+## Scope: wildcards and explicit lists
+
+A context covers the **intersection** of a project set and a work item type set. Each of those two sets is expressed in one of two ways, and the wildcard flag decides which:
+
+| Flag | Companion list | Coverage |
+| --------------------------------------- | ----------------------- | ------------------------------------- |
+| `applies_to_all_projects: true` | `project_ids` empty | Every project in the workspace |
+| `applies_to_all_projects: false` | `project_ids` listed | Exactly the projects you listed |
+| `applies_to_all_work_item_types: true` | `issue_type_ids` empty | Every work item type in the workspace |
+| `applies_to_all_work_item_types: false` | `issue_type_ids` listed | Exactly the types you listed |
+
+The flag and the list are alternatives, never a combination:
+
+- **On write**, sending `project_ids` alongside `applies_to_all_projects: true` is a `400` — the list would have no meaning, so Plane rejects it rather than silently dropping it. The same rule applies to `issue_type_ids` and `applies_to_all_work_item_types`.
+- **On read**, `project_ids` and `issue_type_ids` are computed from the links Plane actually stores. Turning a wildcard on removes those links, so a wildcard context always reads back with an empty list.
+
+::: warning An empty list does not mean "nothing"
+`"project_ids": []` with `"applies_to_all_projects": true` means **every project**. `"project_ids": []` never appears with the flag `false`, because a context can never be scoped to nothing. Always branch on the flag first, then read the list.
+:::
+
+Each axis must be pinned down one way or the other. On create, omitting both the flag and the list for an axis is a `400` — see [Create a context](/api-reference/v2/work-item-property-contexts/create-property-context). On update, you cannot leave an axis empty either: turning a wildcard off has to arrive in the same request as the ids that replace it.
+
+::: info Only one context per property can use `applies_to_all_projects`
+That slot is occupied by the context Plane seeds with the property. Creating a second all-projects context returns `400`. To move it, update the seeded context or delete it first.
+:::
+
+## How Plane picks the context
+
+For a given work item, Plane resolves one context from the property's list by walking four tiers, most specific first, using the work item's project and type:
+
+1. Listed project **and** listed type.
+2. Listed project **and** all types.
+3. All projects **and** listed type.
+4. All projects **and** all types — the seeded context.
+
+The first tier with a match wins outright; the remaining tiers are not consulted and nothing is merged between them. Contexts at _different_ tiers are meant to overlap — that overlap is exactly how a narrow rule beats a broad one. Overlap _within the same tier_ is a genuine ambiguity and is rejected at write time with a `400`.
+
+::: warning No matching context means the property disappears
+If no tier matches a work item's project and type, the property does not surface on that work item at all. This is the practical consequence of deleting the all-projects, all-types context — see [Delete a context](/api-reference/v2/work-item-property-contexts/delete-property-context).
+:::
+
+## What a context overrides
+
+The resolved context replaces the property's values wholesale for work items in its scope — `is_required`, `is_multi`, `default_value`, `settings`, and the list of selectable options all come from the context, not from the property.
+
+Working through the `Severity` example, with the seeded context left at its defaults:
+
+| Field | On the seeded context | On the `Bug severity` context | Bug in Platform | Task in Platform |
+| ------------- | --------------------- | ----------------------------- | --------------- | ---------------- |
+| Tier | 4 (all / all) | 1 (listed / listed) | Tier 1 wins | Tier 4 wins |
+| `is_required` | `false` | `true` | Required | Optional |
+| `is_multi` | `false` | `false` | Single value | Single value |
+| Options | `Low`, `High` | `S1`, `S2`, `S3` | `S1`/`S2`/`S3` | `Low`/`High` |
+| Default | none | `S2 — Major` (`is_default`) | `S2 — Major` | none |
+
+The Bug in Platform never sees `Low` or `High`: option lists are not unioned across contexts, and the resolved context's list is the whole set of permitted values.
+
+## Options belong to a context
+
+Options created through the [property options endpoints](/api-reference/v2/workspace-work-item-property-options/overview) attach to the property's all-projects, all-types context. To give a narrower context its own choices, send the `options` array when you [create](/api-reference/v2/work-item-property-contexts/create-property-context) or [update](/api-reference/v2/work-item-property-contexts/update-property-context) the context. Each entry either references an existing option of the property by `id` or creates a new one by `name`.
+
+On update, `options` is a replacement for the whole list, not an addition to it — an option you leave out is removed from the context along with the values recorded through it.
+
+## Contexts are workspace mode only
+
+These endpoints exist because the property they hang off is defined at the workspace level. `POST`, `PATCH`, and `DELETE` therefore require the workspace to be managing work item types at the workspace level; if it manages them per project, the write returns `409 work_item_types_managed_at_project` instead. Reads are unaffected by the mode.
+
+See [Work item type modes](/api-reference/v2/work-item-type-modes) for how to check which mode a workspace is in.
+
+## Scopes
+
+| Operation | Scope |
+| ---------------------- | --------------------------------------- |
+| List, Get | `workspaces.work_item_properties:read` |
+| Create, Update, Delete | `workspaces.work_item_properties:write` |
diff --git a/docs/api-reference/v2/work-item-property-contexts/update-property-context.md b/docs/api-reference/v2/work-item-property-contexts/update-property-context.md
new file mode 100644
index 00000000..86cd2673
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-contexts/update-property-context.md
@@ -0,0 +1,339 @@
+---
+title: Update a property context
+description: Update the scope or the overrides of a work item property context with the Plane v2 REST API. Partial PATCH semantics, set-replacement for project and type lists, option replacement, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update property context, patch context, applies_to_all_projects, project_ids, issue_type_ids, options replacement, PATCH contexts
+---
+
+# Update a property context
+
+
+
+Change what a context covers, or what it imposes on the property inside that coverage. Both halves are editable in one request: the scope fields move the boundary, and `is_required`, `is_multi`, `default_value`, `settings`, and `options` change the rules within it.
+
+`PATCH` is partial at the field level. Send only the fields you want to change; anything you omit keeps its current value. Fields that hold collections behave differently — see [Lists are replaced, not merged](#lists-are-replaced-not-merged).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property the context belongs to. A project-level property id is not addressable here and returns `404 resource_not_found`.
+
+
+
+
+
+The id of the context to update.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional — send the subset you are changing. The one constraint that survives a partial update is the scope: neither axis may be left without coverage, so turning a wildcard flag off has to arrive in the same request as the ids that replace it.
+
+
+
+
+
+New display name, unique among this property's contexts. Maximum 255 characters.
+
+
+
+
+
+Switch the project axis between "every project" and an explicit list.
+
+Setting it to `true` discards the context's stored `project_ids`, so the context reads back with an empty list. Setting it to `false` requires `project_ids` in the same request unless the context already has projects stored.
+
+Only one context per property may have this set to `true`, so turning it on while another context holds the slot returns `400`.
+
+
+
+
+
+Replace the exact set of projects this context covers. What you send becomes the whole list: ids you leave out are unlinked, and ids you add are linked.
+
+Rejected with a `400` when `applies_to_all_projects` is `true`, and when the list would be empty while the flag is `false`. Every id must belong to this workspace, and duplicates are rejected. Write-only as an input; the response echoes the stored links in `project_ids`.
+
+
+
+
+
+Switch the work item type axis between "every type" and an explicit list. Same rules as `applies_to_all_projects`: turning it on clears `issue_type_ids`, turning it off needs types to fall back on.
+
+
+
+
+
+Replace the exact set of work item types this context covers. Same set-replacement semantics as `project_ids`, and the same validation against the wildcard flag.
+
+
+
+
+
+Whether the property must be filled in inside this scope. Replaces the property's own `is_required` here.
+
+
+
+
+
+Whether the property accepts several values inside this scope. Replaces the property's own `is_multi` here.
+
+
+
+
+
+Values applied when a work item in this scope has nothing set. For `OPTION` properties, options in this context flagged `is_default` take precedence over this array.
+
+
+
+
+
+Replace this context's option list. Entries are the same shape as on create: `id` to keep an existing option, `name` to add a new one, plus optional `description` and `is_default`. An option's `sort_order` is read-only — Plane assigns it from the order of the entries you send.
+
+Sending this field rewrites the list. Any option currently on the context that you do not reference by `id` is removed, along with the values recorded through it. To keep an option and change nothing about it, include `{ "id": "…" }`.
+
+
+
+
+
+Type-specific configuration for this scope, as a JSON object. Replaces the property's own `settings` here.
+
+
+
+
+
+Ordering weight among this property's contexts. Lower values sort first. Presentational only — it does not affect which context wins for a work item.
+
+
+
+
+
+Your system's identifier for this context. Maximum 255 characters. Must be sent together with `external_source`, and the pair must be unique within the property.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters. Must be sent together with `external_id`.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | An axis left without coverage, a list sent alongside its wildcard flag, an id outside the workspace, a duplicate name, a half-filled external pair, or an overlap with a same-tier context. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't change workspace property settings. |
+| `404` | `resource_not_found` | No such context, property, or workspace — or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types per project, so workspace-level property writes are refused. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning A context can never cover nothing
+`{"applies_to_all_projects": false}` on its own is a `400` when the context has no stored projects, and so is `{"project_ids": []}`. An empty scope would strand every value recorded under this context, so Plane refuses the write instead. Send the flag and the replacement ids together.
+:::
+
+::: info Workspace mode only
+Updating a context requires the workspace to manage work item types at the workspace level. In project mode the request returns `409 work_item_types_managed_at_project`. Reads are unaffected. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+## Lists are replaced, not merged
+
+`project_ids`, `issue_type_ids`, and `options` are set-replacements. Sending one is a statement about the whole collection, not an addition to it:
+
+- **Omit the field** — the collection is left exactly as it was. This is the safe default, and it is why the request above changed the type axis without touching projects or options.
+- **Send the field** — what you send becomes the collection. Ids you left out are unlinked; for `options`, an option you left out is deleted along with the values recorded through it.
+
+To add one project to a context, read the current `project_ids`, append to it, and send the full list back. To keep an option unchanged while editing its siblings, include it as `{ "id": "…" }`.
+
+## Moving between a list and the wildcard
+
+The flag and the list are alternatives on each axis, and switching between them has a fixed shape:
+
+- **List to wildcard.** Send `{"applies_to_all_projects": true}` and nothing else on that axis. Sending `project_ids` in the same request is a `400`. The stored project links are dropped, so the context reads back with `"project_ids": []` — which now means every project.
+- **Wildcard to list.** Send `{"applies_to_all_projects": false, "project_ids": ["…"]}` in one request. The flag alone is a `400` when the context has no stored projects to fall back on.
+
+The work item type axis follows the same two rules with `applies_to_all_work_item_types` and `issue_type_ids`.
+
+::: warning Changing scope can delete recorded values
+Every scope edit re-resolves which context each work item uses, and values that no longer have a home are removed:
+
+- A work item whose project and type are no longer covered by **any** context on this property loses its value for that property.
+- An option-typed value keeps its cell but points at an option owned by a context that no longer resolves there — that value is dropped too, because the resolved context's option list is the only one Plane will offer.
+
+Narrowing a context, or widening one so it outranks another with a different option list, both trigger this. Read the property's full context list before changing a boundary, and treat scope edits as a migration rather than a cosmetic change.
+:::
+
+## Overrides apply wherever this context resolves
+
+`is_required`, `is_multi`, `default_value`, `settings`, and `options` take effect as soon as the request succeeds, on every work item that resolves to this context. They replace the property's own values there rather than combining with them, so editing the property itself will not change behavior in a scope that a context already covers.
+
+Which work items that is depends on the other contexts on the property — a more specific one still outranks this one. See [Property contexts overview](/api-reference/v2/work-item-property-contexts/overview) for the precedence rules, and [List property contexts](/api-reference/v2/work-item-property-contexts/list-property-contexts) to see them all at once.
diff --git a/docs/api-reference/v2/work-item-property-options/create-property-option.md b/docs/api-reference/v2/work-item-property-options/create-property-option.md
new file mode 100644
index 00000000..ab3ad54e
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-options/create-property-option.md
@@ -0,0 +1,212 @@
+---
+title: Create a property option
+description: Add a selectable option to an OPTION-typed work item property in a Plane project with the v2 REST API. Body parameters, the default option, project-mode 409s, OAuth scopes, and code examples.
+keywords: plane api v2, create property option, work item property option, OPTION property, is_default, POST property options
+---
+
+# Create a property option
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{property_id}/options/
+
+
+
+
+
+Add one more selectable choice to a project-level property whose `property_type` is `OPTION`. The new option becomes immediately pickable on work items that carry the property.
+
+Use this when the property already exists and you are extending its list. If you are still creating the property, you can pass the full set of choices inline through the property's write-only `options` field instead — see [Work item properties](/api-reference/v2/work-item-properties/overview) — and come back here only for later changes.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project that owns the property.
+
+
+
+
+
+The property to add the option to. It must be an `OPTION` property in this project — a property id from another project returns `404`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+The label shown in the picker, for example `Blocker`. Maximum 255 characters.
+
+
+
+
+
+Free-form explanation of what the option means. Worth filling in when the label alone doesn't tell someone when to choose it.
+
+
+
+
+
+Make this the option that is preselected when a work item is created without an explicit value for this property. Defaults to `false`.
+
+
+
+
+
+Your system's identifier for this option, for sync and import correlation. Maximum 255 characters. Nullable.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters. Nullable. Send it alongside `external_id` — an `external_id` is only unique within its source.
+
+
+
+
+
+::: info sort_order is not writable
+`sort_order` comes back on every read, but it is not a create field. Plane assigns the new option a position when it is created; there is no body parameter that places it.
+:::
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------ |
+| `400` | `validation_error` | `name` missing, or a field over its 255-character limit. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write this project's properties. |
+| `404` | `resource_not_found` | No such property, project, or workspace — or the property belongs to a different project. |
+| `409` | `work_item_types_managed_at_workspace` | The workspace manages work item types at the workspace level, so this project-level write is rejected. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning Wrong mode is a 409, not a 404
+A workspace manages work item types in exactly one mode. If yours is in **workspace mode**, this project-level `POST` returns `409 work_item_types_managed_at_workspace` — the option isn't missing and you aren't unauthorized, the write simply belongs on the workspace surface. Create it through [Property options (workspace)](/api-reference/v2/workspace-work-item-property-options/overview) instead, and see [Work item type modes](/api-reference/v2/work-item-type-modes) for how to detect the mode first.
+:::
+
+
diff --git a/docs/api-reference/v2/work-item-property-options/delete-property-option.md b/docs/api-reference/v2/work-item-property-options/delete-property-option.md
new file mode 100644
index 00000000..459c0b3a
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-options/delete-property-option.md
@@ -0,0 +1,156 @@
+---
+title: Delete a property option
+description: Delete an option from an OPTION-typed work item property in a Plane project with the v2 REST API. Path parameters, the 204 response, project-mode 409s, OAuth scopes, and code examples.
+keywords: plane api v2, delete property option, remove work item property option, OPTION property, 204 no content, DELETE property options
+---
+
+# Delete a property option
+
+
+
+Remove a choice from a project-level `OPTION` property. Use this when an option is no longer offered — a retired severity level, a team that no longer exists.
+
+A successful delete returns `204` with an empty body. There is nothing to parse; branch on the status code.
+
+If you only want to stop offering a choice without removing it from history, consider renaming it with [Update a property option](/api-reference/v2/work-item-property-options/update-property-option) instead — a rename leaves every stored value intact because values reference the option `id`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project that owns the property.
+
+
+
+
+
+The `OPTION` property the option belongs to. See [Work item properties](/api-reference/v2/work-item-properties/overview).
+
+
+
+
+
+The id of the option to delete. Every ancestor in the path is enforced — an option id that belongs to a different property returns `404` rather than deleting anything.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------ |
+| `400` | `validation_error` | The request couldn't be processed as sent — for example a malformed identifier in the path. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write this project's properties. |
+| `404` | `resource_not_found` | No such option, property, project, or workspace — or the option belongs to a different property. |
+| `409` | `work_item_types_managed_at_workspace` | The workspace manages work item types at the workspace level, so this project-level write is rejected. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning Wrong mode is a 409, not a 404
+If the workspace manages work item types at the **workspace** level, this project-level `DELETE` returns `409 work_item_types_managed_at_workspace`. Nothing was deleted, and the option still exists — the write belongs on the workspace surface. Delete it through [Property options (workspace)](/api-reference/v2/workspace-work-item-property-options/overview), and see [Work item type modes](/api-reference/v2/work-item-type-modes) for how to detect the mode before you write.
+:::
+
+::: info 404 after a successful delete
+A repeat `DELETE` of the same option returns `404 resource_not_found`, not another `204`. If you are reconciling state, treat `404` on delete as "already gone" rather than as a failure.
+:::
+
+
diff --git a/docs/api-reference/v2/work-item-property-options/get-property-option.md b/docs/api-reference/v2/work-item-property-options/get-property-option.md
new file mode 100644
index 00000000..5622f979
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-options/get-property-option.md
@@ -0,0 +1,158 @@
+---
+title: Get a property option
+description: Retrieve a single option of an OPTION-typed work item property in a Plane project with the v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get property option, retrieve work item property option, OPTION property, is_default, GET property option by id
+---
+
+# Get a property option
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{property_id}/options/{pk}/
+
+
+
+
+
+Retrieve one option by id. Reach for this when you already hold an option id — from a work item's property value, a webhook payload, or a stored mapping — and need its current label, description, or default flag.
+
+Reads work in either work item type mode, so this endpoint keeps returning data even in a workspace where option **writes** live on the workspace surface. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project that owns the property.
+
+
+
+
+
+The `OPTION` property the option belongs to. See [Work item properties](/api-reference/v2/work-item-properties/overview).
+
+
+
+
+
+The id of the option to retrieve. Every ancestor in the path is enforced — an option id that belongs to a different property returns `404` rather than the option's data.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------------------------------------ |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's properties. |
+| `404` | `resource_not_found` | No such option, property, project, or workspace — or the option belongs to a different property. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Existence is never leaked
+An option outside your tenant returns `404`, not `403`. If a cached id starts returning `404`, re-read the property's choices with [List property options](/api-reference/v2/work-item-property-options/list-property-options) instead of retrying.
+:::
+
+::: info No expansion on options
+Property options don't accept `?expand=`. The response is the flat option object shown here.
+:::
+
+
diff --git a/docs/api-reference/v2/work-item-property-options/list-property-options.md b/docs/api-reference/v2/work-item-property-options/list-property-options.md
new file mode 100644
index 00000000..676b9cc1
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-options/list-property-options.md
@@ -0,0 +1,258 @@
+---
+title: List property options
+description: List the selectable options of an OPTION-typed work item property in a Plane project with the v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list property options, work item property options, OPTION property, sort_order, pagination, GET property options
+---
+
+# List property options
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{property_id}/options/
+
+
+
+
+
+Return the selectable choices of a single project-level property as a paginated list. This is how you build a picker for an `OPTION` property, and how you resolve a human-readable label such as `Critical` to the option `id` you must send when setting a property value on a work item.
+
+The property in the path must have `property_type: "OPTION"` — see [Work item properties](/api-reference/v2/work-item-properties/overview). Properties of any other type have no options.
+
+Reads work in either work item type mode — only writes are mode-gated, so listing options never returns `409`. See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project that owns the property.
+
+
+
+
+
+The property whose options you want to list. A property id from another project returns `404`, even inside the same workspace.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+There are no filters or search on this endpoint — a property's option list is short and is returned whole. Check your spelling on `order_by`: a value outside the list below is not rejected, it silently falls back to the default ordering, so a typo shows up as an unexpected sort order rather than an error.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` , `-sort_order` — the order the property itself presents its choices in
+- `created_at` , `-created_at` — when each option was added
+- `id` , `-id`
+
+Order by `sort_order` when you are rendering the picker to a user; it matches what Plane's own UI shows. `created_at` is orderable but is not returned in the response body.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most properties have well under 50 options, so one page is usually the entire list.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. Omit it for the default offset envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ----------------------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's properties. |
+| `404` | `resource_not_found` | No such property, project, or workspace — or the property belongs to a different project. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info No expansion on options
+Property options don't accept `?expand=`. The response is the flat option object shown here.
+:::
+
+
+
+::: tip Cache the id, refresh the label
+The option `id` is what a work item stores. Cache the id-to-name mapping from this endpoint and refresh it when you render, so a rename in Plane shows up in your UI without breaking the values you already wrote.
+:::
diff --git a/docs/api-reference/v2/work-item-property-options/overview.md b/docs/api-reference/v2/work-item-property-options/overview.md
new file mode 100644
index 00000000..7c800c55
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-options/overview.md
@@ -0,0 +1,120 @@
+---
+title: Property options overview
+description: The Plane API v2 work item property option object. Attributes, the default option, sort order, how options relate to OPTION-typed properties, and project-mode write rules.
+keywords: plane api v2, work item property options, option property, is_default option, sort_order, project properties, custom property choices
+---
+
+# Property options overview
+
+A property option is one selectable choice on a project-level work item property. Options exist **only** for properties whose `property_type` is `OPTION` — a property of any other type (`TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `RELATION`, `URL`, `EMAIL`, `FILE`, or `FORMULA`) has no options to manage. Start at
+[Work item properties](/api-reference/v2/work-item-properties/overview) to create the property itself, then use this sub-resource to manage the choices it offers.
+
+Each option belongs to exactly one property, which belongs to exactly one project. The path carries all three ancestors, and every id in it is checked: an option id that belongs to a different property — or a property that belongs to a different project — returns `404`, never another property's data.
+
+::: info Two ways to create options
+When you create an `OPTION` property you can pass its choices inline through the property's write-only `options` field, which sets up the property and its full list of choices in a single request. This sub-resource is how you manage those choices **afterwards** — adding a new one, renaming it, moving the default, or removing a choice that is no longer offered.
+:::
+
+
+
+
+## The property option object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the option. This is the value stored on a work item when someone picks this choice, so treat it as the stable handle and `name` as the label.
+
+- `name` _string_
+
+ The label shown in the picker, for example `Critical`. Maximum 255 characters.
+
+- `description` _string_
+
+ Free-form explanation of what the option means. Useful when a choice needs a definition that a one-word label can't carry.
+
+- `is_default` _boolean_
+
+ Whether this option is preselected when a work item is created without an explicit value for the property.
+
+- `sort_order` _number_
+
+ Ordering weight within the property. Lower values sort first. Plane assigns it when the option is created — it is not something you send.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map an option to a choice in another system and find it again later. Both are nullable.
+
+::: tip Store the id, not the name
+Work item property values reference an option by `id`. If you match on `name` instead, renaming `Critical` to `Sev 1` silently breaks your integration, while the `id` keeps resolving.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | -------------------------------------------------------------------------------------------------- | ---------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{property_id}/options/` | List options |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{property_id}/options/` | Create an option |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{property_id}/options/{pk}/` | Get an option |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{property_id}/options/{pk}/` | Update an option |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-properties/{property_id}/options/{pk}/` | Delete an option |
+
+`DELETE` returns `204` with an empty body.
+
+## What you can write
+
+The read shape and the write shape are deliberately different. Five fields are writable, and `name` is the only one that is required:
+
+| Field | Create | Update |
+| ----------------- | ----------------- | -------- |
+| `name` | required, max 255 | optional |
+| `description` | optional | optional |
+| `is_default` | optional | optional |
+| `external_id` | optional, max 255 | optional |
+| `external_source` | optional, max 255 | optional |
+
+`id` and `sort_order` are read-only. `sort_order` is not accepted on create or update — Plane assigns and maintains it, so there is no request that repositions an option through this endpoint.
+
+## Project mode only
+
+Writes here are **project-mode** writes. A workspace manages work item types and their properties in exactly one mode: project-level or workspace-level.
+
+::: warning Wrong mode returns 409, not 404
+If the workspace is running in **workspace mode**, `POST`, `PATCH`, and `DELETE` on this path return `409` with the code `work_item_types_managed_at_workspace`. Nothing is missing and nothing is forbidden — the capability lives on the workspace surface instead. Manage those options through [Property options (workspace)](/api-reference/v2/workspace-work-item-property-options/overview).
+
+Reads are unaffected by mode. `GET` on this path keeps working in either mode, so a project still lists the options of the properties it surfaces.
+
+See [Work item type modes](/api-reference/v2/work-item-type-modes) for how to detect which mode a workspace is in before you write.
+:::
+
+## Scopes
+
+| Operation | Scope |
+| ------------------------- | ------------------------------------- |
+| `GET` (list, retrieve) | `projects.work_item_properties:read` |
+| `POST`, `PATCH`, `DELETE` | `projects.work_item_properties:write` |
+
+Options are covered by the property scopes — there is no separate option scope to request.
diff --git a/docs/api-reference/v2/work-item-property-options/update-property-option.md b/docs/api-reference/v2/work-item-property-options/update-property-option.md
new file mode 100644
index 00000000..628def65
--- /dev/null
+++ b/docs/api-reference/v2/work-item-property-options/update-property-option.md
@@ -0,0 +1,223 @@
+---
+title: Update a property option
+description: Update an option of an OPTION-typed work item property in a Plane project with the v2 REST API. Partial PATCH semantics, body parameters, the default option, project-mode 409s, and code examples.
+keywords: plane api v2, update property option, patch property option, work item property option, is_default, PATCH property options
+---
+
+# Update a property option
+
+
+
+Change an option's label, description, default flag, or correlation fields. Work items that already hold this option keep holding it — they store the option's `id`, so a rename is purely cosmetic and no stored value changes.
+
+`PATCH` is partial. Send only the fields you want to change; anything you omit keeps its current value. Omitting a field is not the same as sending `null`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project that owns the property.
+
+
+
+
+
+The `OPTION` property the option belongs to. See [Work item properties](/api-reference/v2/work-item-properties/overview).
+
+
+
+
+
+The id of the option to update. An option id that belongs to a different property returns `404`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional — send the subset you are changing.
+
+
+
+
+
+New label for the option. Maximum 255 characters. Safe to change at any time: values on work items reference the option `id`, not its name.
+
+
+
+
+
+Free-form explanation of what the option means.
+
+
+
+
+
+Whether this option is preselected when a work item is created without an explicit value for the property. Changing it affects only work items created from now on — existing values are untouched.
+
+
+
+
+
+Your system's identifier for this option, for sync and import correlation. Maximum 255 characters. Nullable.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters. Nullable.
+
+
+
+
+
+::: info sort_order is not writable
+`sort_order` is returned on every read but is not accepted in the request body. There is no `PATCH` that repositions an option — Plane maintains the ordering weight itself.
+:::
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------ |
+| `400` | `validation_error` | A field over its 255-character limit, or a value of the wrong type. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write this project's properties. |
+| `404` | `resource_not_found` | No such option, property, project, or workspace — or the option belongs to a different property. |
+| `409` | `work_item_types_managed_at_workspace` | The workspace manages work item types at the workspace level, so this project-level write is rejected. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning Wrong mode is a 409, not a 404
+If the workspace manages work item types at the **workspace** level, this project-level `PATCH` returns `409 work_item_types_managed_at_workspace`. The `GET` on the same path still works — reads are unaffected by mode, only writes are. Update the option through [Property options (workspace)](/api-reference/v2/workspace-work-item-property-options/overview), and see [Work item type modes](/api-reference/v2/work-item-type-modes) for how to detect which mode you are in.
+:::
+
+
diff --git a/docs/api-reference/v2/work-item-type-modes.md b/docs/api-reference/v2/work-item-type-modes.md
new file mode 100644
index 00000000..eb683e79
--- /dev/null
+++ b/docs/api-reference/v2/work-item-type-modes.md
@@ -0,0 +1,430 @@
+---
+title: Work item type modes
+description: A Plane workspace manages work item types at either the project level or the workspace level. How to detect the mode, which endpoints each mode uses, and why the wrong surface returns 409.
+keywords: plane api v2, work item types, project mode, workspace mode, work_item_types_managed_at_workspace, work_item_types_managed_at_project, 409 conflict, custom properties
+---
+
+# Work item type modes
+
+A workspace manages work item types in **exactly one mode**, and that choice decides which endpoints you write to.
+
+| Mode | Types are owned by | You write to |
+| ------------------ | ------------------ | ------------------------------------------------------------------------------- |
+| **Project mode** | Each project | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/` and siblings |
+| **Workspace mode** | The workspace | `/api/v2/workspaces/{slug}/work-item-types/` and siblings |
+
+Both surfaces exist in the API at all times. Only one of them accepts writes for a given workspace. This is the single most confusing thing about v2, and it is worth ten minutes now rather than an afternoon of debugging later.
+
+## The rule that catches everyone
+
+Writing to the wrong mode's endpoint returns **`409`** — not `404`, not `403`.
+
+That status is deliberate. `404` would mean "no such thing" and `403` would mean "you may not do that", and neither is true. The capability exists and you are allowed to use it; it just lives on the other surface. `409` says the request conflicts with the workspace's current configuration, and the stable error `code` tells you which way to go.
+
+| Situation | Status | `code` |
+| ------------------------------------------------------------------- | ------ | -------------------------------------- |
+| You wrote to a **project** endpoint, workspace is in workspace mode | `409` | `work_item_types_managed_at_workspace` |
+| You wrote to a **workspace** endpoint, workspace is in project mode | `409` | `work_item_types_managed_at_project` |
+
+
+
+```json
+{
+ "type": "https://api.plane.so/errors/work_item_types_managed_at_workspace",
+ "title": "Work Item Types Managed At Workspace",
+ "status": 409,
+ "code": "work_item_types_managed_at_workspace",
+ "detail": "Work item types are managed at the workspace level for this workspace."
+}
+```
+
+
+
+Read the code as an instruction, not just a complaint: `work_item_types_managed_at_workspace` means _"retry this against the workspace surface"_, and `work_item_types_managed_at_project` means _"retry this against the project surface"_.
+
+::: tip Reads are not affected by mode
+Only writes are gated. A `GET` works on both surfaces in both modes — and importantly, a project still surfaces its imported types while the workspace is in workspace mode. If you only ever read types, you can ignore this page entirely and list them from the project surface.
+:::
+
+## Discovering the current mode
+
+Ask the workspace features endpoint. The `is_work_item_types_enabled` field is the mode discriminator.
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/features/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "id": "a72f1c40-5b8e-4d19-9f2a-3c6d8e1b7a55",
+ "is_work_item_types_enabled": true,
+ "is_workitem_hierarchy_enabled": false,
+ "is_project_grouping_enabled": false,
+ "is_teams_enabled": true,
+ "is_wiki_enabled": true,
+ "is_initiative_enabled": false,
+ "is_customer_enabled": false,
+ "is_release_enabled": false,
+ "is_state_duration_enabled": false,
+ "is_pi_enabled": false,
+ "work_item_type_default_level": 0,
+ "created_at": "2026-01-14T09:22:41.478363Z"
+}
+```
+
+
+
+- `is_work_item_types_enabled: true` → **workspace mode**. Types are owned by the workspace.
+- `is_work_item_types_enabled: false` → **project mode**. Types are owned by each project.
+
+The toggle only puts the workspace into workspace mode when workspace-level work item types are also available on the workspace's plan. Where they are not, the workspace stays in project mode no matter how the toggle is set — so treat a successful write, not the toggle alone, as proof of which surface is live.
+
+Switching the mode is a `PATCH` to the same endpoint (`workspaces.features:write` scope required). Treat it as an administrative action, not something an integration flips at runtime — it changes which surface every other client in the workspace must write to.
+
+```bash
+curl -X PATCH "https://api.plane.so/api/v2/workspaces/my-team/features/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"is_work_item_types_enabled": true}'
+```
+
+See [Workspace features](/api-reference/v2/workspace-features/overview) for every field on this object.
+
+## Which surface do I use?
+
+```mermaid
+flowchart TD
+ A["GET /api/v2/workspaces/my-team/features/"] --> B{"is_work_item_types_enabled"}
+ B -->|"false"| C["Project mode"]
+ B -->|"true"| D["Workspace mode"]
+
+ C --> C1["Enable types on the project POST .../projects/PROJECT_ID/work-item-types/enable/"]
+ C1 --> C2["Create types and properties under .../projects/PROJECT_ID/..."]
+
+ D --> D1["Create types and properties under .../workspaces/my-team/..."]
+ D1 --> D2["Import the types into each project POST .../projects/PROJECT_ID/work-item-types/import/"]
+
+ C2 --> E["Read the type schema GET .../projects/PROJECT_ID/work-item-types/TYPE_ID/schema/"]
+ D2 --> E
+```
+
+Note where the two paths converge: **reading a type's schema always happens on the project surface**, in both modes, because a schema describes what you can write on a work item _in a project_.
+
+## Project mode lifecycle
+
+In project mode each project owns its own types. Two projects can have entirely unrelated type sets.
+
+### 1. Enable types on the project
+
+A project starts without work item types. Enabling creates the project's default `Task` type and returns it.
+
+```bash
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-types/enable/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "id": "2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4",
+ "name": "Task",
+ "description": "Default work item type with the option to add new properties",
+ "is_active": true,
+ "is_default": true,
+ "is_epic": false,
+ "level": 0,
+ "logo_props": { "in_use": "icon", "icon": { "name": "Briefcase", "background_color": "#6d7b8a" } },
+ "created_at": "2026-01-14T09:22:41.478363Z"
+}
+```
+
+
+
+Send this once per project. Calling it again returns the existing default type rather than creating a second one.
+
+### 2. Create a type
+
+```bash
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-types/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Bug",
+ "description": "Something is broken in production",
+ "is_active": true
+}'
+```
+
+### 3. Create a property
+
+Properties are created against the project, independent of any type. A property needs a `display_name` and a `property_type`.
+
+```bash
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-properties/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "display_name": "Severity",
+ "property_type": "OPTION",
+ "is_required": true,
+ "is_multi": false
+}'
+```
+
+For an `OPTION` property, add its choices under the property:
+
+```bash
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-properties/9e51a08b-3d47-4f62-bc19-2a7d5e0f8c31/options/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "name": "Sev 1", "is_default": false }'
+```
+
+### 4. Attach the property to a type
+
+Creating a property does not put it on anything. Attach it to the types that should carry it — one property can serve several types.
+
+```bash
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-types/2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4/properties/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "properties": ["9e51a08b-3d47-4f62-bc19-2a7d5e0f8c31"] }'
+```
+
+Detaching is a `DELETE` on the same collection with the property id in the path. Detaching removes the property from the type; it does not delete the property.
+
+### 5. Read the type's schema
+
+The schema endpoint is the one call that tells you everything writable on a work item of this type — standard fields with their option lists, plus the type's custom properties.
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-types/2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4/schema/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+
+
+```json
+{
+ "type_id": "2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4",
+ "type_name": "Bug",
+ "type_description": "Something is broken in production",
+ "type_logo_props": { "in_use": "icon", "icon": { "name": "Bug", "background_color": "#c2410c" } },
+ "fields": {
+ "name": { "type": "string", "required": true, "max_length": 255 },
+ "priority": {
+ "type": "option",
+ "required": false,
+ "default": "none",
+ "options": [
+ { "value": "urgent", "label": "Urgent" },
+ { "value": "high", "label": "High" },
+ { "value": "medium", "label": "Medium" },
+ { "value": "low", "label": "Low" },
+ { "value": "none", "label": "None" }
+ ]
+ },
+ "state_id": {
+ "type": "uuid",
+ "required": false,
+ "options": [
+ { "id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f", "name": "In Progress", "color": "#3f76ff", "group": "started" }
+ ]
+ },
+ "assignee_ids": { "type": "uuid", "is_multi": true, "required": false },
+ "start_date": { "type": "date", "required": false, "format": "YYYY-MM-DD" }
+ },
+ "custom_fields": {
+ "severity": {
+ "id": "9e51a08b-3d47-4f62-bc19-2a7d5e0f8c31",
+ "type": "OPTION",
+ "name": "severity",
+ "display_name": "Severity",
+ "description": "",
+ "required": true,
+ "is_multi": false,
+ "options": [{ "id": "5c3e7d81-91b4-4a2e-8f60-1d4b9c6a2e77", "name": "Sev 1", "logo_props": null }]
+ }
+ }
+}
+```
+
+
+
+Add `?include=members,labels` to inline the assignee and label option lists. They can be large, so they are left out unless you ask.
+
+## Workspace mode lifecycle
+
+In workspace mode one set of types is defined once and shared. Projects opt into the types they want by importing them.
+
+### 1. Create a type on the workspace
+
+```bash
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/work-item-types/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Bug",
+ "description": "Something is broken in production",
+ "is_active": true
+}'
+```
+
+
+
+```json
+{
+ "id": "2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4",
+ "name": "Bug",
+ "description": "Something is broken in production",
+ "is_active": true,
+ "is_default": false,
+ "is_epic": false,
+ "level": 0,
+ "logo_props": { "in_use": "icon", "icon": { "name": "Bug", "background_color": "#c2410c" } },
+ "created_at": "2026-01-14T09:22:41.478363Z"
+}
+```
+
+
+
+### 2. Create workspace properties and attach them
+
+Same two steps as project mode, one path segment shorter — there is no `project_id`.
+
+```bash
+# create the property
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/work-item-properties/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "display_name": "Severity", "property_type": "OPTION", "is_required": true }'
+
+# add its options
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/work-item-properties/9e51a08b-3d47-4f62-bc19-2a7d5e0f8c31/options/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "name": "Sev 1" }'
+
+# attach it to a type
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/work-item-types/2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4/properties/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "properties": ["9e51a08b-3d47-4f62-bc19-2a7d5e0f8c31"] }'
+```
+
+### 3. Narrow a property with contexts (workspace mode only)
+
+A workspace property applies everywhere by default. A **property context** narrows where it applies and can override `is_required` / `is_multi` / `default_value` for that slice — for example, "Severity is required, but only on the Bug type in these three projects."
+
+```bash
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/work-item-properties/9e51a08b-3d47-4f62-bc19-2a7d5e0f8c31/contexts/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "applies_to_all_projects": false,
+ "applies_to_all_work_item_types": false,
+ "project_ids": ["4af68566-94a4-4eb3-94aa-50dc9427067b"],
+ "issue_type_ids": ["2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4"],
+ "is_required": true
+}'
+```
+
+Contexts exist only on the workspace surface — there is no project-mode equivalent, because a project-owned property is already scoped to its project. See [Property contexts](/api-reference/v2/work-item-property-contexts/overview).
+
+### 4. Import workspace types into a project
+
+A workspace type is not usable in a project until the project imports it. This is what makes a shared type show up on that project's work items.
+
+::: warning The import endpoint lives on the project path but requires workspace mode
+`POST .../projects/{project_id}/work-item-types/import/` sits under a project URL, yet it is a **workspace-mode** operation — importing only makes sense when the workspace owns the types. Calling it in project mode returns `409 work_item_types_managed_at_project`. It is the one endpoint where the path segment and the required mode do not line up, so it surprises people.
+:::
+
+```bash
+curl -X POST "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-types/import/" \
+ -H "X-Api-Key: $PLANE_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "work_item_types": ["2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4"] }'
+```
+
+Importing is idempotent — re-importing a type the project already has changes nothing.
+
+### 5. Read the schema from the project
+
+Once imported, the type behaves like any other type in that project. Read its schema from the **project** surface, exactly as in project mode:
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-types/2d9b6f31-4a70-4c88-9d15-8b0e3f27c6a4/schema/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+This is the practical payoff of "reads are unaffected by mode": the code that reads a type and writes a work item against it is identical in both modes. Only the code that _defines_ types has to care.
+
+## Endpoints by mode
+
+| Operation | Project mode | Workspace mode |
+| ------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
+| Enable types | `POST .../projects/{project_id}/work-item-types/enable/` | Not applicable — `PATCH .../features/` |
+| List / create types | `.../projects/{project_id}/work-item-types/` | `.../workspaces/{slug}/work-item-types/` |
+| Get / update / delete a type | `.../projects/{project_id}/work-item-types/{pk}/` | `.../workspaces/{slug}/work-item-types/{pk}/` |
+| Mark a type as default | `.../projects/{project_id}/work-item-types/{pk}/mark-default/` | `.../workspaces/{slug}/work-item-types/{pk}/mark-default/` |
+| List / create properties | `.../projects/{project_id}/work-item-properties/` | `.../workspaces/{slug}/work-item-properties/` |
+| Property options | `.../projects/{project_id}/work-item-properties/{property_id}/options/` | `.../workspaces/{slug}/work-item-properties/{property_id}/options/` |
+| Attach / detach a property to a type | `.../projects/{project_id}/work-item-types/{type_id}/properties/` | `.../workspaces/{slug}/work-item-types/{type_id}/properties/` |
+| Property contexts | Not available | `.../workspaces/{slug}/work-item-properties/{property_id}/contexts/` |
+| Import workspace types into a project | Not available | `POST .../projects/{project_id}/work-item-types/import/` |
+| Read a type's schema | `GET .../projects/{project_id}/work-item-types/{pk}/schema/` | Same — read from the project surface |
+
+## Writing mode-agnostic code
+
+If your integration has to work against workspaces you do not control, resolve the mode once at startup and pick a base path from it:
+
+```python
+import requests
+
+BASE = "https://api.plane.so/api/v2"
+HEADERS = {"X-Api-Key": "your-api-key"}
+
+
+def type_base_path(slug, project_id):
+ """Return the URL prefix that accepts work-item-type WRITES for this workspace."""
+ features = requests.get(f"{BASE}/workspaces/{slug}/features/", headers=HEADERS)
+ features.raise_for_status()
+
+ if features.json()["is_work_item_types_enabled"]:
+ return f"{BASE}/workspaces/{slug}" # workspace mode
+ return f"{BASE}/workspaces/{slug}/projects/{project_id}" # project mode
+
+
+base = type_base_path("my-team", "4af68566-94a4-4eb3-94aa-50dc9427067b")
+response = requests.post(
+ f"{base}/work-item-types/",
+ headers=HEADERS,
+ json={"name": "Bug", "description": "Something is broken in production"},
+)
+```
+
+Also handle the `409` at the call site. The mode can change between your startup check and your write, and the error code tells you exactly which surface to retry on:
+
+```python
+if response.status_code == 409:
+ code = response.json()["code"]
+ if code == "work_item_types_managed_at_workspace":
+ ... # retry against /workspaces/{slug}/...
+ elif code == "work_item_types_managed_at_project":
+ ... # retry against /workspaces/{slug}/projects/{project_id}/...
+```
+
+Branch on `code`, never on the HTTP status alone — a plain `409 conflict` from this family of endpoints means something entirely different, such as a duplicate name or a delete blocked because the type still has work items.
+
+## Related
+
+- [Work item types (project)](/api-reference/v2/work-item-types/overview)
+- [Work item types (workspace)](/api-reference/v2/workspace-work-item-types/overview)
+- [Work item properties (project)](/api-reference/v2/work-item-properties/overview) and [(workspace)](/api-reference/v2/workspace-work-item-properties/overview)
+- [Property contexts](/api-reference/v2/work-item-property-contexts/overview)
+- [Workspace features](/api-reference/v2/workspace-features/overview)
+- [Errors](/api-reference/v2/errors)
diff --git a/docs/api-reference/v2/work-item-type-properties/attach-type-property.md b/docs/api-reference/v2/work-item-type-properties/attach-type-property.md
new file mode 100644
index 00000000..4b187b88
--- /dev/null
+++ b/docs/api-reference/v2/work-item-type-properties/attach-type-property.md
@@ -0,0 +1,222 @@
+---
+title: Attach a property to a type
+description: Attach existing custom properties to a Plane work item type with the v2 REST API. Body parameters, OAuth scopes, mode conflicts, error codes, and code examples.
+keywords: plane api v2, attach property to type, work item type properties, custom properties, POST type properties
+---
+
+# Attach a property to a type
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/properties/
+
+
+
+
+
+Expose one or more existing custom properties on a work item type. After this call, work items of that type collect
+those properties.
+
+::: warning This does not create a property
+The body is a list of **ids of properties that already exist** in the project. Sending a definition — `display_name`,
+`property_type`, and friends — is a `400`. Create the property first with
+[Create a work item property](/api-reference/v2/work-item-properties/create-work-item-property), then attach the id it
+returns.
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item type belongs to.
+
+
+
+
+
+The work item type to attach the properties to.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+The ids of the properties to attach. Every id must belong to a property in **this project** — an id from another
+project or another workspace fails the whole request with `400 validation_error`, and nothing is attached. Nothing is
+attached partially: either all the ids are valid or none are applied.
+
+The array cannot be empty. Attaching an id the type already exposes is not an error and does not create a duplicate, so
+retrying a request that may have already landed is safe.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ------------------------------------------------------------------------------ |
+| `400` | `validation_error` | `properties` missing or empty, or an id that isn't a property in this project. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't edit this project's work item types. |
+| `404` | `resource_not_found` | No such workspace, project, or type — or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_workspace` | This workspace manages work item types at the workspace level. See below. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning Wrong mode is a 409, not a 404
+Attaching is a project-mode write. If the workspace manages work item types at the **workspace** level, this endpoint
+returns `409 work_item_types_managed_at_workspace` — the capability exists, it just lives on the other surface. Attach
+there instead with
+[Attach a property to a workspace type](/api-reference/v2/workspace-work-item-type-properties/attach-workspace-type-property).
+
+Branch on the `code`, not the status: a `409` here means "wrong surface", never "already attached". See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+::: info The response is ids, not objects
+`201` returns a `properties` array — the ids that are now attached to the type. It does not echo the property
+definitions. Read them back with
+[List type properties](/api-reference/v2/work-item-type-properties/list-type-properties) or
+[Get a type property](/api-reference/v2/work-item-type-properties/get-type-property) when you need `display_name`,
+`property_type`, or the option list.
+:::
diff --git a/docs/api-reference/v2/work-item-type-properties/detach-type-property.md b/docs/api-reference/v2/work-item-type-properties/detach-type-property.md
new file mode 100644
index 00000000..987d00fd
--- /dev/null
+++ b/docs/api-reference/v2/work-item-type-properties/detach-type-property.md
@@ -0,0 +1,185 @@
+---
+title: Detach a property from a type
+description: Detach a custom property from a Plane work item type with the v2 REST API. Path parameters, OAuth scopes, mode conflicts, error codes, and code examples.
+keywords: plane api v2, detach property from type, remove property from work item type, work item type properties, DELETE type property
+---
+
+# Detach a property from a type
+
+
+
+Stop a work item type from exposing a custom property. This removes the link between the type and the property and
+returns `204` with an empty body.
+
+::: tip Detaching is not deleting
+The property definition survives. It stays in the project, stays attached to every other type that uses it, and can be
+attached again later with
+[Attach a property to a type](/api-reference/v2/work-item-type-properties/attach-type-property). To remove the
+definition itself, use
+[Delete a work item property](/api-reference/v2/work-item-properties/delete-work-item-property).
+:::
+
+::: warning Values recorded on this type's work items are removed
+The link is not the only thing that goes. Values already stored for this property on work items **of this type** are
+removed along with it, and re-attaching does not restore them. Export what you need before detaching a property that
+has been in use. Work items of other types keep their values for this property.
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item type belongs to.
+
+
+
+
+
+The work item type to detach the property from.
+
+
+
+
+
+The id of the property to detach — the property's own id, not a separate link id. It is the same value you passed in
+the `properties` array when you attached it.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | --------------------------------------------------------------------------------- |
+| `400` | `validation_error` | Malformed request — for example a `pk` that isn't a valid UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't edit this project's work item types. |
+| `404` | `resource_not_found` | The property isn't attached to this type, or no such type, project, or workspace. |
+| `409` | `work_item_types_managed_at_workspace` | This workspace manages work item types at the workspace level. See below. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning Wrong mode is a 409, not a 404
+Detaching is a project-mode write. If the workspace manages work item types at the **workspace** level, this endpoint
+returns `409 work_item_types_managed_at_workspace` and nothing is changed. Detach on the workspace surface instead with
+[Detach a property from a workspace type](/api-reference/v2/workspace-work-item-type-properties/detach-workspace-type-property).
+
+Branch on the `code`: a `409` means "wrong surface", a `404` means "that property isn't on this type". See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+::: info Detach is not idempotent the way attach is
+Attaching a property twice is harmless, but detaching one that is already gone returns `404` rather than `204`. If you
+are replaying a request, treat a `404` as "already detached" instead of retrying.
+:::
+
+
diff --git a/docs/api-reference/v2/work-item-type-properties/get-type-property.md b/docs/api-reference/v2/work-item-type-properties/get-type-property.md
new file mode 100644
index 00000000..c5851a7d
--- /dev/null
+++ b/docs/api-reference/v2/work-item-type-properties/get-type-property.md
@@ -0,0 +1,196 @@
+---
+title: Get a type property
+description: Retrieve one custom property through a Plane work item type with the v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get type property, work item type properties, custom property, attached property, GET type property by id
+---
+
+# Get a type property
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/properties/{pk}/
+
+
+
+
+
+Retrieve a single property **as exposed by a work item type**. The response is the full property definition — the same
+object [List type properties](/api-reference/v2/work-item-type-properties/list-type-properties) returns, for one id.
+
+The type in the path is part of the lookup, not decoration: a property that exists in the project but is not attached
+to this type returns `404`. That makes this endpoint a cheap "is this property on this type?" check before you send a
+value or offer the field in a form.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item type belongs to.
+
+
+
+
+
+The work item type the property is attached to.
+
+
+
+
+
+The id of the property to retrieve. This is the property's own id — the same value you pass to
+[attach](/api-reference/v2/work-item-type-properties/attach-type-property) and
+[detach](/api-reference/v2/work-item-type-properties/detach-type-property) it.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | -------------------------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's work item types. |
+| `404` | `resource_not_found` | No such workspace, project, type, or property — or the property isn't attached to this type. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info A 404 here can mean "not attached"
+The property may exist and be perfectly healthy — just not linked to this type. Read it without the type in the path
+using [Get a work item property](/api-reference/v2/work-item-properties/get-work-item-property) to tell the two cases
+apart, or list the type's properties to see what it actually exposes.
+:::
+
+::: info One flat object, and no mode gate
+The response is the flat property object shown here, with `options` already inlined for `OPTION` properties — there is
+nothing to fetch separately. Reads also work in either work item type mode; only attaching and detaching are
+mode-gated. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
diff --git a/docs/api-reference/v2/work-item-type-properties/list-type-properties.md b/docs/api-reference/v2/work-item-type-properties/list-type-properties.md
new file mode 100644
index 00000000..c5612c27
--- /dev/null
+++ b/docs/api-reference/v2/work-item-type-properties/list-type-properties.md
@@ -0,0 +1,317 @@
+---
+title: List type properties
+description: List the custom properties attached to a Plane work item type with the v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list type properties, work item type properties, custom properties, attached properties, GET type properties
+---
+
+# List type properties
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/properties/
+
+
+
+
+
+Return the custom properties currently attached to one work item type, as a paginated list. This is the call that
+answers "what fields does a `Bug` have?" — reach for it before rendering a form, validating an import, or deciding
+which property values to send when you create a work item.
+
+Each entry is the **full property definition**, not a link stub, so labels, input types, and option lists all arrive in
+this one response. Properties that exist in the project but are not attached to this type are not returned; for the
+project-wide inventory use
+[List work item properties](/api-reference/v2/work-item-properties/list-work-item-properties).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item type belongs to.
+
+
+
+
+
+The work item type whose properties you want to list.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+There are no filters on this endpoint — the attachment itself is the filter. What you can control is ordering and page
+size.
+
+Neither `order_by` nor `paginate` is validated — check your spelling, because both fail silently. A value outside the
+lists below is not rejected: an unrecognized `order_by` falls back to the default ordering, and anything other than
+`paginate=cursor` uses offset pagination.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` , `-sort_order` — the order the properties are shown in, and the default
+- `created_at` , `-created_at` — when each property was created
+- `id` , `-id`
+
+Order by `sort_order` when you are rendering the type's form to a user; it is the order Plane itself uses. Note that
+`sort_order` is not part of the returned property object — sort by `created_at` or `id` if you need the sort key to be
+visible in the payload.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most types carry a handful of properties, so one page is usually the whole set.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response rather
+than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of
+`next` and `total_count`. Pair it with `order_by=created_at` or `order_by=id` — the default `sort_order` is not a
+unique key, so it is not cursor-eligible. Omit the parameter for the default offset envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the
+response.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's work item types. |
+| `404` | `resource_not_found` | No such workspace, project, or type — or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Reads work in either mode
+Listing is not mode-gated. A project surfaces the properties on its types whether the workspace manages types at the
+project level or the workspace level — only the writes on this path
+([attach](/api-reference/v2/work-item-type-properties/attach-type-property) and
+[detach](/api-reference/v2/work-item-type-properties/detach-type-property)) care. See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+::: tip Branch on `property_type`, not on the label
+`display_name` is what a human renamed the field to this week. `property_type`, `is_multi`, and `is_required` are what
+tell your code whether to render a date picker or a multi-select and whether a value is mandatory. Key your integration
+on `id` and `property_type`; treat `display_name` as text for humans.
+:::
diff --git a/docs/api-reference/v2/work-item-type-properties/overview.md b/docs/api-reference/v2/work-item-type-properties/overview.md
new file mode 100644
index 00000000..da3bfe2d
--- /dev/null
+++ b/docs/api-reference/v2/work-item-type-properties/overview.md
@@ -0,0 +1,222 @@
+---
+title: Type properties overview
+description: How a Plane work item type exposes custom properties in API v2 — the attach/detach link, the property object reads return, and mode conflicts.
+keywords: plane api v2, work item type properties, attach property to type, detach property, custom properties, issue type properties
+---
+
+# Type properties overview
+
+A work item type — `Bug`, `Feature`, `Task` — decides which custom properties a work item of that type collects. This
+resource is the link between the two. It lists the properties a type currently exposes, attaches more, and detaches
+them.
+
+::: warning This resource attaches properties — it does not create them
+There is no create here, and no update at all. A property is a **project-level** object with its own lifecycle: it is
+defined once under `/work-item-properties/` and can be exposed by several types.
+
+- To define a new property, use
+ [Create a work item property](/api-reference/v2/work-item-properties/create-work-item-property).
+- To edit its `display_name`, `is_required`, options, or any other field, use
+ [Update a work item property](/api-reference/v2/work-item-properties/update-work-item-property).
+- To make an existing property appear on a type, use
+ [Attach a property to a type](/api-reference/v2/work-item-type-properties/attach-type-property) — the `POST` on this
+ path takes ids of properties that already exist.
+
+`POST`ing a property definition (`display_name`, `property_type`, …) to this path is a `400`. It only accepts a list of
+ids.
+:::
+
+So adding a new field to a type is two calls: create the property in the project, then attach it to the type. The
+payoff is that "Severity" can be one definition shared by `Bug` and `Incident` instead of two definitions that drift
+apart.
+
+
+
+
+## The property object
+
+Reads on this path return the **full property definition**, not a thin link object. One call to
+[List type properties](/api-reference/v2/work-item-type-properties/list-type-properties) gives you everything needed to
+render the type's form — field labels, input types, option lists — with no second lookup under
+`/work-item-properties/`.
+
+Every field is read-only here. Changing a definition goes through
+[Update a work item property](/api-reference/v2/work-item-properties/update-work-item-property).
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the property. This is the id you send to attach it, and the `pk` you delete to detach it.
+
+- `name` _string_
+
+ Machine-readable name, slugified by Plane from `display_name`. Use `id` for lookups — `name` follows the label.
+
+- `display_name` _string_
+
+ The label shown on the work item form.
+
+- `description` _string_
+
+ Free-form explanation of what the property captures. Nullable.
+
+- `property_type` _string_
+
+ What kind of value the property holds. One of `TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `OPTION`, `RELATION`, `URL`,
+ `EMAIL`, `FILE`, or `FORMULA`. This is the field to branch on when you build an input or parse a value.
+
+- `relation_type` _string_
+
+ For a `RELATION` property, what it points at: `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`. `null` for every other
+ `property_type`.
+
+- `is_required` _boolean_
+
+ Whether a value must be supplied on work items of a type that exposes this property.
+
+- `is_multi` _boolean_
+
+ Whether the property holds several values instead of one.
+
+- `is_active` _boolean_
+
+ Whether the property is currently enabled. This lives on the property, not on the link — deactivating affects every
+ type the property is attached to, while detaching affects only one type.
+
+- `default_value` _array of string_
+
+ Values applied when a work item is created without an explicit value for this property.
+
+- `settings` _any_
+
+ Type-specific configuration. The shape depends on `property_type`.
+
+- `validation_rules` _any_
+
+ Constraints applied to submitted values. The shape depends on `property_type`.
+
+- `logo_props` _any_
+
+ Icon and appearance metadata used when the property is rendered.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they map a property to a record in another system.
+
+- `created_at` _string (date-time)_
+
+ When the property was created.
+
+- `options` _array of any_
+
+ For an `OPTION` property, its selectable options inline — each with `id`, `name`, `description`, `is_default`,
+ `sort_order`, `external_id`, and `external_source`. An empty array for every other `property_type`. Manage them with
+ [Property options](/api-reference/v2/work-item-property-options/overview).
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | -------------------------------------------------------------------------------------------- | ------------------------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/properties/` | List the properties on a type |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/properties/` | Attach properties to a type |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/properties/{pk}/` | Get one property through a type |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/properties/{pk}/` | Detach a property from a type |
+
+`{pk}` is the **property id**, not a separate link id. The same id you attach is the id you delete.
+
+## Attaching
+
+`POST` takes `{"properties": ["", …]}` and appends those properties to the type. The response is `201` with a
+`properties` array.
+
+- Only properties belonging to the **same project** can be attached. An id from another project is a
+ `400 validation_error` that names the offending ids, never a silent skip — one bad id rejects the whole call.
+- Re-attaching a property the type already exposes is harmless. The call still returns `201` and the type is not left
+ with a duplicate, so a replayed request is safe.
+
+## Detaching
+
+`DELETE .../properties/{pk}/` removes the link and returns `204`.
+
+::: warning Detaching clears stored values
+The property definition survives — it stays in the project and stays attached to every other type that uses it, ready
+to re-attach. What does not survive is the data: the values already recorded for that property on work items **of this
+type** are removed with the link, and re-attaching does not bring them back.
+
+Deleting the definition outright is a different call —
+[Delete a work item property](/api-reference/v2/work-item-properties/delete-work-item-property).
+:::
+
+## Mode matters for writes, not reads
+
+Attaching and detaching are **project-mode** writes. If the workspace manages work item types at the workspace level,
+both return `409 work_item_types_managed_at_workspace` — the capability exists, it just lives on the other surface. Use
+[Attach a property to a workspace type](/api-reference/v2/workspace-work-item-type-properties/attach-workspace-type-property)
+instead.
+
+Reads are unaffected: listing and retrieving a type's properties works in either mode.
+
+See [Work item type modes](/api-reference/v2/work-item-type-modes) for how to tell which mode a workspace is in.
+
+## Changed from v1
+
+- In v1 a property was created **inside** a type, at
+ `POST /api/v1/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/work-item-properties/`, so it belonged
+ to that one type.
+- In v2 the definition moved up to the project (`/work-item-properties/`) and this path became a link:
+ `POST` attaches existing properties, `DELETE` detaches one. There is no create and no update here.
+- One property can now be attached to several types.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
diff --git a/docs/api-reference/v2/work-item-types/create-work-item-type.md b/docs/api-reference/v2/work-item-types/create-work-item-type.md
new file mode 100644
index 00000000..712595c8
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/create-work-item-type.md
@@ -0,0 +1,220 @@
+---
+title: Create a work item type
+description: Create a project-level work item type in Plane with the v2 REST API. Body parameters, project mode requirements, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create work item type, issue type, project mode, external_id, POST work item types
+---
+
+# Create a work item type
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/
+
+
+
+
+
+Add a work item type to a project — `Bug`, `Spike`, `Chore`, whatever your team actually tracks. The new type is immediately selectable on work items, and becomes the target you attach custom properties to.
+
+Call [Enable work item types](/api-reference/v2/work-item-types/enable-work-item-types) first. Creating a type in a project where the feature has never been turned on leaves you with a type nobody can pick.
+
+Keep the `id` from the response. Every later step — attaching properties, reading the schema, creating a work item of this type — is keyed on it.
+
+::: warning This write requires project mode
+If the workspace manages types at the workspace level, this returns `409 work_item_types_managed_at_workspace`. Create the type on [the workspace surface](/api-reference/v2/workspace-work-item-types/create-workspace-work-item-type) instead, then [import it into the project](/api-reference/v2/work-item-types/import-work-item-types). See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to add the type to.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Display name for the type, for example `Bug`. Maximum 255 characters.
+
+
+
+
+
+What this type is for. This is not decoration: it is returned as `type_description` by the [schema endpoint](/api-reference/v2/work-item-types/get-work-item-type-schema), which is how an integration or agent decides between `Bug` and `Task` without a human in the loop.
+
+
+
+
+
+Whether the type can be assigned to new work items. Create it inactive if you are staging a configuration and want to attach its properties before anyone can select it.
+
+
+
+
+
+Your system's identifier for this type, for sync and import correlation. Maximum 255 characters, nullable.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters, nullable.
+
+
+
+
+
+
+::: info is_default, is_epic, level and logo_props are not accepted here
+`is_default` moves through [mark-default](/api-reference/v2/work-item-types/mark-default-work-item-type). `is_epic` and `level` are read-only. `logo_props` is generated by Plane when the type is created — the response tells you what it picked.
+:::
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ---------------------------------------------------------------------------------- |
+| `400` | `validation_error` | Missing `name`, or a field over its 255-character limit. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't create work item types. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_workspace` | The workspace manages types at the workspace level. Use the workspace surface. |
+| `409` | `conflict` | A type in this project already uses this `external_id` and `external_source` pair. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Reuse external_id to stay idempotent
+`external_id` alone is not unique — the pair `(external_id, external_source)` is, within a project. Send both when you are syncing from another system so a repeated import surfaces as a clean `409 conflict` instead of a duplicate type.
+:::
diff --git a/docs/api-reference/v2/work-item-types/delete-work-item-type.md b/docs/api-reference/v2/work-item-types/delete-work-item-type.md
new file mode 100644
index 00000000..9be83c14
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/delete-work-item-type.md
@@ -0,0 +1,159 @@
+---
+title: Delete a work item type
+description: Delete a project work item type in Plane with the v2 REST API. Protected deletes, project mode requirements, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete work item type, issue type, 204, conflict, project mode, DELETE work item types
+---
+
+# Delete a work item type
+
+
+
+Remove a work item type from a project. A successful delete returns `204` with an empty body.
+
+Deletion is guarded. A type that is the project default, or that still has work items on it, is rejected with `409 conflict` rather than cascading — the API will not silently untype existing work.
+
+If your goal is to stop people picking a type, [set `is_active` to `false`](/api-reference/v2/work-item-types/update-work-item-type) instead. That works regardless of how many work items already use the type, and it is reversible.
+
+::: warning This write requires project mode
+If the workspace manages types at the workspace level, this returns `409 work_item_types_managed_at_workspace`. Delete the type on [the workspace surface](/api-reference/v2/workspace-work-item-types/delete-workspace-work-item-type) instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the type belongs to.
+
+
+
+
+
+The id of the work item type to delete.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ------------------------------------------------------------------------------ |
+| `400` | `validation_error` | The request could not be processed as sent. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't delete work item types. |
+| `404` | `resource_not_found` | No such type, project, or workspace — or the type belongs to another project. |
+| `409` | `work_item_types_managed_at_workspace` | The workspace manages types at the workspace level. Use the workspace surface. |
+| `409` | `conflict` | The type is the project default, or work items still use it. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## Before a delete will succeed
+
+Work through these in order:
+
+1. **It must not be the default.** Promote another type with [mark-default](/api-reference/v2/work-item-types/mark-default-work-item-type). Marking a new default clears the flag on this one in the same request.
+2. **No work items may reference it.** Move them to another type, or delete them. There is no force flag — the check is the point.
+
+Both failures come back as `409 conflict`, distinguished by the `detail` string. Branch on the status and code; treat `detail` as text for humans.
+
+::: tip Two 409s, two different meanings
+`conflict` means the type is protected or in use — a state you can fix. `work_item_types_managed_at_workspace` means you called the wrong surface — a routing mistake, fixed by calling the workspace endpoint instead. Always read the `code`, never just the status.
+:::
diff --git a/docs/api-reference/v2/work-item-types/enable-work-item-types.md b/docs/api-reference/v2/work-item-types/enable-work-item-types.md
new file mode 100644
index 00000000..59c62be5
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/enable-work-item-types.md
@@ -0,0 +1,179 @@
+---
+title: Enable work item types
+description: Turn work item types on for a Plane project with the v2 REST API. The first call in the project-mode lifecycle, the provisioned default type, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, enable work item types, issue types, project mode, default type, POST enable
+---
+
+# Enable work item types
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/enable/
+
+
+
+
+
+Turn the work item types feature on for a project. **This is the first call in the project-mode lifecycle** — until it succeeds the project has no types, so there is nothing to create properties against and nothing for a work item's `type_id` to point at.
+
+Enabling also provisions a default type, normally named `Task`, and returns it. That returned `id` is the type new work items land on when no `type_id` is supplied, and it is a valid target for [attaching properties](/api-reference/v2/work-item-type-properties/attach-type-property) straight away.
+
+There is **no request body**. The path carries everything the call needs.
+
+::: warning This write requires project mode
+If the workspace manages types at the workspace level, this returns `409 work_item_types_managed_at_workspace`. In that mode there is nothing to enable per project — the workspace already owns the types, and you [import them into the project](/api-reference/v2/work-item-types/import-work-item-types) instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to turn work item types on for.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+None. Send the request without a body.
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ------------------------------------------------------------------------------- |
+| `400` | `validation_error` | The request could not be processed as sent. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't configure this project's work item types. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_workspace` | The workspace manages types at the workspace level. Import instead of enabling. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Calling it twice is safe
+A project that already has a default type gets that type back rather than a second one. You can put this call at the head of a provisioning script without guarding it — `200` either way, and the `id` you need in the body.
+:::
+
+## What comes next
+
+With the feature on and a default type in hand, the rest of the setup runs in order:
+
+1. [Create your own types](/api-reference/v2/work-item-types/create-work-item-type) — `Bug`, `Spike`, and so on.
+2. [Create the properties](/api-reference/v2/work-item-properties/create-work-item-property) they should carry.
+3. [Attach each property to a type](/api-reference/v2/work-item-type-properties/attach-type-property), deciding there whether it is required.
+4. [Read the schema](/api-reference/v2/work-item-types/get-work-item-type-schema) to confirm what a work item of that type now accepts.
+
+The full sequence is laid out in the [work item types overview](/api-reference/v2/work-item-types/overview).
diff --git a/docs/api-reference/v2/work-item-types/get-work-item-type-schema.md b/docs/api-reference/v2/work-item-types/get-work-item-type-schema.md
new file mode 100644
index 00000000..ea75a2bb
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/get-work-item-type-schema.md
@@ -0,0 +1,373 @@
+---
+title: Get a work item type schema
+description: Discover the fields and custom properties a Plane work item type accepts with the v2 REST API. The include parameter, fields and custom_fields shapes, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, work item type schema, custom properties, discovery endpoint, custom_fields, include members labels, agent tooling
+---
+
+# Get a work item type schema
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{pk}/schema/
+
+
+
+
+
+Ask the API what a work item of this type accepts. The response describes the **standard fields** every work item has — with the project's actual states, priorities, and estimate points inlined as valid options — plus the **custom properties** attached to this type, including which ones are required and what values they allow.
+
+This is the discovery endpoint. A client that is about to create a work item calls this first, then builds its request body from the answer instead of hardcoding a field list. That matters most for two callers:
+
+- **Integrations**, which must adapt when an admin adds a required property to a type. Reading the schema each time means a new required field surfaces as a field to fill, not as a `400` in production.
+- **AI agents and generated tooling**, which need a machine-readable contract. `fields` and `custom_fields` are shaped to be handed straight to a function-calling schema: every entry carries a type, a required flag, and its allowed options.
+
+Reads are unaffected by [work item type mode](/api-reference/v2/work-item-type-modes) — the schema resolves the type's properties whether they are owned by the project or by the workspace.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the type belongs to. The schema is project-specific: states, labels, members, and estimate points all come from this project.
+
+
+
+
+
+The id of the work item type whose schema you want.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+
+
+
+
+Comma-separated extra option lists to inline. Two values are recognized:
+
+- `members` — inlines the project's active members as the options for `assignee_ids`, and for any custom property that is a user relation
+- `labels` — inlines the project's labels as the options for `label_ids`
+
+Omit it and those fields still appear, they just describe themselves without an option list. These lists can be large in a big workspace, so ask for them only when you are actually rendering a picker — for example `?include=members,labels`.
+
+
+
+
+
+
+
+
+### Response Fields
+
+
+
+
+
+The type this schema describes. Send it as `type_id` on the work item you create. Nullable.
+
+
+
+
+
+The type's display name, for example `Bug`. Nullable.
+
+
+
+
+
+The type's description. This is the text a human or an agent reads to decide whether this is the right type for the work at hand, which is why filling it in on [create](/api-reference/v2/work-item-types/create-work-item-type) is worth the effort. Nullable.
+
+
+
+
+
+The icon and background color rendered next to the type. Nullable.
+
+
+
+
+
+The standard work item fields, keyed by the body parameter name you would send — `name`, `description_html`, `priority`, `state_id`, `assignee_ids`, `label_ids`, `start_date`, `target_date`, `parent_id`. Each entry carries a `type`, a `required` flag, and where it applies an `is_multi` flag, a `default`, a `max_length`, a `format`, or an `options` array.
+
+`state_id` and `priority` always arrive with their options inlined, because those are the two fields a client cannot guess. `estimate_point_id` appears only when the project has an estimate system configured.
+
+
+
+
+
+The custom properties attached to this type, keyed by property name. Each entry carries the property `id`, its `type`, `name`, `display_name`, `description`, a `required` flag, and an `is_multi` flag.
+
+- `OPTION` properties also carry an `options` array of `{ id, name, logo_props }`, narrowed to the options this type allows.
+- `RELATION` properties also carry a `relation_type`.
+
+Empty when the type has no properties attached, or when custom properties are not available for the workspace.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ----------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's work item types. |
+| `404` | `resource_not_found` | No such type, project, or workspace — or the type belongs to another project. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## How to use the schema
+
+The response is a plan for the next request. A client that reads it correctly needs no hardcoded knowledge of the project's configuration.
+
+1. **Pick the type.** Read `type_name` and `type_description` to confirm this is the right type, and carry `type_id` into the work item body.
+2. **Fill the standard fields from `fields`.** Anything with `"required": true` must be present. Where an entry has `options`, send one of those `id` values — a state id from another project is a `400`, never a silent link.
+3. **Fill the custom properties from `custom_fields`.** Send them under `custom_fields` on the work item, keyed by the same property name. Entries with `"required": true` must be present on create.
+4. **Respect `is_multi`.** A field with `"is_multi": true` takes an array; one without takes a single value.
+
+::: tip Re-read the schema, don't cache it forever
+Admins add and retire properties. A property that became required since your last sync is the most common cause of a surprise `400` on work item create. Reading the schema at the start of a sync run costs one request and removes the failure mode entirely.
+:::
+
+## Property types
+
+`custom_fields` entries carry the property's `type`. The possible values are `TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `OPTION`, `RELATION`, `URL`, `EMAIL`, `FILE`, and `FORMULA`.
+
+A `RELATION` property additionally carries `relation_type`, one of `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`, telling you what kind of id its value refers to.
+
+::: info Options are narrowed per type
+The `options` list on an `OPTION` property is what **this type** allows, which can be a subset of every option defined on the property. Read the options from the schema rather than from [the property options endpoint](/api-reference/v2/work-item-property-options/list-property-options) when you want the values valid for this specific type.
+:::
+
+## Related
+
+- [Attach a property to a type](/api-reference/v2/work-item-type-properties/attach-type-property) — what puts an entry into `custom_fields`
+- [Create a work item property](/api-reference/v2/work-item-properties/create-work-item-property) — defining the property in the first place
+- [Create a work item](/api-reference/v2/work-items/create-work-item) — where the schema gets spent
diff --git a/docs/api-reference/v2/work-item-types/get-work-item-type.md b/docs/api-reference/v2/work-item-types/get-work-item-type.md
new file mode 100644
index 00000000..5ca07c07
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/get-work-item-type.md
@@ -0,0 +1,162 @@
+---
+title: Get a work item type
+description: Retrieve a single project work item type from Plane with the v2 REST API. Path parameters, response attributes, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get work item type, issue type, type_id, is_default, is_epic, GET work item types
+---
+
+# Get a work item type
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{pk}/
+
+
+
+
+
+Retrieve one work item type by id. Use it to confirm a type still exists and is still active before you write a work item against it, or to refresh a cached name after someone renamed the type in the app.
+
+This returns the type's own attributes only. It does **not** tell you which fields or custom properties a work item of this type accepts — that is [Get a work item type schema](/api-reference/v2/work-item-types/get-work-item-type-schema).
+
+Reads work in either [work item type mode](/api-reference/v2/work-item-type-modes), so this call never needs to branch on how the workspace is configured.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the type belongs to.
+
+
+
+
+
+The id of the work item type to retrieve.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ----------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this project's work item types. |
+| `404` | `resource_not_found` | No such type, project, or workspace — or the type belongs to another project. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info A type from another project is a 404
+Type ids are scoped to the project path you call them on. Passing a valid type id under the wrong `project_id` returns `404 resource_not_found` rather than `403` — existence outside your scope is never leaked.
+:::
+
+::: tip Check is_active before offering a type
+`is_active: false` means the type has been retired from pickers without being deleted. Existing work items keep it, but new ones should not be created with it.
+:::
diff --git a/docs/api-reference/v2/work-item-types/import-work-item-types.md b/docs/api-reference/v2/work-item-types/import-work-item-types.md
new file mode 100644
index 00000000..f9518401
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/import-work-item-types.md
@@ -0,0 +1,191 @@
+---
+title: Import work item types
+description: Import workspace-level work item types into a Plane project with the v2 REST API. Body parameters, mode requirements, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, import work item types, workspace types, workspace mode, project types, POST import
+---
+
+# Import work item types
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/import/
+
+
+
+
+
+Make a set of **workspace-level** work item types available inside one project. Import does not create anything: the types already exist on the workspace, and this call adds them to a project's roster so work items there can use them.
+
+This is the workspace-mode way to answer "which types does this project use". Reach for it when a workspace defines `Bug`, `Task`, and `Incident` centrally and a new project should offer `Bug` and `Task` but not `Incident`. List the candidates with [List workspace work item types](/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types), then send the ids you want.
+
+After a successful import, the imported types show up in [List work item types](/api-reference/v2/work-item-types/list-work-item-types) for the project and each one's [schema](/api-reference/v2/work-item-types/get-work-item-type-schema) resolves against this project's states, labels, and members.
+
+::: warning This is the one write here that requires workspace mode
+Every other write in this group requires project mode. Import is the opposite: the types it moves are workspace-owned, so it only makes sense when the workspace manages types at the workspace level.
+
+Calling it while the workspace runs in **project** mode returns `409 work_item_types_managed_at_project` — in that mode a project authors its own types with [Create a work item type](/api-reference/v2/work-item-types/create-work-item-type) and there is nothing to import. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to import the types into.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+The ids of the workspace work item types to make available in this project. Send them in one request rather than looping — the import is applied as a batch.
+
+An id that does not resolve to a work item type on this workspace is rejected with `400 validation_error`.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | -------------------------------------------------------------------------------- |
+| `400` | `validation_error` | `work_item_types` missing, or an entry that isn't a UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't configure this project's work item types. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | The workspace manages types per project. Create the type in the project instead. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info The response tells you nothing — read the list instead
+A successful import returns `200` with an empty body, not the types it imported. To confirm the result, follow it with [List work item types](/api-reference/v2/work-item-types/list-work-item-types) for the project.
+:::
+
+::: tip Re-importing is safe
+Sending an id the project already has is not an error and does not duplicate anything. That makes the call safe to run on every provisioning pass: send the full desired set each time rather than diffing first.
+:::
+
+## When to import versus create
+
+| Situation | Use |
+| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
+| The workspace owns types and this project should offer some of them | Import |
+| The project owns its own types | [Create a work item type](/api-reference/v2/work-item-types/create-work-item-type) |
+| You need a new type that does not exist at the workspace level yet | [Create it on the workspace](/api-reference/v2/workspace-work-item-types/create-workspace-work-item-type), then import it |
diff --git a/docs/api-reference/v2/work-item-types/list-work-item-types.md b/docs/api-reference/v2/work-item-types/list-work-item-types.md
new file mode 100644
index 00000000..a71a2296
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/list-work-item-types.md
@@ -0,0 +1,265 @@
+---
+title: List work item types
+description: List a Plane project's work item types with the v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list work item types, issue types, type_id, pagination, GET work item types
+---
+
+# List work item types
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/
+
+
+
+
+
+Return the work item types available in a project as a paginated list. This is how you resolve a type name such as `Bug` to the `type_id` you send when creating a work item, and how you build a type picker that stays in sync with the project's configuration.
+
+Reading types is unaffected by [work item type mode](/api-reference/v2/work-item-type-modes). A project in workspace mode still lists the types imported into it here, so a read integration never has to branch on mode.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project whose work item types you want to list.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+Neither `order_by` nor `paginate` is validated — check your spelling, because both fail silently. An unrecognized `order_by` value falls back to the default ordering, and anything other than `paginate=cursor` uses offset pagination. A typo surfaces as an unexpected sort order or envelope, not as an error.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `level` , `-level` — the type hierarchy level
+- `name` , `-name` — alphabetical
+- `created_at` , `-created_at` — when each type was added
+- `id` , `-id`
+
+Order by `level` when you are rendering types to a user; it is the order the project itself uses.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most projects define a handful of types, so one page is usually the whole set.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. Omit it for the default offset envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+::: info An empty list is a real answer
+A project that has never called [enable](/api-reference/v2/work-item-types/enable-work-item-types) returns an empty `data` array rather than an error. Treat "no types" as "the feature has not been turned on for this project yet", not as a failure.
+:::
+
+::: tip Listing does not tell you what a type accepts
+The list gives you names and ids. To find out which fields and custom properties a work item of a given type takes, call [Get a work item type schema](/api-reference/v2/work-item-types/get-work-item-type-schema) with that type's `id`.
+:::
diff --git a/docs/api-reference/v2/work-item-types/mark-default-work-item-type.md b/docs/api-reference/v2/work-item-types/mark-default-work-item-type.md
new file mode 100644
index 00000000..96b5f726
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/mark-default-work-item-type.md
@@ -0,0 +1,179 @@
+---
+title: Mark a work item type as default
+description: Promote a Plane project work item type to the project default with the v2 REST API. Single-request promotion, project mode requirements, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, mark default work item type, is_default, default issue type, project mode, POST mark-default
+---
+
+# Mark a work item type as default
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{pk}/mark-default/
+
+
+
+
+
+Promote a type to the project's default — the type new work items get when the request supplies no `type_id`.
+
+**Exactly one type per project is the default.** Marking a new one clears the flag on the type that held it, so promotion is a single request rather than a demote-then-promote pair. There is no way to have zero defaults, and no way to have two.
+
+There is **no request body**. The type is identified by `{pk}` in the path, and the response is the updated type.
+
+`is_default` is deliberately not writable through [create](/api-reference/v2/work-item-types/create-work-item-type) or [update](/api-reference/v2/work-item-types/update-work-item-type). Moving the flag has a side effect on another row, so it gets its own endpoint instead of hiding inside a `PATCH`.
+
+::: warning This write requires project mode
+If the workspace manages types at the workspace level, this returns `409 work_item_types_managed_at_workspace`. Promote the type on [the workspace surface](/api-reference/v2/workspace-work-item-types/mark-default-workspace-work-item-type) instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the type belongs to.
+
+
+
+
+
+The id of the work item type to make the default.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+None. Send the request without a body.
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ------------------------------------------------------------------------------ |
+| `400` | `validation_error` | The request could not be processed as sent. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't update work item types. |
+| `404` | `resource_not_found` | No such type, project, or workspace — or the type belongs to another project. |
+| `409` | `work_item_types_managed_at_workspace` | The workspace manages types at the workspace level. Use the workspace surface. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning The response only shows the type you promoted
+The previously default type is demoted in the same operation, but it is not included in the response. If you are holding cached type objects, refresh the whole set with [List work item types](/api-reference/v2/work-item-types/list-work-item-types) after promoting, or your cache will briefly show two defaults.
+:::
+
+::: tip The default type is protected
+Once a type is the default it cannot be deleted, and it cannot be deactivated with `"is_active": false`. Both are the same guard from two directions: a project always needs somewhere for untyped work items to land. To retire the current default, promote a replacement first — this call — and then delete or deactivate the old one.
+:::
diff --git a/docs/api-reference/v2/work-item-types/overview.md b/docs/api-reference/v2/work-item-types/overview.md
new file mode 100644
index 00000000..ecadbd1c
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/overview.md
@@ -0,0 +1,154 @@
+---
+title: Work item types overview
+description: The Plane API v2 project work item type object. Attributes, the project-mode lifecycle from enable to schema, endpoints, and the mode conflict rules.
+keywords: plane api v2, work item types, issue types, custom properties, project mode, work item type schema, is_epic, mark default
+---
+
+# Work item types overview
+
+A work item type gives work items a shape. `Bug`, `Task`, and `Spike` are not just labels — each type carries its own set of custom properties, so a `Bug` can require a severity and a reproduction URL while a `Task` requires neither. Every work item in a type-enabled project points at exactly one type through `type_id`.
+
+The pages in this group cover the **project-level** work item type endpoints: types owned by a single project and configured under `/projects/{project_id}/work-item-types/`.
+
+::: warning A workspace runs in exactly one mode
+Work item types are managed either **per project** or **per workspace** — never both. The endpoints on this page are the project surface, and their **writes require project mode**. Calling one while the workspace runs in workspace mode returns `409 work_item_types_managed_at_workspace`, not a `404` or `403`: the capability exists, it just lives on the other surface.
+
+**Reads are unaffected by mode.** A project still lists and reads its types in either mode, so `GET` and the schema endpoint always work.
+
+See [Work item type modes](/api-reference/v2/work-item-type-modes) for how to detect which mode a workspace is in, and [the workspace type endpoints](/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types) for the other surface.
+:::
+
+
+
+
+## The work item type object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the type. This is the value you send as `type_id` when creating a work item, and the `{type_id}` in the type-properties paths.
+
+- `name` _string_
+
+ Display name, for example `Bug`. Maximum 255 characters.
+
+- `description` _string_
+
+ Free-form explanation of when this type should be used. Worth filling in — an integration or agent reading the [schema endpoint](/api-reference/v2/work-item-types/get-work-item-type-schema) sees this text as `type_description` and uses it to choose between types.
+
+- `is_active` _boolean_
+
+ Whether the type can be assigned to new work items. Deactivating a type hides it from pickers without deleting it or touching the work items that already use it.
+
+- `is_default` _boolean_
+
+ Whether new work items land on this type when no `type_id` is supplied. Exactly one type per project holds the flag. It is not writable through create or update — move it with [Mark a work item type as default](/api-reference/v2/work-item-types/mark-default-work-item-type).
+
+- `is_epic` _boolean_
+
+ Whether this is the project's epic type. Read-only — epics are provisioned by Plane, not authored through this endpoint.
+
+- `level` _number_
+
+ Hierarchy level for the type. Read-only, and the value to sort on when you want the order the project itself uses — see [List work item types](/api-reference/v2/work-item-types/list-work-item-types).
+
+- `logo_props` _any_
+
+ The icon and background color rendered next to the type. Generated by Plane when the type is created, so it is read-only here.
+
+- `created_at` _string (date-time)_
+
+ When the type was created.
+
+::: info external_id is write-only
+`external_id` and `external_source` are accepted on create and update as correlation fields for sync and import, but they are **not returned** in the read shape. Neither is `updated_at`. To find a type you previously synced, keep your own mapping from `external_id` to the returned `id`.
+:::
+
+
+
+## The project-mode lifecycle
+
+Types and their properties are set up in a fixed order. Each step depends on the id returned by the one before it.
+
+1. **Enable the feature** — `POST .../work-item-types/enable/`
+
+ Turns work item types on for the project. This is the first call: until it succeeds, the project has no types to create properties against. It returns the default type Plane provisions, normally named `Task`. See [Enable work item types](/api-reference/v2/work-item-types/enable-work-item-types).
+
+2. **Create your types** — `POST .../work-item-types/`
+
+ Add `Bug`, `Spike`, or whatever your team actually tracks. Keep the `id` from the response — every later step needs it. See [Create a work item type](/api-reference/v2/work-item-types/create-work-item-type).
+
+3. **Create the properties** — `POST .../work-item-properties/`
+
+ Properties are defined at the project level and are independent of any type. A `Severity` option property is created once. See [Create a work item property](/api-reference/v2/work-item-properties/create-work-item-property).
+
+4. **Attach properties to a type** — `POST .../work-item-types/{type_id}/properties/`
+
+ This is the step that makes a property apply to a type, and where you decide whether it is required for that type. The same property can be attached to several types with different settings. See [Attach a property to a type](/api-reference/v2/work-item-type-properties/attach-type-property).
+
+5. **Read the schema** — `GET .../work-item-types/{pk}/schema/`
+
+ Ask the API what a work item of this type accepts: the standard fields with their valid options, plus every custom property attached in step 4. This is the call a client makes right before it constructs a work item. See [Get a work item type schema](/api-reference/v2/work-item-types/get-work-item-type-schema).
+
+After that, create work items with `type_id` set to the type and `custom_fields` filled in from the schema.
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | ------------------------------------------------------------------------------------ | ------------------------------------- |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/enable/` | Enable work item types for a project |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/` | List work item types |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/` | Create a work item type |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{pk}/` | Get a work item type |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{pk}/` | Update a work item type |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{pk}/` | Delete a work item type |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{pk}/schema/` | Get a work item type schema |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{pk}/mark-default/` | Mark a type as the project default |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/import/` | Import workspace types into a project |
+
+## What you can and cannot write
+
+The writable surface is deliberately small: `name`, `description`, `is_active`, `external_id`, and `external_source`.
+
+- `is_default` moves through [mark-default](/api-reference/v2/work-item-types/mark-default-work-item-type), not the request body.
+- `is_epic` and `level` are read-only. Epic types are a separate surface and do not appear in these lists.
+- `logo_props` is generated when the type is created.
+
+## The default type
+
+Exactly one type per project is the default. Marking a new one clears the flag on the previous holder, so promoting a type is a single request rather than a demote-then-promote pair.
+
+The default type is protected. It cannot be deleted, and it cannot be deactivated with `"is_active": false` — promote a different type first.
+
+## Importing instead of creating
+
+`POST .../work-item-types/import/` copies **workspace-level** types into a project rather than creating new ones. It is the workspace-mode counterpart to `create`, so it is the one endpoint in this group whose write requires workspace mode. See [Import work item types](/api-reference/v2/work-item-types/import-work-item-types).
diff --git a/docs/api-reference/v2/work-item-types/update-work-item-type.md b/docs/api-reference/v2/work-item-types/update-work-item-type.md
new file mode 100644
index 00000000..79eaec71
--- /dev/null
+++ b/docs/api-reference/v2/work-item-types/update-work-item-type.md
@@ -0,0 +1,228 @@
+---
+title: Update a work item type
+description: Update a project work item type in Plane with the v2 REST API. Partial PATCH semantics, body parameters, project mode requirements, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update work item type, patch issue type, is_active, external_id, project mode, PATCH work item types
+---
+
+# Update a work item type
+
+
+
+Rename a type, rewrite its description, retire it from pickers, or correct its sync correlation fields. Work items already using the type are untouched — you are editing the type, not retyping anything.
+
+`PATCH` is partial. Send only the fields you want to change; anything you omit keeps its current value. Omitting a field is not the same as sending `null`.
+
+::: warning This write requires project mode
+If the workspace manages types at the workspace level, this returns `409 work_item_types_managed_at_workspace`. Edit the type on [the workspace surface](/api-reference/v2/workspace-work-item-types/update-workspace-work-item-type) instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the type belongs to.
+
+
+
+
+
+The id of the work item type to update.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional — send the subset you are changing.
+
+
+
+
+
+New display name for the type. Maximum 255 characters. Renaming is safe: work items reference the type by `type_id`, not by name.
+
+
+
+
+
+What this type is for. It is returned as `type_description` by the [schema endpoint](/api-reference/v2/work-item-types/get-work-item-type-schema), so this is the field to edit when integrations are choosing the wrong type.
+
+
+
+
+
+Whether the type can be assigned to new work items. Setting it to `false` retires the type from pickers without deleting it — existing work items keep the type and stay readable. This is the safe alternative to [delete](/api-reference/v2/work-item-types/delete-work-item-type), which a type in use rejects.
+
+
+
+
+
+Your system's identifier for this type, for sync and import correlation. Maximum 255 characters, nullable.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters, nullable.
+
+
+
+
+
+
+::: info is_default, is_epic and level are not accepted here
+`is_default` moves through [mark-default](/api-reference/v2/work-item-types/mark-default-work-item-type), which is a dedicated request precisely because promoting one type demotes another. `is_epic` and `level` are read-only, as is `logo_props`.
+:::
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------------------------- | ---------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A field over its 255-character limit, or `"is_active": false` on the default type. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't update work item types. |
+| `404` | `resource_not_found` | No such type, project, or workspace — or the type belongs to another project. |
+| `409` | `work_item_types_managed_at_workspace` | The workspace manages types at the workspace level. Use the workspace surface. |
+| `409` | `conflict` | Another type in this project already uses this `external_id` and `external_source` pair. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: warning The default type cannot be deactivated
+Sending `"is_active": false` for the project's default type is rejected — every project needs a type for untyped work items to land on. Promote another type with [mark-default](/api-reference/v2/work-item-types/mark-default-work-item-type) first, then deactivate this one.
+:::
+
+::: tip Deactivate rather than delete
+[Delete](/api-reference/v2/work-item-types/delete-work-item-type) refuses to remove a type that still has work items. `"is_active": false` is the usual intent anyway: the type stops appearing in pickers, historical work items keep rendering correctly, and the change is reversible.
+:::
diff --git a/docs/api-reference/v2/work-items/archive-work-item.md b/docs/api-reference/v2/work-items/archive-work-item.md
new file mode 100644
index 00000000..dfafc0db
--- /dev/null
+++ b/docs/api-reference/v2/work-items/archive-work-item.md
@@ -0,0 +1,187 @@
+---
+title: Archive a work item
+description: Archive a Plane work item with the v2 REST API. Bodyless POST, the archived_at timestamp, visibility of archived items, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, archive work item, archived_at, POST archive, hide work item, reversible archive
+---
+
+# Archive a work item
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/archive/
+
+
+
+
+
+Archive a work item: stamp `archived_at` with the current time and take it out of the active work item set.
+
+This is a **bodyless** `POST` — send no JSON at all. The response is the full work item in its usual read shape, with
+`archived_at` now populated, so you can confirm the change without a follow-up read.
+
+Archiving is reversible with [Unarchive a work item](/api-reference/v2/work-items/unarchive-work-item), and it is
+entirely separate from [deleting](/api-reference/v2/work-items/delete-work-item) — an archived work item still exists.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item's UUID. This lookup is UUID-only — a `PROJ-142` identifier here returns `404`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+None. The endpoint takes no request body; anything you send is ignored.
+
+
+
+::: warning Archiving twice returns 404
+Once archived, a work item is outside the default query set, so a second `archive` call on the same work item can't
+resolve it and returns `404 resource_not_found` — not a `409`, and not a silent success. If you're re-running a job,
+treat a `404` here as "already archived" rather than an error worth retrying.
+:::
+
+
+
+### Scopes
+
+`projects.work_items:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------------------- |
+| `400` | `validation_error` | The request could not be processed — for example a `pk` that isn't a valid UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't archive this work item. |
+| `404` | `resource_not_found` | No such work item, it's outside your project or tenant, or it is already archived. |
+| `409` | `conflict` | The write collides with a protected-resource constraint. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## What archiving changes
+
+- `archived_at` goes from `null` to a timestamp. It is the only field the call touches — state, assignees, dates, and
+ labels are untouched.
+- The work item drops out of [List work items](/api-reference/v2/work-items/list-work-items) and out of plain detail
+ reads, including the [identifier route](/api-reference/v2/work-items/get-work-item-by-identifier).
+- Nothing is deleted. Comments, links, and relations survive, and
+ [unarchiving](/api-reference/v2/work-items/unarchive-work-item) restores the work item exactly as it was.
+
+`archived_at` is read-only on the work item itself, so you cannot archive by `PATCH`ing a timestamp — this endpoint is
+the only way in, and unarchive is the only way out.
diff --git a/docs/api-reference/v2/work-items/create-work-item.md b/docs/api-reference/v2/work-items/create-work-item.md
new file mode 100644
index 00000000..c3daaea5
--- /dev/null
+++ b/docs/api-reference/v2/work-items/create-work-item.md
@@ -0,0 +1,424 @@
+---
+title: Create a work item
+description: Create a work item in a Plane project with the v2 REST API. Body parameters, id and human-readable inputs, priority values, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create work item, POST work items, state_id, assignees by email, labels by name, parent identifier, priority
+---
+
+# Create a work item
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-items/
+
+
+
+
+
+Create a work item in a project. Only `name` is required — everything else falls back to the project's defaults, so the
+smallest useful create is one field.
+
+Each relation accepts either its id field or a **write-only human-readable parallel**: `state` instead of `state_id`,
+`assignees` (emails) instead of `assignee_ids`, `labels` (names) instead of `label_ids`, and so on. Send one or the
+other, never both. This is what lets an importer or an agent write a fully-populated work item without first resolving
+six UUIDs.
+
+The response is the standard sparse read shape, with `custom_fields` populated.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to create the work item in.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Title of the work item. Maximum 255 characters. The only required field.
+
+
+
+
+
+Rich-text body as HTML, for example `
Steps to reproduce…
`. The HTML is sanitized on the way in; content that
+can't be sanitized is rejected with a `400`.
+
+`description_html` is **not** part of the read shape, so it will not appear in the response.
+
+
+
+
+
+One of `urgent`, `high`, `medium`, `low`, `none`. Defaults to `none`.
+
+
+
+
+
+The workflow state to create the work item in. Must be a state of this project. Omit it and the work item lands in the
+project's default state.
+
+
+
+
+
+The state's **name** instead of its id, for example `In Progress`. Matched case-insensitively within the project.
+Write-only.
+
+Unknown name or a name that matches more than one state is a `400`. Sending both `state` and `state_id` is a `400`.
+
+
+
+
+
+The work item type. Accepts a type owned by the project and a workspace-level type imported into it. Requires work item
+types to be enabled.
+
+
+
+
+
+The type's **name** instead of its id, for example `Bug`. Matched case-insensitively among the project's non-epic
+types. Write-only.
+
+
+
+
+
+The parent work item. The parent must be in the same workspace, but it may live in a different project.
+
+
+
+
+
+The parent's **identifier** instead of its id, for example `PROJ-118`. Resolved within the workspace. Write-only.
+
+
+
+
+
+User ids to assign. Each must be an active, assignable member of the project.
+
+Leave it out and the project's default assignee is applied, if one is configured. Send `[]` to create the work item
+with no assignee at all.
+
+
+
+
+
+Member **email addresses** instead of ids, for example `["ana@example.com"]`. Every address must belong to an active,
+assignable project member — any that don't are named in the `400`. Write-only.
+
+
+
+
+
+Label ids to apply. Each must be a label of this project, or a workspace-level label available to it.
+
+
+
+
+
+Label **names** instead of ids, for example `["regression", "auth"]`. Matched case-insensitively against the project's
+labels and the workspace-level labels available to it. A name that exists at both levels is ambiguous and returns a
+`400` telling you to use `label_ids`. Write-only.
+
+
+
+
+
+The estimate point to assign, from the project's active estimate system.
+
+
+
+
+
+The estimate point's **value** instead of its id, for example `5` or `L`. Resolved against the project's active
+estimate. Write-only.
+
+
+
+
+
+Planned start, for example `2026-01-12`. Must not be after `target_date`.
+
+
+
+
+
+Planned due date, for example `2026-01-20`.
+
+
+
+
+
+Your system's identifier for this work item, for sync and import correlation. Maximum 255 characters.
+
+Stored and filterable on [List work items](/api-reference/v2/work-items/list-work-items), but not returned on reads.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters.
+
+
+
+
+
+
+::: info Setting custom property values
+Work item type custom properties are written through a separate `custom_fields` object on the request body, keyed by
+property name, and are validated against the type you selected. Its shape depends on the type you chose, so it is not
+declared in the OpenAPI schema — read it from
+[the type's schema endpoint](/api-reference/v2/work-item-types/get-work-item-type-schema). It requires custom
+properties to be enabled for the workspace — sending it otherwise is a `400`. Properties you omit are filled from the
+type's defaults. See [work item properties](/api-reference/v2/work-item-properties/list-work-item-properties).
+:::
+
+
+
+### Scopes
+
+`projects.work_items:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | Missing `name`; a bad `priority`; `start_date` after `target_date`; an unresolvable or ambiguous `state`/`type`/`parent`/`assignees`/`labels`/`estimate`; both a name and its `*_id`; or an id from another project or workspace. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't create work items in this project. |
+| `403` | `workflow_transition_denied` | A workflow rule forbids creating a work item in the requested state. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `409` | `conflict` | The write collides with a uniqueness or protected-resource constraint. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## Names or ids, not both
+
+The human-readable inputs are resolved to their id equivalents before anything is written, and the rules are the same
+for all six:
+
+| You send | What happens |
+| ------------------------------------ | --------------------------------------------------------- |
+| `state_id` only | Used directly. Must belong to this project. |
+| `state` only | Resolved by name, case-insensitively, within the project. |
+| `state` and `state_id` | `400` — "Provide either state_id or state, not both." |
+| A name matching nothing | `400` naming the field and the value you sent. |
+| A name matching more than one record | `400` telling you to use the id field instead. |
+
+Prefer the id fields in code you control and reuse across runs; prefer the names when you're bridging a system whose
+vocabulary is already words — a Jira status, a Slack email, a label the user typed.
+
+## Workflow rules can reject a create
+
+If the project runs workflow rules, creating a work item directly into a restricted state returns `403` with
+`workflow_transition_denied` — distinct from a plain permission `forbidden`. Branch on the `code`: `forbidden` means
+ask for access, `workflow_transition_denied` means create it in an allowed state and move it from there.
diff --git a/docs/api-reference/v2/work-items/delete-work-item.md b/docs/api-reference/v2/work-items/delete-work-item.md
new file mode 100644
index 00000000..a2f2f050
--- /dev/null
+++ b/docs/api-reference/v2/work-items/delete-work-item.md
@@ -0,0 +1,165 @@
+---
+title: Delete a work item
+description: Delete a work item in a Plane project with the v2 REST API. Soft-delete behavior, the 204 response, when to archive instead, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete work item, DELETE work item, soft delete, 204 no content, archive vs delete
+---
+
+# Delete a work item
+
+
+
+Delete a work item. The delete is a soft delete — the row is retained internally — but it is gone from every API
+surface, and there is no endpoint to bring it back.
+
+A successful delete returns `204` with an empty body. Deleting a work item that is already gone returns `404`, so the
+call is not idempotent from the client's point of view.
+
+If you want the work item out of the way but recoverable, use
+[Archive a work item](/api-reference/v2/work-items/archive-work-item) instead.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item's UUID. This lookup is UUID-only — a `PROJ-142` identifier here returns `404`.
+
+
+
+
+
+
+::: warning Deleting is not reversible through the API
+There is no undelete endpoint. Confirm with the user before wiring this into an automation, and prefer archiving for
+anything you might want back.
+:::
+
+
+
+### Scopes
+
+`projects.work_items:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | -------------------------------------------------------------------------------- |
+| `400` | `validation_error` | The request could not be processed — for example a `pk` that isn't a valid UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't delete this work item. |
+| `404` | `resource_not_found` | No such work item, or it's outside your project or tenant. |
+| `409` | `conflict` | The delete collides with a protected-resource constraint. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## Who can delete
+
+Deleting is gated per work item, not just per project. Depending on how the project's roles are configured, the right
+to delete may be limited to administrators plus the work item's own creator — so a token that can edit a work item may
+still get a `403` when it tries to delete one it didn't create. The response is `403 forbidden` and nothing is written.
+
+## Delete or archive?
+
+| | Delete | Archive |
+| --------------- | ------------------------- | ---------------------------------------------------------------------- |
+| Endpoint | This page | [Archive](/api-reference/v2/work-items/archive-work-item) |
+| Response | `204`, empty body | `200` with the work item |
+| Reversible | No | Yes, via [unarchive](/api-reference/v2/work-items/unarchive-work-item) |
+| Effect on reads | Gone everywhere | Excluded from the default list and detail reads |
+| Use it for | Mistakes, spam, test data | Finished or abandoned work you want to keep |
diff --git a/docs/api-reference/v2/work-items/get-work-item-by-identifier.md b/docs/api-reference/v2/work-items/get-work-item-by-identifier.md
new file mode 100644
index 00000000..261a5ea1
--- /dev/null
+++ b/docs/api-reference/v2/work-items/get-work-item-by-identifier.md
@@ -0,0 +1,211 @@
+---
+title: Get a work item by identifier
+description: Look up a Plane work item by its PROJ-123 human key across a workspace, without knowing the project UUID. Path parameters, expandable relations, OAuth scopes, errors, and code examples.
+keywords: plane api v2, work item identifier, PROJ-123, human key, lookup work item by key, workspace scoped work item
+---
+
+# Get a work item by identifier
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-items/{identifier}/
+
+
+
+
+
+Retrieve a work item by its human key — `PROJ-142` — anywhere in the workspace.
+
+This route exists for callers that **don't know the project UUID**. A `PROJ-142` in a commit message, a chat command, a
+support ticket, or an LLM prompt carries no project id, and making the caller first list projects, find the one whose
+identifier is `PROJ`, and then call the project-scoped route would be three requests to answer one question. Here it is
+one request against the workspace.
+
+The route is read-only. Creates, updates, deletes, and archiving all go through the project-scoped UUID routes.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The work item's human key: the project identifier, a hyphen, and the work item's number — `PROJ-142`.
+
+The project part must start with a letter, and it is upper-cased before lookup, so `proj-142` resolves the same work
+item as `PROJ-142`. A project identifier is unique within a workspace, which is what makes this key unambiguous.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+
+
+
+
+Comma-separated relations to embed alongside the ids: `state`, `type`, `parent`, `assignees`, `labels`.
+
+Expansion is separate-key — `?expand=state` keeps `state_id` and adds a `state` object next to it. An unknown value is
+a `400`.
+
+
+
+
+
+
+::: info Everything unresolvable is a 404
+A malformed key (`PROJ_142`, `142`, `-142`), a key from a project you can't see, a key in another workspace, and a key
+that resolves to nothing all return the same `404`. That is deliberate: the response never reveals whether a work item
+exists, only whether _you_ can read it. Archived work items are not served from this route either.
+:::
+
+
+
+### Scopes
+
+`projects.work_items:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | --------------------------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your token scope can't read work items. |
+| `404` | `resource_not_found` | Malformed identifier, no such work item, it's outside your tenant or visibility, or archived. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## Getting from the identifier to a write
+
+The response body is the full read shape, so it hands you everything a follow-up write needs — `id` for the detail
+route. The project UUID is not in the read shape, so pair the lookup with the project you already know, or resolve it
+once and cache it:
+
+```bash
+# 1. resolve the human key
+GET /api/v2/workspaces/my-team/work-items/PROJ-142/
+
+# 2. write against the project-scoped UUID route
+PATCH /api/v2/workspaces/my-team/projects/{project_id}/work-items/8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13/
+```
+
+## Identifier vs UUID
+
+| | `identifier` | `id` |
+| ---------- | ----------------------------------------- | ----------------------------------------------------------------------------- |
+| Example | `PROJ-142` | `8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13` |
+| Scope | Unique within a workspace | Globally unique |
+| Route | This page — reads only | [Get a work item](/api-reference/v2/work-items/get-work-item) and every write |
+| Comes from | The project identifier plus `sequence_id` | Assigned by Plane at creation |
+
+Both are stable. Use the identifier where a human is in the loop, and the UUID everywhere else.
diff --git a/docs/api-reference/v2/work-items/get-work-item.md b/docs/api-reference/v2/work-items/get-work-item.md
new file mode 100644
index 00000000..cbfe1306
--- /dev/null
+++ b/docs/api-reference/v2/work-items/get-work-item.md
@@ -0,0 +1,228 @@
+---
+title: Get a work item
+description: Retrieve a single work item by UUID with the Plane v2 REST API. Path parameters, expandable relations, custom fields, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get work item, retrieve work item, work item by id, expand state assignees, custom fields
+---
+
+# Get a work item
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/
+
+
+
+
+
+Retrieve one work item by its UUID. This is the endpoint to use after a list call, or any time you already hold the
+work item's `id`.
+
+Unlike the list endpoint, this response populates `custom_fields` — the work item type's custom property values, keyed
+by property name. If you only have a `PROJ-142`-style key and not the project UUID, use
+[Get a work item by identifier](/api-reference/v2/work-items/get-work-item-by-identifier) instead.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item's UUID. This lookup is UUID-only — a `PROJ-142` identifier here returns `404`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+
+
+
+
+Comma-separated relations to embed alongside the ids: `state`, `type`, `parent`, `assignees`, `labels`.
+
+Expansion is separate-key — `?expand=state` keeps `state_id` and adds a `state` object next to it. An unknown value is
+a `400`.
+
+
+
+
+
+
+::: warning Archived work items are not returned
+An archived work item sits outside the default query set, so a read of one returns `404` — the same response as a work
+item that never existed. [Unarchive](/api-reference/v2/work-items/unarchive-work-item) it first if you need to read it
+again. See [Archive a work item](/api-reference/v2/work-items/archive-work-item).
+:::
+
+
+
+### Scopes
+
+`projects.work_items:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ----------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read work items in this project. |
+| `404` | `resource_not_found` | No such work item, it belongs to another project or tenant, or it's archived. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## What you get here that a list doesn't give you
+
+- **`custom_fields`** — the work item type's custom property values. The list endpoint returns `null` for this field on
+ every row to avoid resolving properties per row; only single-item reads populate it.
+- **Cheaper expansion** — `?expand=` works on both, but expanding one work item is a fixed cost while expanding a full
+ page multiplies it.
+
+Everything else — the field set, the sparse `*_id` shape — is identical to a row from
+[List work items](/api-reference/v2/work-items/list-work-items).
diff --git a/docs/api-reference/v2/work-items/list-work-items.md b/docs/api-reference/v2/work-items/list-work-items.md
new file mode 100644
index 00000000..0eacab9b
--- /dev/null
+++ b/docs/api-reference/v2/work-items/list-work-items.md
@@ -0,0 +1,456 @@
+---
+title: List work items
+description: List work items in a Plane project with the v2 REST API. Filters, search, ordering, offset and cursor pagination, expandable relations, OAuth scopes, and code examples.
+keywords: plane api v2, list work items, filter work items, work item pagination, order_by, state_group, priority filter, cursor pagination
+---
+
+# List work items
+
+
+ GET
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-items/
+
+
+
+
+
+List the work items in a project. This is the endpoint you build reports, syncs, and board views on: it takes a wide
+set of filters, orders by stored columns or by meaning, and paginates by offset or by cursor.
+
+Results are always scoped to the project in the path and to what your token is allowed to see. Filters can only narrow
+that set — they never widen it.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project to list work items from.
+
+
+
+
+
+
+
+
+### Filters
+
+Every filter is optional, and filters combine with AND. Each concept below has a base parameter plus the `__in` and
+`__isnull` variants noted in its description.
+
+
+
+
+
+Match work items in a specific state. Use `state_id__in` with a comma-separated list to match any of several states.
+
+
+
+
+
+Match by the state's workflow group instead of a specific state — stable across projects that name their states
+differently. One of `backlog`, `unstarted`, `started`, `completed`, `cancelled`, `triage`.
+
+Use `state_group__in` for several groups, for example `?state_group__in=started,completed`.
+
+
+
+
+
+One of `urgent`, `high`, `medium`, `low`, `none`. Use `priority__in` for several, for example
+`?priority__in=urgent,high`.
+
+
+
+
+
+Match work items assigned to a user. `assignee_id__in` matches any of several users; `assignee_id__isnull=true`
+returns only unassigned work items.
+
+
+
+
+
+Match work items carrying a label. `label_id__in` matches any of several labels; `label_id__isnull=true` returns only
+unlabeled work items.
+
+
+
+
+
+Match work items of a given work item type. `type_id__in` accepts a comma-separated list.
+
+
+
+
+
+Match the children of a work item. `parent_id__in` accepts several parents; `parent_id__isnull=true` returns only
+top-level work items.
+
+
+
+
+
+Match work items in a cycle. `cycle_id__in` accepts several cycles; `cycle_id__isnull=true` returns the backlog of
+work items in no cycle at all.
+
+
+
+
+
+Match work items in a module. `module_id__in` accepts several modules; `module_id__isnull=true` returns work items in
+no module.
+
+
+
+
+
+Match the single work item with this number within the project — the `142` of `PROJ-142`.
+
+
+
+
+
+Filter drafts in or out. Drafts are work items still being composed in the Plane app.
+
+
+
+
+
+Correlation filters for sync and import. `external_source` is the system a record came from and `external_id` is its
+key there, so the pair is how you find the Plane work item that mirrors a row in your own database.
+
+These values are not returned on reads — the lookup is one-way.
+
+
+
+
+
+Bound the creation timestamp. Pass an ISO 8601 datetime, for example `2026-01-01T00:00:00Z`. Use both for a window.
+
+
+
+
+
+Bound the last-modified timestamp. This is the pair to use for incremental sync — poll with
+`?updated_at__gte=&order_by=updated_at`.
+
+`updated_at` is filterable and orderable but is not part of the read shape, so it does not come back in the response
+body.
+
+
+
+
+
+Bound the planned start date, for example `2026-01-01`.
+
+
+
+
+
+Bound the planned due date. `?target_date__lte=2026-01-31&state_group__in=backlog,unstarted,started` is the "what is
+about to slip" query.
+
+
+
+
+
+
+
+
+### Search and ordering
+
+
+
+
+
+Free-text search over the work item name.
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending. Defaults to `-created_at`.
+
+- `created_at`, `-created_at`
+- `updated_at`, `-updated_at`
+- `sequence_id`, `-sequence_id`
+- `id`, `-id`
+- `sort_order`, `-sort_order` — the manual board ordering
+- `priority`, `-priority` — semantic: `urgent` → `high` → `medium` → `low` → `none`, not alphabetical
+- `state_group`, `-state_group` — semantic: workflow order, not alphabetical
+
+`priority` and `state_group` sort by meaning, which is what you want for a board but is not cursor-eligible. Pair
+either with `?paginate=cursor` and you get `400 ordering_not_cursor_eligible`; use the default offset pages instead.
+
+
+
+
+
+
+
+
+### Pagination
+
+Offset is the default. Cursor is opt-in per request.
+
+
+
+
+
+Page size. Defaults to 50, maximum 200.
+
+
+
+
+
+Rows to skip from the start of the result set. Maximum 10000 — past that, switch to cursor pagination.
+
+
+
+
+
+Defaults to `true`. Pass `?count=false` to skip the `COUNT(*)` and omit `total_count` from the envelope. Worth doing on
+large projects when you only need the rows.
+
+
+
+
+
+Set to `cursor` to opt into keyset pagination. The envelope changes to `next_cursor` / `has_more` and drops
+`total_count`. Use it for deep or long-running traversals where offset pages would drift as rows are inserted. Follow
+the returned `next_cursor` as described in [Pagination](/api-reference/v2/pagination).
+
+
+
+
+
+
+
+
+### Expansion
+
+
+
+
+
+Comma-separated relations to embed alongside the ids: `state`, `type`, `parent`, `assignees`, `labels`.
+
+Expansion is separate-key — `?expand=state` keeps `state_id` and adds a `state` object next to it. An unknown value is
+a `400`.
+
+
+
+
+
+
+::: warning Bad filter values fail loudly, bad `order_by` does not
+The enum-backed _filters_ (`priority`, `priority__in`, `state_group`, `state_group__in`) are validated against their
+allowed values. A typo returns `400 validation_error` naming the parameter — it does not silently return an empty page.
+Treat an empty `data` array as a genuine "no matches".
+
+`order_by` and `paginate` are the exception: neither is validated. An unrecognized `order_by` silently falls back to the
+default ordering, and anything other than `paginate=cursor` silently uses offset pagination — so check your spelling
+there, because a typo shows up as an unexpected sort order or envelope rather than an error.
+:::
+
+::: info Archived work items are excluded
+The list returns active work items only — anything with `archived_at` set is filtered out, so archiving a work item
+takes it off every page of this list. See [Archive a work item](/api-reference/v2/work-items/archive-work-item).
+:::
+
+
+
+### Scopes
+
+`projects.work_items:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | --------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read work items in this project. |
+| `404` | `resource_not_found` | No such workspace or project, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## Why `custom_fields` is `null` here
+
+Custom property values are resolved per work item. Doing that for a full page would mean an extra lookup pass for every
+row, so the list endpoint skips it and returns `custom_fields: null` on every item — deliberately, not because the
+values are missing.
+
+To read custom property values, fetch the work item on its own:
+[Get a work item](/api-reference/v2/work-items/get-work-item) or
+[Get by identifier](/api-reference/v2/work-items/get-work-item-by-identifier). Both populate `custom_fields`.
+
+## Paging through everything
+
+Branch your client on `pagination.style` rather than guessing from the keys present.
+
+- **Offset** — follow the integer in `next` until it is `null`. Fine for the first few thousand rows; `offset` caps at 10000.
+- **Cursor** — send `?paginate=cursor`, then follow `next_cursor` while `has_more` is `true`. There is no
+ `total_count`, and rows inserted mid-traversal won't shift your position.
+
+Full details in [Pagination](/api-reference/v2/pagination).
diff --git a/docs/api-reference/v2/work-items/overview.md b/docs/api-reference/v2/work-items/overview.md
new file mode 100644
index 00000000..522a2141
--- /dev/null
+++ b/docs/api-reference/v2/work-items/overview.md
@@ -0,0 +1,273 @@
+---
+title: Work items overview
+description: The Plane API v2 work item object. Sparse id-based reads, the PROJ-123 identifier, custom fields, human-readable write inputs, expandable relations, and every work item endpoint.
+keywords: plane api v2, work items, issues, tasks, sequence id, identifier, custom fields, expand, priority, archive work item
+---
+
+# Work items overview
+
+A work item is the unit of work in Plane — the thing that gets assigned, scheduled, moved through a workflow, and
+closed. Every work item belongs to exactly one project and sits in exactly one [state](/api-reference/v2/states/overview)
+of that project's workflow.
+
+Work items are the most heavily used resource in the API, so the v2 shape is deliberately narrow: reads return **ids,
+not nested objects**, and every relation is a `*_id` or `*_ids` field you can resolve on your own schedule or pull in
+with [`?expand=`](/api-reference/v2/expanding-relations). Writes go the other way — they accept ids **and** the
+human-readable names you already have, so you rarely need a lookup round trip before creating something.
+
+[Learn more about work items](https://docs.plane.so/core-concepts/work-items/overview)
+
+
+
+
+## The work item object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the work item. This is the `{pk}` on every project-scoped detail route.
+
+- `name` _string_
+
+ Title of the work item. Maximum 255 characters.
+
+- `identifier` _string_
+
+ The human key, for example `PROJ-142`. It is the project's identifier joined to `sequence_id`, and it is what people
+ paste into chat and commit messages. Use it with
+ [Get a work item by identifier](/api-reference/v2/work-items/get-work-item-by-identifier) when you don't have the
+ project UUID.
+
+- `sequence_id` _integer_
+
+ The work item's number within its project. Assigned by Plane and never reused.
+
+- `priority` _string_
+
+ One of `urgent`, `high`, `medium`, `low`, or `none`. Never null — an unprioritized work item reads `none`.
+
+- `state_id` _string (uuid)_
+
+ The workflow state the work item is currently in.
+
+- `type_id` _string (uuid)_
+
+ The work item type. `null` when the project has no types enabled or the item is untyped.
+
+- `assignee_ids` _array of string_
+
+ User ids assigned to the work item. Empty array when unassigned.
+
+- `label_ids` _array of string_
+
+ Label ids applied to the work item. Empty array when unlabeled.
+
+- `parent_id` _string (uuid)_
+
+ The parent work item, or `null` for a top-level item. A parent may live in another project of the same workspace.
+
+- `start_date` _string (date)_
+
+ Planned start, or `null`.
+
+- `target_date` _string (date)_
+
+ Planned due date, or `null`.
+
+- `is_draft` _boolean_
+
+ Whether the work item is still a draft. Drafts are created in the Plane app and are excluded from most boards.
+
+- `archived_at` _string (date-time)_
+
+ When the work item was archived, or `null` if it is active. See [Archiving](#archiving-and-deleting).
+
+- `created_at` _string (date-time)_
+
+ When the work item was created.
+
+- `created_by_id` _string (uuid)_
+
+ The user who created the work item. `null` for items created by an automation with no acting user.
+
+- `custom_fields` _object_
+
+ Values of the work item type's custom properties, keyed by property name. **Populated only on single-item
+ responses** — it is always `null` on the list endpoint. See [Custom fields](#custom-fields).
+
+::: info The read shape is sparse and fixed
+`description_html`, `external_id`, `external_source`, and `estimate_point_id` are accepted on writes but are **not**
+part of the read shape — they will not come back on any response. There is also no `updated_at`, no `project_id`, and
+no `workspace_id`: you already know the project from the path.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/` | [List work items](/api-reference/v2/work-items/list-work-items) |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/` | [Create a work item](/api-reference/v2/work-items/create-work-item) |
+| `GET` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/` | [Get a work item](/api-reference/v2/work-items/get-work-item) |
+| `PATCH` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/` | [Update a work item](/api-reference/v2/work-items/update-work-item) |
+| `DELETE` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/` | [Delete a work item](/api-reference/v2/work-items/delete-work-item) |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/archive/` | [Archive a work item](/api-reference/v2/work-items/archive-work-item) |
+| `POST` | `/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/unarchive/` | [Unarchive a work item](/api-reference/v2/work-items/unarchive-work-item) |
+| `GET` | `/api/v2/workspaces/{slug}/work-items/{identifier}/` | [Get by identifier](/api-reference/v2/work-items/get-work-item-by-identifier) |
+
+Every project-scoped route needs `projects.work_items:read` for `GET` and `projects.work_items:write` for `POST`,
+`PATCH`, and `DELETE`.
+
+## Two ways to identify a work item
+
+| Key | Looks like | Route |
+| -------------------- | -------------------------------------- | ----------------------------------------------------------------- |
+| UUID (`id`) | `8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13` | `.../projects/{project_id}/work-items/{pk}/` — reads and writes |
+| Human (`identifier`) | `PROJ-142` | `/api/v2/workspaces/{slug}/work-items/{identifier}/` — reads only |
+
+The two lookups are separate routes on purpose. The UUID route is the canonical surface for everything, including
+writes. The identifier route is workspace-scoped and read-only, and it exists precisely so a caller holding only
+`PROJ-142` — a chat bot, a commit-hook, an LLM agent — can fetch the work item without first discovering which project
+it belongs to.
+
+## Custom fields
+
+`custom_fields` is the work item's custom property values from its work item type, keyed by the property's `name`.
+Each entry carries the property definition `id`, a human-useful `value`, and a `value_detail` object that keeps the
+underlying record for option and relation properties (`null` for plain scalars). Properties the work item has no value
+for are omitted rather than emitted as `null`.
+
+::: warning `custom_fields` is null on the list endpoint
+Resolving custom properties costs one lookup pass per work item, so the list endpoint deliberately skips it to avoid an
+N+1 across a full page of results. `custom_fields` is populated only on retrieve, retrieve by identifier, create, and
+update. It is `null` on list, and `null` on the archive and unarchive responses too — those return the work item, not
+its property values.
+
+If you are exporting custom property values for many work items, list to get the ids, then fetch the ones you need
+individually. Do not expect `custom_fields` to arrive from a list call.
+:::
+
+The object is empty (`{}`) when the work item is untyped or custom properties are not enabled for the workspace. See
+[work item properties](/api-reference/v2/work-item-properties/list-work-item-properties) for the property definitions
+behind these values.
+
+## Writing: send ids, or send names
+
+Every relation on a write has two accepted inputs — the canonical id field, and a **write-only** human-readable
+parallel. You may send either one, never both.
+
+| Relation | Id input | Human input | Human input accepts |
+| --------- | ------------------- | ----------- | ----------------------------------------------- |
+| State | `state_id` | `state` | The state's name, for example `In Progress` |
+| Type | `type_id` | `type` | The type's name, for example `Bug` |
+| Parent | `parent_id` | `parent` | The parent's identifier, for example `PROJ-118` |
+| Assignees | `assignee_ids` | `assignees` | Member email addresses |
+| Labels | `label_ids` | `labels` | Label names |
+| Estimate | `estimate_point_id` | `estimate` | The estimate point's value, for example `5` |
+
+This exists because the values a caller already holds are almost never UUIDs. An importer has a Jira status string. A
+Slack command has an email address. An LLM agent has the word "Bug". Without the parallel inputs each of those would
+need a list call per field before it could write anything — six lookups to create one work item. With them, the create
+is a single request.
+
+The id fields remain the precise option, and you should prefer them when you already have ids or when a name might be
+ambiguous:
+
+- Names resolve **case-insensitively** and must match exactly one record. A name that matches nothing is a `400`, and
+ so is a name that matches more than one — the error tells you to use the id field instead.
+- Sending both a name and its id (for example `state` and `state_id`) is a `400`. Pick one.
+- The human inputs are **write-only**. Responses always come back in the sparse id shape, so `state` never appears in a
+ response body — `state_id` does.
+
+## Expanding relations
+
+Work items are one of only two resources that support `?expand=`. The accepted values are `state`, `type`, `parent`,
+`assignees`, and `labels`, comma-separated.
+
+Expansion is **separate-key**: the `*_id` field is always present, and the expanded object is _added_ beside it under
+the bare name. `?expand=state` gives you both `state_id` and a `state` object; it never swaps one for the other, so a
+client that reads ids keeps working when someone adds an expand to the request.
+
+```bash
+GET .../work-items/?expand=state,assignees
+```
+
+An unknown expand value is a `400`. Expansion works on the list, both retrieve routes, and the create/update responses.
+See [Expanding relations](/api-reference/v2/expanding-relations).
+
+## Archiving and deleting
+
+Archiving and deleting are different operations with different consequences.
+
+- **Archive** ([archive](/api-reference/v2/work-items/archive-work-item) /
+ [unarchive](/api-reference/v2/work-items/unarchive-work-item)) sets or clears `archived_at`. It is reversible, and it
+ removes the work item from the default query set — archived items stop appearing in list results and in plain detail
+ reads.
+- **Delete** ([delete](/api-reference/v2/work-items/delete-work-item)) is a soft delete that returns `204`. It is not
+ reversible through the API.
+
+Neither one is `PATCH`-able: `archived_at` is read-only, so you cannot archive a work item by patching a timestamp onto
+it.
+
+## Changed from v1
+
+- Relations are ids with explicit names: `state` → **`state_id`**, `parent` → **`parent_id`**, `type` → **`type_id`**,
+ `assignees` → **`assignee_ids`**, `labels` → **`label_ids`**, `created_by` → **`created_by_id`**.
+- Reads no longer return `updated_at`, `updated_by`, `project`, `workspace`, `description_html`,
+ `description_stripped`, `description_binary`, `sort_order`, `completed_at`, `estimate_point`, or `module`.
+- **`identifier`** (`PROJ-142`) and **`custom_fields`** are new on the read shape.
+- Writes accept human-readable parallels (`state`, `type`, `parent`, `assignees`, `labels`, `estimate`) alongside the
+ id fields.
+- `?expand=` is now **separate-key** — it adds an object beside the id instead of replacing the id. The allowed values
+ are `state`, `type`, `parent`, `assignees`, and `labels`; v1's `project` and `module` expansions are gone.
+- Updates are `PATCH` only. `PUT` returns `405`.
+- Lists return a pagination envelope instead of a bare array, and errors are RFC 9457 `application/problem+json`.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
diff --git a/docs/api-reference/v2/work-items/unarchive-work-item.md b/docs/api-reference/v2/work-items/unarchive-work-item.md
new file mode 100644
index 00000000..bb7b8869
--- /dev/null
+++ b/docs/api-reference/v2/work-items/unarchive-work-item.md
@@ -0,0 +1,193 @@
+---
+title: Unarchive a work item
+description: Restore an archived Plane work item with the v2 REST API. Bodyless POST, clearing archived_at, idempotent behavior, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, unarchive work item, restore work item, archived_at null, POST unarchive
+---
+
+# Unarchive a work item
+
+
+ POST
+ /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/unarchive/
+
+
+
+
+
+Restore an archived work item: clear `archived_at` and put it back into the active set, where it shows up in lists and
+plain detail reads again.
+
+This is a **bodyless** `POST` — send no JSON at all. The response is the full work item in its usual read shape with
+`archived_at` back to `null`.
+
+Unlike most detail routes, this one deliberately looks past the archive filter to find the work item — that is the
+whole point. It also means the call is safe to repeat: unarchiving a work item that isn't archived succeeds and changes
+nothing.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item's UUID. This lookup is UUID-only — a `PROJ-142` identifier here returns `404`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+None. The endpoint takes no request body; anything you send is ignored.
+
+
+
+::: tip Safe to retry
+Unarchive resolves archived and active work items alike, so a repeat call is a no-op that returns `200` with
+`archived_at: null`. Archive is the asymmetric one — see
+[Archive a work item](/api-reference/v2/work-items/archive-work-item).
+:::
+
+
+
+### Scopes
+
+`projects.work_items:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | -------------------------------------------------------------------------------- |
+| `400` | `validation_error` | The request could not be processed — for example a `pk` that isn't a valid UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't archive or unarchive this work item. |
+| `404` | `resource_not_found` | No such work item, or it's outside your project or tenant. |
+| `409` | `conflict` | The write collides with a protected-resource constraint. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## What comes back, and what doesn't
+
+The response is the work item's standard read shape with `archived_at` cleared. `custom_fields` is `null` here — the
+archive verbs return the work item, not its custom property values. Fetch
+[the work item](/api-reference/v2/work-items/get-work-item) afterwards if you need them.
+
+Nothing else is restored or changed, because nothing else was touched when the work item was archived: state,
+assignees, labels, dates, comments, and relations all survived the archive untouched.
+
+## Finding archived work items to restore
+
+Archived work items are excluded from [List work items](/api-reference/v2/work-items/list-work-items) and from plain
+detail reads, so the API gives you nothing to browse them with. Record the `id` when you archive — the archive response
+returns it — or pick the work item out of the Plane app's archive view, then unarchive it by UUID. This route is the
+one place an archived work item still resolves.
diff --git a/docs/api-reference/v2/work-items/update-work-item.md b/docs/api-reference/v2/work-items/update-work-item.md
new file mode 100644
index 00000000..9db4c536
--- /dev/null
+++ b/docs/api-reference/v2/work-items/update-work-item.md
@@ -0,0 +1,358 @@
+---
+title: Update a work item
+description: Partially update a Plane work item with the v2 REST API. PATCH semantics, id and human-readable inputs, replacing assignees and labels, workflow rules, scopes, errors, and code examples.
+keywords: plane api v2, update work item, PATCH work item, change state, reassign work item, clear parent, workflow transition denied
+---
+
+# Update a work item
+
+
+
+Update a work item. Every field is optional and the update is partial — send only what changes, and everything you
+leave out stays exactly as it was.
+
+Omitting a field is not the same as sending `null`. Omit `target_date` and the existing due date is preserved; send
+`"target_date": null` and it is cleared.
+
+There is no `PUT` in v2. A `PUT` to this path returns `405 method_not_allowed`.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The project the work item belongs to.
+
+
+
+
+
+The work item's UUID. This lookup is UUID-only — a `PROJ-142` identifier here returns `404`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+New title. Maximum 255 characters.
+
+
+
+
+
+Replacement rich-text body as HTML. Sanitized on the way in; content that can't be sanitized is rejected with a `400`.
+Not part of the read shape, so it won't appear in the response.
+
+
+
+
+
+One of `urgent`, `high`, `medium`, `low`, `none`.
+
+
+
+
+
+Move the work item to another state of the same project. This is the field workflow rules police — see
+[Workflow rules](#workflow-rules-can-reject-a-transition).
+
+
+
+
+
+The target state's **name** instead of its id, for example `Done`. Matched case-insensitively within the project.
+Write-only. Sending both `state` and `state_id` is a `400`.
+
+
+
+
+
+Change the work item type. Send `null` to make the work item untyped.
+
+
+
+
+
+The type's **name** instead of its id, for example `Bug`. Write-only.
+
+
+
+
+
+Re-parent the work item. The parent must be in the same workspace and may be in a different project. Send `null` to
+detach it and make it top-level.
+
+
+
+
+
+The parent's **identifier** instead of its id, for example `PROJ-118`. Write-only.
+
+
+
+
+
+**Replaces** the whole assignee set — it is not additive. Send the complete list you want, `[]` to unassign everyone,
+or omit the field to leave assignees untouched.
+
+
+
+
+
+Member **email addresses** instead of ids. Same replace-the-set semantics. Every address must belong to an active,
+assignable project member. Write-only.
+
+
+
+
+
+**Replaces** the whole label set. Send `[]` to strip all labels, or omit the field to leave them untouched.
+
+
+
+
+
+Label **names** instead of ids. Same replace-the-set semantics. A name that exists at both project and workspace level
+is ambiguous and returns a `400` telling you to use `label_ids`. Write-only.
+
+
+
+
+
+Change the estimate point, from the project's active estimate system. Send `null` to clear the estimate.
+
+
+
+
+
+The estimate point's **value** instead of its id, for example `5`. Write-only.
+
+
+
+
+
+Planned start, or `null` to clear it. Must not be after `target_date`.
+
+
+
+
+
+Planned due date, or `null` to clear it.
+
+
+
+
+
+Your system's identifier for this work item. Maximum 255 characters. Filterable on
+[List work items](/api-reference/v2/work-items/list-work-items), but not returned on reads.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters.
+
+
+
+
+
+
+::: info Fields you cannot patch
+`id`, `identifier`, `sequence_id`, `is_draft`, `archived_at`, `created_at`, and `created_by_id` are read-only — sending
+them has no effect. In particular, archiving is not a `PATCH` on `archived_at`; use
+[Archive a work item](/api-reference/v2/work-items/archive-work-item).
+
+Custom property values go through a separate `custom_fields` object on the request body, keyed by property name — its
+shape follows the work item's type, so it is not declared in the OpenAPI schema. See
+[the type's schema endpoint](/api-reference/v2/work-item-types/get-work-item-type-schema). On a `PATCH` only the
+properties you submit are replaced; untouched properties keep their values and are never required-checked.
+:::
+
+
+
+### Scopes
+
+`projects.work_items:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A bad `priority`; `start_date` after `target_date`; an unresolvable or ambiguous `state`/`type`/`parent`/`assignees`/`labels`/`estimate`; both a name and its `*_id`; or an id from another project or workspace. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't edit this work item. |
+| `403` | `workflow_transition_denied` | A workflow rule forbids the requested state transition. |
+| `404` | `resource_not_found` | No such work item, or it's outside your project or tenant. |
+| `409` | `conflict` | The write collides with a uniqueness or protected-resource constraint. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## Omitted, empty, and null
+
+These three are different, and the difference matters most on the list fields:
+
+| Request body | Result |
+| ------------------------- | ----------------------------------- |
+| `{}` — field omitted | Assignees unchanged. |
+| `{"assignee_ids": []}` | All assignees removed. |
+| `{"assignee_ids": ["…"]}` | Assignees become exactly that list. |
+| `{"parent_id": null}` | Parent detached. |
+
+`assignee_ids` and `label_ids` replace the set rather than adding to it, so to add one assignee you send the existing
+ids plus the new one. Read the work item first if you don't already hold the current list.
+
+## Workflow rules can reject a transition
+
+If the project runs workflow rules, a state change they don't permit returns `403` with `workflow_transition_denied`
+and nothing is written. That is a different situation from `forbidden`, which means your role or token scope can't edit
+the work item at all — branch on the `code`, not the status.
+
+Only a state change can trigger it. Updating a name or a due date never runs the transition check.
diff --git a/docs/api-reference/v2/workspace-features/get-workspace-features.md b/docs/api-reference/v2/workspace-features/get-workspace-features.md
new file mode 100644
index 00000000..48b17bd4
--- /dev/null
+++ b/docs/api-reference/v2/workspace-features/get-workspace-features.md
@@ -0,0 +1,163 @@
+---
+title: Get workspace features
+description: Read a Plane workspace's feature flags with the v2 REST API, including is_work_item_types_enabled — the work item type mode discriminator. Scopes, errors, and code examples.
+keywords: plane api v2, get workspace features, feature flags, is_work_item_types_enabled, work item type mode, GET features
+---
+
+# Get workspace features
+
+
+ GET
+ /api/v2/workspaces/{slug}/features/
+
+
+
+
+
+Read the workspace's feature flags. There is one feature record per workspace, so this returns a single object rather than a list.
+
+Call it during client start-up. `is_work_item_types_enabled` tells you which surface accepts work item type and property writes, and the remaining flags tell you which parts of your UI or sync are worth showing at all.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+None. This endpoint takes no filters, no pagination, and no `?expand=` — passing one is not a supported way to widen the response.
+
+
+
+
+
+### Scopes
+
+`workspaces.features:read`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | -------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this workspace's features. |
+| `404` | `resource_not_found` | No such workspace, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip This is the mode check
+`is_work_item_types_enabled: true` means the workspace manages work item types at the **workspace** level; `false` means each **project** does. Writes sent to the other surface return `409`, with `work_item_types_managed_at_workspace` or `work_item_types_managed_at_project`. Reads are unaffected. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+::: info Cache it, but not forever
+The flags change rarely, so caching the response for the life of a request batch is reasonable. Re-read it whenever a write returns `409 work_item_types_managed_at_workspace` or `work_item_types_managed_at_project` — that code means your cached mode is stale.
+:::
+
+
+
+## Related
+
+- [Update workspace features](/api-reference/v2/workspace-features/update-workspace-features)
+- [Workspace features overview](/api-reference/v2/workspace-features/overview) — every flag, one line each
+- [Work item type modes](/api-reference/v2/work-item-type-modes)
diff --git a/docs/api-reference/v2/workspace-features/overview.md b/docs/api-reference/v2/workspace-features/overview.md
new file mode 100644
index 00000000..2e2d29fd
--- /dev/null
+++ b/docs/api-reference/v2/workspace-features/overview.md
@@ -0,0 +1,159 @@
+---
+title: Workspace features overview
+description: The Plane API v2 workspace feature object. Every feature flag explained, how is_work_item_types_enabled selects the work item type mode, and how to read and toggle features.
+keywords: plane api v2, workspace features, feature flags, is_work_item_types_enabled, work item type mode, is_teams_enabled, is_wiki_enabled, workspace settings
+---
+
+# Workspace features overview
+
+Workspace features are the switches that decide which parts of Plane exist for a workspace. One object, one `GET`, one `PATCH` — but it is the object every capable client reads first, because two of its fields change how the rest of the API behaves.
+
+The important one is `is_work_item_types_enabled`. It is the **mode discriminator for work item types**: it tells you whether types and their properties are owned by the workspace or by each project, and therefore which endpoints will accept your writes. Getting it wrong is a `409`, not a `404` or a `403`.
+
+
+
+
+## The workspace feature object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the workspace's feature record.
+
+- `is_work_item_types_enabled` _boolean_
+
+ Whether work item types are managed at the **workspace** level. `true` means workspace mode — types and properties are defined once for the workspace and imported into projects. `false` means project mode — each project owns its own types. This is the field to read before any type or property write. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+
+- `work_item_type_default_level` _integer_
+
+ The default level applied to work item types in this workspace. The schema constrains it to an integer and declares no enum, so treat any value as legal and leave it as returned unless you are deliberately changing type levels.
+
+- `is_workitem_hierarchy_enabled` _boolean_
+
+ Whether work items can be nested into a parent and child hierarchy in this workspace.
+
+- `is_project_grouping_enabled` _boolean_
+
+ Whether projects can be organized into groups in the workspace.
+
+- `is_teams_enabled` _boolean_
+
+ Whether teamspaces are available. Teamspaces have their own endpoints under the `teamspaces:*` scopes.
+
+- `is_wiki_enabled` _boolean_
+
+ Whether the workspace-level wiki is available, behind the `wiki.pages:*` scopes.
+
+- `is_initiative_enabled` _boolean_
+
+ Whether initiatives — the layer that groups projects and epics toward a larger outcome — are available.
+
+- `is_customer_enabled` _boolean_
+
+ Whether customers and customer requests are available, behind the `customers:*` scopes.
+
+- `is_release_enabled` _boolean_
+
+ Whether releases are available, behind the `releases:*` scopes.
+
+- `is_state_duration_enabled` _boolean_
+
+ Whether Plane records how long work items spend in each state.
+
+- `is_pi_enabled` _boolean_
+
+ Whether Pi, Plane's AI assistant, is available in the workspace.
+
+- `created_at` _string (date-time)_
+
+ When the feature record was created.
+
+::: warning Watch the spelling of the hierarchy flag
+It is `is_workitem_hierarchy_enabled` — no underscore between `work` and `item` — while its neighbor is `is_work_item_types_enabled`. Unknown keys in a `PATCH` body do not toggle anything, so a misspelling looks like a silent no-op. Read the response back and confirm the flag actually moved.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| ------- | ------------------------------------- | ----------------------------- |
+| `GET` | `/api/v2/workspaces/{slug}/features/` | Read the workspace's features |
+| `PATCH` | `/api/v2/workspaces/{slug}/features/` | Toggle workspace features |
+
+There is one feature record per workspace, so the path has no id and there is nothing to create or delete.
+
+## Discovering the work item type mode
+
+A workspace manages work item types in exactly one mode, and this endpoint is how a client finds out which:
+
+```bash
+curl "https://api.plane.so/api/v2/workspaces/my-team/features/" \
+ -H "X-Api-Key: $PLANE_API_KEY"
+```
+
+- `is_work_item_types_enabled: true` → **workspace mode**. Type and property writes go to `/api/v2/workspaces/{slug}/work-item-types/` and friends. The project-level equivalents return `409 work_item_types_managed_at_workspace`.
+- `is_work_item_types_enabled: false` → **project mode**. Those writes go to `/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/`. The workspace-level equivalents return `409 work_item_types_managed_at_project`.
+
+**Reads are unaffected by mode.** A project lists its types in either mode, so a read-only integration never has to branch. Only writes are gated.
+
+Full walkthrough, including how each surface differs: [Work item type modes](/api-reference/v2/work-item-type-modes).
+
+::: tip Read the mode, do not guess it
+Branching on the `409` you got last time bakes yesterday's configuration into your client. Read `is_work_item_types_enabled`, cache it briefly, and re-read it when a write comes back `409` — the mode may have been switched by an admin between your calls.
+:::
+
+## Flags change what other endpoints do
+
+A disabled feature is not a hidden endpoint. Turning a flag off removes the capability from the workspace, so calls that depend on it stop being useful even when they remain routable. Two consequences worth designing for:
+
+- **Toggling `is_work_item_types_enabled` moves the write surface for every client in the workspace**, not just yours. Treat it as an administrative action, not something an integration flips at runtime.
+- **A flag being `true` is necessary, not sufficient.** Features also have to be available on the workspace's plan. Treat a successful call, not the flag alone, as proof a capability is live.
+
+## Scopes
+
+| Operation | Scope |
+| --------- | --------------------------- |
+| `GET` | `workspaces.features:read` |
+| `PATCH` | `workspaces.features:write` |
+
+## Changed from v1
+
+- Flags are now prefixed and suffixed consistently: v1's `project_grouping`, `initiatives`, `teams`, `customers`, `wiki`, and `pi` are `is_project_grouping_enabled`, `is_initiative_enabled`, `is_teams_enabled`, `is_customer_enabled`, `is_wiki_enabled`, and `is_pi_enabled`.
+- New in v2: `is_work_item_types_enabled`, `work_item_type_default_level`, `is_workitem_hierarchy_enabled`, `is_release_enabled`, and `is_state_duration_enabled`.
+- The response now carries `id` and `created_at` alongside the flags.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
+
+## Related
+
+- [Get workspace features](/api-reference/v2/workspace-features/get-workspace-features)
+- [Update workspace features](/api-reference/v2/workspace-features/update-workspace-features)
+- [Work item type modes](/api-reference/v2/work-item-type-modes)
diff --git a/docs/api-reference/v2/workspace-features/update-workspace-features.md b/docs/api-reference/v2/workspace-features/update-workspace-features.md
new file mode 100644
index 00000000..ee09eba3
--- /dev/null
+++ b/docs/api-reference/v2/workspace-features/update-workspace-features.md
@@ -0,0 +1,248 @@
+---
+title: Update workspace features
+description: Toggle a Plane workspace's feature flags with the v2 REST API, including switching the work item type mode. Body parameters, scopes, error codes, and code examples.
+keywords: plane api v2, update workspace features, toggle feature flags, PATCH features, is_work_item_types_enabled, switch work item type mode
+---
+
+# Update workspace features
+
+
+ PATCH
+ /api/v2/workspaces/{slug}/features/
+
+
+
+
+
+Turn workspace features on or off. The update is partial — send only the flags you want to change, and every field you omit keeps its current value. Omitting a field is not the same as sending `null`.
+
+The response is the full feature object after the change, so you never need a follow-up `GET` to confirm what happened.
+
+::: warning Toggling `is_work_item_types_enabled` moves the write surface
+This flag decides whether work item types are managed at the workspace or the project level. Flipping it changes which endpoints accept type and property writes **for every client in the workspace**, and in-flight integrations start getting `409` from the surface they were using. Treat it as an administrative action, not a runtime decision. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional. `id` and `created_at` are read-only — sending them has no effect.
+
+
+
+
+
+Manage work item types at the workspace level (`true`) or per project (`false`). This is the work item type mode switch — read [Work item type modes](/api-reference/v2/work-item-type-modes) before changing it.
+
+
+
+
+
+The default level applied to work item types in this workspace. The schema declares no enum and no bounds, so it accepts any integer.
+
+
+
+
+
+Allow work items to be nested into a parent and child hierarchy. Note the spelling — no underscore between `work` and `item`.
+
+
+
+
+
+Allow projects to be organized into groups in the workspace.
+
+
+
+
+
+Enable teamspaces for the workspace.
+
+
+
+
+
+Enable the workspace-level wiki.
+
+
+
+
+
+Enable initiatives, the layer that groups projects and epics toward a larger outcome.
+
+
+
+
+
+Enable customers and customer requests.
+
+
+
+
+
+Enable releases.
+
+
+
+
+
+Record how long work items spend in each state.
+
+
+
+
+
+Enable Pi, Plane's AI assistant, in the workspace.
+
+
+
+
+
+
+
+
+### Scopes
+
+`workspaces.features:write`
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A flag was sent with a non-boolean value, or `work_item_type_default_level` was not an integer. Includes an `errors[]` array naming the field. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't change this workspace's features. |
+| `404` | `resource_not_found` | No such workspace, or it's outside your tenant. |
+| `409` | `conflict` | The requested toggle conflicts with the workspace's current state and was not applied. Re-read the object before retrying. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Unknown keys are not toggles
+A body key that is not one of the fields above is not a flag and does not change anything. Always compare the response object against what you sent rather than assuming a `200` means your flag moved; `is_workitem_hierarchy_enabled` in particular is easy to mistype as `is_work_item_hierarchy_enabled`.
+:::
+
+
+
+::: tip Confirm the mode after switching it
+After a `PATCH` that changes `is_work_item_types_enabled`, read the value straight from the response and use it to pick your write surface. Anything you cached before this call is now wrong. Then follow [Work item type modes](/api-reference/v2/work-item-type-modes) — workspace mode also changes how types reach a project, since projects import workspace types instead of defining their own.
+:::
+
+## Related
+
+- [Get workspace features](/api-reference/v2/workspace-features/get-workspace-features)
+- [Workspace features overview](/api-reference/v2/workspace-features/overview)
+- [Work item type modes](/api-reference/v2/work-item-type-modes)
diff --git a/docs/api-reference/v2/workspace-work-item-properties/create-workspace-work-item-property.md b/docs/api-reference/v2/workspace-work-item-properties/create-workspace-work-item-property.md
new file mode 100644
index 00000000..9fe5c567
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-properties/create-workspace-work-item-property.md
@@ -0,0 +1,329 @@
+---
+title: Create a workspace work item property
+description: Define a workspace-level custom property with the Plane v2 REST API. Property types, relation types, options, contexts, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create work item property, workspace custom property, property_type, relation_type, POST work item properties, work item type modes
+---
+
+# Create a workspace work item property
+
+
+ POST
+ /api/v2/workspaces/{slug}/work-item-properties/
+
+
+
+
+
+Define a custom property once for the whole workspace. `display_name` and `property_type` are the only
+required fields; everything else refines how the value is captured.
+
+::: warning A new property is not live yet
+Creating a property adds it to the workspace catalog. It appears on work items only once a **context** binds
+it to projects and work item types. Follow this call with
+[Create a property context](/api-reference/v2/work-item-property-contexts/create-property-context), or the
+property will sit in the catalog unused.
+:::
+
+::: warning Wrong mode is a 409, not a 404
+If the workspace manages work item types at the **project** level, this route returns `409` with the code
+`work_item_types_managed_at_project`. Create the property on the project-level resource instead. See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+The label shown wherever the property is rendered. Maximum 255 characters.
+
+
+
+
+
+What kind of value the property holds. Choose carefully — it governs what `relation_type`, `options`,
+`settings`, and `validation_rules` mean.
+
+- `TEXT` — free-form text
+- `DATETIME` — a date and time
+- `DECIMAL` — a number
+- `BOOLEAN` — true or false
+- `OPTION` — a choice from a defined list
+- `RELATION` — a pointer to another record; pair it with `relation_type`
+- `URL` — a link
+- `EMAIL` — an email address
+- `FILE` — a file
+- `FORMULA` — a formula-backed value
+
+A value outside this list is a `400 validation_error`.
+
+
+
+
+
+For a `RELATION` property, what it points at. One of `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`. Leave it out
+— or send `null` — for every other property type.
+
+
+
+
+
+Free-form explanation of what the property captures. Nullable.
+
+
+
+
+
+Whether a value must be supplied for this property.
+
+
+
+
+
+Whether the property accepts more than one value.
+
+
+
+
+
+Whether the property is in use. Send `false` to define a property without putting it into circulation yet.
+
+
+
+
+
+The value applied when none is supplied. Send an array even for a single value.
+
+
+
+
+
+Seed the choices for an `OPTION` property in the same call. Write-only: the created options come back on the
+read-only `options` array of the response, and are managed afterwards through
+[Property options](/api-reference/v2/workspace-work-item-property-options/overview), which is also where the
+fields an option accepts are documented.
+
+
+
+
+
+Type-specific configuration. Its shape depends on `property_type`.
+
+
+
+
+
+Type-specific validation configuration.
+
+
+
+
+
+Your system's identifier for this property, for sync and import correlation. Maximum 255 characters, nullable.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters, nullable.
+
+
+
+
+
+## After creating
+
+1. Bind it — [Create a property context](/api-reference/v2/work-item-property-contexts/create-property-context)
+ decides which projects and work item types see the property.
+2. For `OPTION` properties, refine the choices with
+ [Property options](/api-reference/v2/workspace-work-item-property-options/overview).
+3. To put the property on a workspace-level work item type directly, use
+ [Attach properties to a workspace type](/api-reference/v2/workspace-work-item-type-properties/attach-workspace-type-property).
diff --git a/docs/api-reference/v2/workspace-work-item-properties/delete-workspace-work-item-property.md b/docs/api-reference/v2/workspace-work-item-properties/delete-workspace-work-item-property.md
new file mode 100644
index 00000000..af5b3a47
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-properties/delete-workspace-work-item-property.md
@@ -0,0 +1,155 @@
+---
+title: Delete a workspace work item property
+description: Delete a workspace-level custom property with the Plane v2 REST API. What deletion takes with it, mode conflicts, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete work item property, DELETE workspace custom property, retire property, work item type modes, 409 conflict
+---
+
+# Delete a workspace work item property
+
+
+
+Remove a property from the workspace catalog. The delete is soft and the response is `204` with an empty body.
+
+This is the wide blast radius option: the property leaves every project and work item type it reached, unlike
+[detaching it from a single type](/api-reference/v2/workspace-work-item-type-properties/detach-workspace-type-property),
+which only affects that one type.
+
+::: tip Retire before you delete
+If the property has been collecting values, `PATCH` it with `is_active: false` first. The definition stays
+addressable while you migrate, and you can reverse the decision with a single field. Deleting is the move for
+a property created by mistake.
+:::
+
+::: warning Wrong mode is a 409, not a 404
+If the workspace manages work item types at the **project** level, this route returns `409` with the code
+`work_item_types_managed_at_project`. Delete the property on the project-level resource instead. See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The property's id.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | -------------------------------------------------------------------------- |
+| `400` | `validation_error` | The request failed validation — most often a `pk` that isn't a valid UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write workspace work item properties. |
+| `404` | `resource_not_found` | No such workspace or property, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types at the project level. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Deleting the property takes its scoping with it
+A property's [contexts](/api-reference/v2/work-item-property-contexts/overview) and
+[options](/api-reference/v2/workspace-work-item-property-options/overview) are mounted underneath it. Once the
+property is gone, those sub-resources are no longer addressable — `GET`s under the deleted `property_id`
+return `404 resource_not_found`.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-properties/get-workspace-work-item-property.md b/docs/api-reference/v2/workspace-work-item-properties/get-workspace-work-item-property.md
new file mode 100644
index 00000000..cb1b00a7
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-properties/get-workspace-work-item-property.md
@@ -0,0 +1,179 @@
+---
+title: Get a workspace work item property
+description: Retrieve one workspace-level custom property with the Plane v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get work item property, workspace custom property, GET property by id, property_type
+---
+
+# Get a workspace work item property
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-properties/{pk}/
+
+
+
+
+
+Retrieve a single workspace property by id. Use it to read a property's full configuration — its
+`property_type`, whether it is required or multi-valued, and the `options` behind an `OPTION` property —
+before rendering a field or before sending a `PATCH`.
+
+Reads are unaffected by the workspace's
+[work item type mode](/api-reference/v2/work-item-type-modes). A property outside your tenant returns `404`,
+never `403` — existence is never leaked.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The property's id.
+
+
+
+
+
+::: info This says nothing about where the property applies
+The property object does not carry its scoping. To find out which projects and work item types see it, list
+its contexts with
+[List property contexts](/api-reference/v2/work-item-property-contexts/list-property-contexts). A property with
+no contexts is defined but not applied anywhere.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-properties/list-workspace-work-item-properties.md b/docs/api-reference/v2/workspace-work-item-properties/list-workspace-work-item-properties.md
new file mode 100644
index 00000000..117cb214
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-properties/list-workspace-work-item-properties.md
@@ -0,0 +1,306 @@
+---
+title: List workspace work item properties
+description: List the custom properties defined at the workspace level with the Plane v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list workspace work item properties, custom properties, custom fields, GET work item properties, pagination
+---
+
+# List workspace work item properties
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-properties/
+
+
+
+
+
+Return the workspace's custom property catalog as a paginated list. This is how you resolve a property name to
+the `id` you need when creating a context or attaching the property to a work item type, and how you build a
+picker that stays in sync with what the workspace has defined.
+
+The list is the catalog, not what is live: it includes properties that have no
+[context](/api-reference/v2/work-item-property-contexts/overview) yet and therefore do not appear on any work
+item. Reads here are unaffected by the workspace's
+[work item type mode](/api-reference/v2/work-item-type-modes).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+This list takes no filters or search term — the whole catalog is returned, page by page. Check your spelling on
+`order_by` and `paginate`: neither is validated, so an unrecognized `order_by` silently falls back to the default
+ordering and anything other than `paginate=cursor` silently uses offset pagination.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` , `-sort_order` — the display order Plane uses for the property list. This is the order you
+ want when rendering a form; note that `sort_order` itself is not returned on the property object.
+- `created_at` , `-created_at` — when each property was created
+- `id` , `-id`
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the
+response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead
+of `next` and `total_count`. Omit it for the default offset envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from
+the response.
+
+
+
+
+
+::: tip Match on id, not label
+`display_name` is a label humans edit. Resolve it to `id` once and store the id — that is what contexts, type
+attachments, and property values refer to.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-properties/overview.md b/docs/api-reference/v2/workspace-work-item-properties/overview.md
new file mode 100644
index 00000000..66922fff
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-properties/overview.md
@@ -0,0 +1,212 @@
+---
+title: Workspace work item properties overview
+description: The Plane API v2 workspace work item property object. Property types, relation types, options, and how contexts decide where a workspace property applies.
+keywords: plane api v2, workspace work item properties, custom properties, custom fields, property_type, relation_type, property contexts
+---
+
+# Workspace work item properties overview
+
+A workspace work item property is a custom field defined **once for the whole workspace** — `Severity`,
+`Customer impact`, `Rollout date` — instead of being redefined project by project. It is the workspace-level
+counterpart to project work item properties.
+
+```text
+/api/v2/workspaces/{slug}/work-item-properties/
+```
+
+::: warning Defining a property is not the same as applying it
+Creating a property here adds it to the workspace catalog. It does not put it on any work item. A property
+reaches work items through a **context**, which binds it to projects and work item types. A workspace property
+with no context is defined but not yet applied anywhere. See
+[Property contexts](/api-reference/v2/work-item-property-contexts/overview).
+:::
+
+::: warning Writes require workspace mode
+A workspace manages work item types in exactly one mode: project-level or workspace-level. Creating, updating,
+or deleting on this route while the workspace is in **project** mode returns `409` with the code
+`work_item_types_managed_at_project` — the capability lives on the project surface instead. Reads are
+unaffected by mode. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+
+## The property object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the property. This is the id you use in a context's payload and when attaching the
+ property to a workspace work item type.
+
+- `name` _string_
+
+ Read-only companion to `display_name`. You never set it directly.
+
+- `display_name` _string_
+
+ The label shown wherever the property is rendered. This is the field you write. Maximum 255 characters.
+
+- `description` _string_
+
+ Free-form explanation of what the property captures. May be `null`.
+
+- `property_type` _string_
+
+ What kind of value the property holds. One of `TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `OPTION`,
+ `RELATION`, `URL`, `EMAIL`, `FILE`, or `FORMULA`. This is the one decision worth getting right up front —
+ it determines what the other fields mean.
+
+- `relation_type` _string_
+
+ For a `RELATION` property, what the property points at. One of `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`.
+ `null` for every other property type.
+
+- `is_required` _boolean_
+
+ Whether a value must be supplied for this property.
+
+- `is_multi` _boolean_
+
+ Whether the property accepts more than one value.
+
+- `is_active` _boolean_
+
+ Whether the property is in use. Set it to `false` to retire a property without deleting it.
+
+- `default_value` _array of string_
+
+ The value applied when none is supplied. Always an array, even when `is_multi` is `false`.
+
+- `options` _array of any_
+
+ The choices for an `OPTION` property. Read-only on this object — manage the list through
+ [Property options](/api-reference/v2/workspace-work-item-property-options/overview), or seed it at create
+ time with the write-only `options` body field.
+
+- `settings` _any_
+
+ Type-specific configuration. Its shape depends on `property_type`.
+
+- `validation_rules` _any_
+
+ Type-specific validation configuration.
+
+- `logo_props` _any_
+
+ Presentation data for the property's icon.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map a property to a field in another system
+ and find it again later. Either may be `null`.
+
+- `created_at` _string (date-time)_
+
+ When the property was created.
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | ------------------------------------------------------ | --------------------------- |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-properties/` | List workspace properties |
+| `POST` | `/api/v2/workspaces/{slug}/work-item-properties/` | Create a workspace property |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-properties/{pk}/` | Get a workspace property |
+| `PATCH` | `/api/v2/workspaces/{slug}/work-item-properties/{pk}/` | Update a workspace property |
+| `DELETE` | `/api/v2/workspaces/{slug}/work-item-properties/{pk}/` | Delete a workspace property |
+
+## Property types
+
+`property_type` decides how the value is captured and which of the other fields matter.
+
+| `property_type` | What it holds |
+| --------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `TEXT` | Free-form text |
+| `DATETIME` | A date and time |
+| `DECIMAL` | A number |
+| `BOOLEAN` | True or false |
+| `OPTION` | A choice from a defined list — see [Property options](/api-reference/v2/workspace-work-item-property-options/overview) |
+| `RELATION` | A pointer to another record — pair it with `relation_type` |
+| `URL` | A link |
+| `EMAIL` | An email address |
+| `FILE` | A file |
+| `FORMULA` | A formula-backed value |
+
+`relation_type` is only meaningful for `RELATION`, and takes `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`. On
+every other property type it is `null`.
+
+## Contexts decide where a property applies
+
+The catalog answers "what properties exist in this workspace". A **context** answers "where does this one show
+up". Each context binds the property to a set of projects and work item types — named explicitly through
+`project_ids` and `issue_type_ids`, or opened up with `applies_to_all_projects` and
+`applies_to_all_work_item_types` — and carries its own `is_required`, `is_multi`, `default_value`, `options`,
+and `sort_order`.
+
+So the usual sequence is:
+
+1. `POST /work-item-properties/` — define the property once.
+2. `POST /work-item-properties/{property_id}/contexts/` — say where it applies.
+
+Skip step 2 and the property exists but appears nowhere. Full detail lives in
+[Property contexts](/api-reference/v2/work-item-property-contexts/overview).
+
+## Related resources
+
+- [Property options](/api-reference/v2/workspace-work-item-property-options/overview) — the choices behind an
+ `OPTION` property.
+- [Workspace type properties](/api-reference/v2/workspace-work-item-type-properties/overview) — attach an
+ existing property to a workspace-level work item type.
+- [Work item type modes](/api-reference/v2/work-item-type-modes) — which surface your workspace writes to.
diff --git a/docs/api-reference/v2/workspace-work-item-properties/update-workspace-work-item-property.md b/docs/api-reference/v2/workspace-work-item-properties/update-workspace-work-item-property.md
new file mode 100644
index 00000000..3928a255
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-properties/update-workspace-work-item-property.md
@@ -0,0 +1,297 @@
+---
+title: Update a workspace work item property
+description: Partially update a workspace-level custom property with the Plane v2 REST API. PATCH semantics, body parameters, mode conflicts, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update work item property, PATCH work item property, workspace custom property, property_type, work item type modes
+---
+
+# Update a workspace work item property
+
+
+
+Change a workspace property in place. `PATCH` is partial: send only the fields you want to change, and
+everything you omit is left untouched. Omitting a field is not the same as sending `null` — `null` clears a
+nullable field, omission changes nothing.
+
+There is no `PUT` on this resource; sending one returns `405 method_not_allowed`.
+
+Every project and work item type the property reaches sees the change — that is the point of defining it at
+the workspace level.
+
+::: warning Wrong mode is a 409, not a 404
+If the workspace manages work item types at the **project** level, this route returns `409` with the code
+`work_item_types_managed_at_project`. Update the property on the project-level resource instead. See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The property's id.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional. The body itself is optional too, though an empty `PATCH` does nothing useful.
+
+
+
+
+
+The label shown wherever the property is rendered. Maximum 255 characters. Renaming is safe for integrations
+that key on `id`.
+
+
+
+
+
+One of `TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `OPTION`, `RELATION`, `URL`, `EMAIL`, `FILE`, or `FORMULA`.
+Changing the type of a property that is already collecting values reinterprets what those values mean —
+prefer retiring the old property with `is_active: false` and creating a new one.
+
+
+
+
+
+For a `RELATION` property, what it points at. One of `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`. Nullable.
+
+
+
+
+
+Free-form explanation of what the property captures. Nullable — send `null` to clear it.
+
+
+
+
+
+Whether a value must be supplied for this property.
+
+
+
+
+
+Whether the property accepts more than one value.
+
+
+
+
+
+Set to `false` to retire the property without deleting it, and back to `true` to bring it into circulation
+again.
+
+
+
+
+
+The value applied when none is supplied. Always an array. Send `[]` to clear it.
+
+
+
+
+
+Write-only. For day-to-day option management — adding one choice, renaming another — use
+[Property options](/api-reference/v2/workspace-work-item-property-options/overview), which addresses each
+option by id.
+
+
+
+
+
+Type-specific configuration. Its shape depends on `property_type`.
+
+
+
+
+
+Type-specific validation configuration.
+
+
+
+
+
+Your system's identifier for this property. Maximum 255 characters, nullable.
+
+
+
+
+
+The system `external_id` came from. Maximum 255 characters, nullable.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | ------------------------------------------------------------------------ |
+| `400` | `validation_error` | An enum value outside the allowed set, or a field over its length limit. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write workspace work item properties. |
+| `404` | `resource_not_found` | No such workspace or property, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types at the project level. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Scoping is edited elsewhere
+`PATCH` here changes the property's definition, not where it applies. To narrow or widen the projects and work
+item types that see it, update its
+[property contexts](/api-reference/v2/work-item-property-contexts/overview) instead.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-property-options/create-workspace-property-option.md b/docs/api-reference/v2/workspace-work-item-property-options/create-workspace-property-option.md
new file mode 100644
index 00000000..48d89199
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-property-options/create-workspace-property-option.md
@@ -0,0 +1,211 @@
+---
+title: Create a workspace property option
+description: Add a selectable choice to a workspace-level OPTION property with the Plane v2 REST API. Body parameters, the single-default rule, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create property option, workspace work item property, OPTION property, dropdown choice, is_default, POST options
+---
+
+# Create a workspace property option
+
+
+ POST
+ /api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/
+
+
+
+
+
+Add a choice to a workspace-level `OPTION` property. The new option is appended to the end of the property's list and becomes immediately selectable on work items across the workspace.
+
+The property must have `property_type: "OPTION"`. Posting an option to a property of any other type returns `400 validation_error` — the option list is meaningless for a `TEXT` or `DECIMAL` property.
+
+::: warning Workspace mode only
+This write requires the workspace to manage work item types at the workspace level. In project mode it returns `409 work_item_types_managed_at_project` — add the option through the project-level endpoint instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level `OPTION` property to add the choice to. A project-scoped property id is a `404` here — this path never crosses into a project. See [Workspace work item properties](/api-reference/v2/workspace-work-item-properties/overview).
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+The choice as it is displayed. Maximum 255 characters.
+
+
+
+
+
+Free-form text explaining when to pick this choice.
+
+
+
+
+
+Make this the property's default choice. At most one option per property can be the default, and there is no automatic hand-off: if another option already has it, this request is rejected with `400 validation_error`. Clear the current default with a `PATCH` first.
+
+
+
+
+
+Your system's identifier for this option, for sync and import correlation. Maximum 255 characters.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters.
+
+
+
+
+
+`sort_order` is not accepted. Plane appends the new option after the current last one, so create options in the order you want them displayed.
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | Missing `name`, a field over 255 characters, `is_default` when the property already has a default, or a property whose `property_type` is not `OPTION`. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write this workspace's properties. |
+| `404` | `resource_not_found` | No such workspace or workspace-level property, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types at the project level. Use the project-level options endpoint. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Seeding a whole option set
+Create the options in display order in one pass, then set the default with a follow-up `PATCH`. That keeps every create request free of `is_default` and avoids tripping the one-default rule when a request is retried.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-property-options/delete-workspace-property-option.md b/docs/api-reference/v2/workspace-work-item-property-options/delete-workspace-property-option.md
new file mode 100644
index 00000000..ad0e2bf9
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-property-options/delete-workspace-property-option.md
@@ -0,0 +1,151 @@
+---
+title: Delete a workspace property option
+description: Remove a selectable choice from a workspace-level OPTION property with the Plane v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete property option, workspace work item property, OPTION property, dropdown choice, DELETE option
+---
+
+# Delete a workspace property option
+
+
+
+Remove a choice from a workspace-level `OPTION` property. The delete is soft — the option stops being selectable and disappears from reads, and the request returns `204` with an empty body.
+
+Deleting an option is not the same as renaming it. If the choice is being reworded, `PATCH` it instead: [Update a workspace property option](/api-reference/v2/workspace-work-item-property-options/update-workspace-property-option) keeps the option's `id`, so work items already holding it keep their value.
+
+::: warning Workspace mode only
+This write requires the workspace to manage work item types at the workspace level. In project mode it returns `409 work_item_types_managed_at_project` — delete the option through the project-level endpoint instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property the option belongs to. See [Workspace work item properties](/api-reference/v2/workspace-work-item-properties/overview).
+
+
+
+
+
+The option to delete. It is looked up within the property, so an option id under the wrong `property_id` is a `404`.
+
+
+
+
+
+Deleting the property's default option leaves the property with no default. Nothing promotes another option in its place — set a new default explicitly if the property needs one.
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write this workspace's properties. |
+| `404` | `resource_not_found` | No such workspace, workspace-level property, or option — or it's outside your tenant. Already-deleted options are `404` too. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types at the project level. Use the project-level options endpoint. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Work items that already hold the option
+The delete cascades: property values pointing at this option are soft-deleted with it, so work items that carried the choice lose it. That cascade runs in the background, so a read taken immediately after the `204` can still show the old value for a moment.
+
+If you are rewording a choice rather than retiring it, [update the option](/api-reference/v2/workspace-work-item-property-options/update-workspace-property-option) instead — a rename keeps the `id` and every work item keeps its value.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-property-options/get-workspace-property-option.md b/docs/api-reference/v2/workspace-work-item-property-options/get-workspace-property-option.md
new file mode 100644
index 00000000..eac301b2
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-property-options/get-workspace-property-option.md
@@ -0,0 +1,151 @@
+---
+title: Get a workspace property option
+description: Retrieve a single selectable option on a workspace-level OPTION property with the Plane v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get property option, workspace work item property, OPTION property, dropdown choice, GET option
+---
+
+# Get a workspace property option
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/{pk}/
+
+
+
+
+
+Retrieve one option on a workspace-level `OPTION` property. Use it to resolve a stored option id back to a display name, or to confirm an option still exists before writing it onto a work item.
+
+Reads work in either work item type mode — only writes are mode-gated. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property the option belongs to. See [Workspace work item properties](/api-reference/v2/workspace-work-item-properties/overview).
+
+
+
+
+
+The option to retrieve.
+
+
+
+
+
+The option is looked up within the property, so a valid option id under the wrong `property_id` is a `404` rather than a match. Deleted options are gone from this endpoint too — a soft-deleted option returns `404`.
+
+
+
+::: tip Fetching every option at once
+Reading the property returns its `options` inline, so one property read is cheaper than a fan-out of option reads when you need the whole set. Reach for this endpoint when you hold a single option id and want just that row.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-property-options/list-workspace-property-options.md b/docs/api-reference/v2/workspace-work-item-property-options/list-workspace-property-options.md
new file mode 100644
index 00000000..1a852b97
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-property-options/list-workspace-property-options.md
@@ -0,0 +1,251 @@
+---
+title: List workspace property options
+description: List the selectable options on a workspace-level OPTION property with the Plane v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list property options, workspace work item property, OPTION property, dropdown choices, pagination, GET options
+---
+
+# List workspace property options
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/
+
+
+
+
+
+Return the selectable choices on a workspace-level `OPTION` property as a paginated list. This is how you resolve an option name to the `id` you store when setting the property on a work item, and how you build a picker that stays in sync with the property definition.
+
+Results are ordered by `sort_order` by default, which is the order the option list is meant to be displayed in.
+
+Reads work in either work item type mode. Only writes are mode-gated — see [Work item type modes](/api-reference/v2/work-item-type-modes).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property whose options you want — see [Workspace work item properties](/api-reference/v2/workspace-work-item-properties/overview).
+
+This endpoint filters options by property rather than looking the property up, so an id that isn't a workspace-level property in this workspace — including a project-scoped property id — comes back as an empty list, not a `404`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+There are no filters on this resource. A property's option list is small and self-contained, so fetch it whole and match on `id` in your own code.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` , `-sort_order` — the property's own display order. This is the default.
+- `created_at` , `-created_at` — when each option was added
+- `id` , `-id`
+
+A value outside this list is ignored and the default `sort_order` ordering is used, so a typo shows up as an unexpected order rather than an error.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most properties have fewer than 20 options, so one page is usually the whole list.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. Omit it for the default offset envelope.
+
+Cursor pagination needs a unique, monotonic sort key, and `sort_order` is neither. Pair it with `order_by=created_at` or `order_by=id`; a bare `?paginate=cursor` falls back to the default `sort_order` ordering and is rejected with `ordering_not_cursor_eligible`.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+::: info An empty list is a real answer
+Three different situations all return `200` with an empty `data` array: an `OPTION` property that has no options yet, a property of some other `property_type`, and a `property_id` that isn't a workspace-level property at all. When an empty list is surprising, read the property itself and check both that it exists at the workspace level and that its `property_type` is `OPTION`.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-property-options/overview.md b/docs/api-reference/v2/workspace-work-item-property-options/overview.md
new file mode 100644
index 00000000..a0eab47f
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-property-options/overview.md
@@ -0,0 +1,125 @@
+---
+title: Workspace property options overview
+description: The Plane API v2 workspace-level work item property option object. Attributes, the single default option, server-assigned sort order, and the mode rule that governs writes.
+keywords: plane api v2, workspace property options, work item property options, OPTION property, dropdown choices, is_default, sort_order
+---
+
+# Workspace property options overview
+
+An option is one selectable choice on a workspace-level custom property whose `property_type` is `OPTION` — the entries a person picks from when filling that property in on a work item.
+
+Options hang off the property, not off a project. The path carries no project segment:
+
+```text
+/api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/
+```
+
+A property of any other type has no options. Adding one to a `TEXT`, `BOOLEAN`, `DECIMAL` or other non-`OPTION` property is rejected with `400 validation_error`, so create the property with `property_type: "OPTION"` first — see [Workspace work item properties](/api-reference/v2/workspace-work-item-properties/overview).
+
+
+
+
+## The property option object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the option. This is the value you store when you set the property on a work item, so treat it as the stable handle — `name` is only a label.
+
+- `name` _string_
+
+ The choice as it is displayed. Maximum 255 characters.
+
+- `description` _string_
+
+ Free-form text explaining when to pick this choice.
+
+- `is_default` _boolean_
+
+ Whether this is the property's default choice. At most one option per property can carry it.
+
+- `sort_order` _number_
+
+ Position of the option within the property's list. Lower values come first, and this is the order the list endpoint returns by default. Assigned by Plane — it is not a writable field.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map an option to a record in another system and find it again later.
+
+::: tip Store the id, not the name
+Renaming an option keeps its `id`, so integrations that matched on `id` keep working and integrations that matched on `name` break. Resolve names to ids once, then reference ids.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | ---------------------------------------------------------------------------- | ------------- |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/` | List options |
+| `POST` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/` | Create option |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/{pk}/` | Get option |
+| `PATCH` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/{pk}/` | Update option |
+| `DELETE` | `/api/v2/workspaces/{slug}/work-item-properties/{property_id}/options/{pk}/` | Delete option |
+
+## Scopes
+
+| Operation | Scope |
+| ------------------------- | --------------------------------------- |
+| `GET` (list, retrieve) | `workspaces.work_item_properties:read` |
+| `POST`, `PATCH`, `DELETE` | `workspaces.work_item_properties:write` |
+
+Options are covered by the workspace property scopes — there is no separate option scope to request, and the project-level scope root (`projects.work_item_properties:*`) does not grant access here.
+
+## Writes are workspace-mode only
+
+::: warning Wrong mode is a 409, not a 404
+A workspace manages work item types and their properties in exactly one mode. These endpoints are the workspace-mode surface, so `POST`, `PATCH`, and `DELETE` here require workspace mode. Call them while the workspace is in project mode and you get `409 work_item_types_managed_at_project` — the capability exists, it just lives on the project-level surface.
+
+Reads are unaffected by mode: `GET` works in either mode. See [Work item type modes](/api-reference/v2/work-item-type-modes) for how to check which mode a workspace is in, and [Property options (project)](/api-reference/v2/work-item-property-options/overview) for the project-level equivalent.
+:::
+
+## Only one option can be the default
+
+Setting `is_default: true` while another option on the same property already has it is rejected with `400 validation_error`. There is no automatic hand-off. To move the default, clear it on the current holder first:
+
+1. `PATCH` the current default with `{"is_default": false}`.
+2. `PATCH` (or `POST`) the new option with `{"is_default": true}`.
+
+## Sort order is assigned, not sent
+
+`sort_order` is not part of the request body on either create or update. A new option is appended after the existing ones, so the creation order is the display order. If you need a specific order, create the options in that order.
+
+## Options are also inlined on the property
+
+A property read returns its `options` array inline, so fetching the property is enough when you only need the choices for rendering. Reach for these endpoints when you need to page through a long option list, or to add, rename, or remove a choice. Which work items and projects the property applies to is decided by its contexts — see [Property contexts](/api-reference/v2/work-item-property-contexts/overview).
+
+## Changed from v1
+
+- v1 had no workspace-level surface. Options existed only under `/projects/{project_id}/work-item-properties/{property_id}/options/`; a workspace-level property's options now live at the workspace path with no project segment.
+- The scope is `workspaces.work_item_properties:read` / `:write`. v1 used `projects.work_item_property_options:*`.
+- The read shape is trimmed to `id`, `name`, `description`, `is_default`, `sort_order`, `external_id`, and `external_source`. `workspace`, `project`, `property`, `parent`, `logo_props`, `is_active`, and the `created_*` / `updated_*` audit fields are no longer returned.
+- `is_active` and `parent` are no longer accepted on write.
+
+See [Migrating from v1](/api-reference/v2/migrating-from-v1) for the full list.
diff --git a/docs/api-reference/v2/workspace-work-item-property-options/update-workspace-property-option.md b/docs/api-reference/v2/workspace-work-item-property-options/update-workspace-property-option.md
new file mode 100644
index 00000000..b9448ca1
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-property-options/update-workspace-property-option.md
@@ -0,0 +1,218 @@
+---
+title: Update a workspace property option
+description: Rename a workspace-level property option or move the default with the Plane v2 REST API. PATCH body parameters, the single-default rule, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update property option, workspace work item property, OPTION property, is_default, PATCH option
+---
+
+# Update a workspace property option
+
+
+
+Change one option on a workspace-level `OPTION` property — rename it, reword its description, or move the property's default onto it.
+
+The update is partial: fields you omit are left untouched, and omitting a field is not the same as sending `null`. Renaming an option keeps its `id`, so every work item already holding the option keeps its value.
+
+::: warning Workspace mode only
+This write requires the workspace to manage work item types at the workspace level. In project mode it returns `409 work_item_types_managed_at_project` — update the option through the project-level endpoint instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The workspace-level property the option belongs to. See [Workspace work item properties](/api-reference/v2/workspace-work-item-properties/overview).
+
+
+
+
+
+The option to update. It is looked up within the property, so an option id under the wrong `property_id` is a `404`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional. Send only what you are changing.
+
+
+
+
+
+The choice as it is displayed. Maximum 255 characters.
+
+
+
+
+
+Free-form text explaining when to pick this choice.
+
+
+
+
+
+Make this the property's default choice, or send `false` to clear it. At most one option per property can be the default: setting it while a different option already holds it returns `400 validation_error`, so clear the current default first and then set the new one.
+
+
+
+
+
+Your system's identifier for this option, for sync and import correlation. Maximum 255 characters.
+
+
+
+
+
+The system `external_id` came from, for example `github` or `jira`. Maximum 255 characters.
+
+
+
+
+
+`sort_order` is not accepted here — an option's position is fixed at creation time and cannot be moved through the API.
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | A field over 255 characters, or `is_default: true` when another option on the property is already the default. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write this workspace's properties. |
+| `404` | `resource_not_found` | No such workspace, workspace-level property, or option — or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types at the project level. Use the project-level options endpoint. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+## Moving the default
+
+There is no automatic hand-off, so promoting a new default takes two requests:
+
+1. `PATCH` the option that currently has `is_default: true` with `{"is_default": false}`.
+2. `PATCH` the new option with `{"is_default": true}`.
+
+Run it the other way round and the promotion is the request that fails, leaving the existing default untouched. Nothing is half-applied either way.
diff --git a/docs/api-reference/v2/workspace-work-item-type-properties/attach-workspace-type-property.md b/docs/api-reference/v2/workspace-work-item-type-properties/attach-workspace-type-property.md
new file mode 100644
index 00000000..1faa5369
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-type-properties/attach-workspace-type-property.md
@@ -0,0 +1,189 @@
+---
+title: Attach properties to a workspace type
+description: Attach existing workspace custom properties to a workspace-level work item type with the Plane v2 REST API. Body parameters, mode conflicts, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, attach property to type, workspace work item type properties, POST type properties, work item type modes, 409 conflict
+---
+
+# Attach properties to a workspace type
+
+
+ POST
+ /api/v2/workspaces/{slug}/work-item-types/{type_id}/properties/
+
+
+
+
+
+Attach one or more **existing** workspace properties to a workspace-level work item type. This does not create
+properties — create those first with
+[Create a workspace property](/api-reference/v2/workspace-work-item-properties/create-workspace-work-item-property),
+then send their ids here.
+
+Because the same property can be attached to many types, this is how you reuse one `Severity` definition
+across `Bug`, `Incident`, and `Escalation` instead of maintaining three near-identical fields.
+
+::: warning Wrong mode is a 409, not a 404
+If the workspace manages work item types at the **project** level, this route returns `409` with the code
+`work_item_types_managed_at_project`. Attach through the project-level route instead. See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The workspace work item type to attach the properties to.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Ids of properties to attach, taken from the workspace property catalog. Send the whole set you want in one
+call rather than one request per property.
+
+Every id must already exist in this workspace. An unknown id — including one that belongs to another workspace
+— fails the request with `400 validation_error`; nothing is attached, and a cross-tenant id is never silently
+linked.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | ----------------------------------------------------------------------------- |
+| `400` | `validation_error` | `properties` missing, not an array, or holding an id that doesn't exist here. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write workspace work item types. |
+| `404` | `resource_not_found` | No such workspace or type, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types at the project level. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info The response is ids, not objects
+`201` returns only the `properties` array. To render the attached properties, follow with
+[List properties on a workspace type](/api-reference/v2/workspace-work-item-type-properties/list-workspace-type-properties)
+or fetch them individually.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-type-properties/detach-workspace-type-property.md b/docs/api-reference/v2/workspace-work-item-type-properties/detach-workspace-type-property.md
new file mode 100644
index 00000000..8b093c93
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-type-properties/detach-workspace-type-property.md
@@ -0,0 +1,157 @@
+---
+title: Detach a property from a workspace type
+description: Detach a custom property from a workspace-level work item type with the Plane v2 REST API. Why detaching never deletes, mode conflicts, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, detach property, remove property from work item type, DELETE type property, work item type modes, 409 conflict
+---
+
+# Detach a property from a workspace type
+
+
+
+Remove a property from a workspace-level work item type. The type stops carrying the property; nothing else
+changes.
+
+::: info Detaching is not deleting
+The property stays in the workspace catalog and stays attached to every other type that uses it. To remove it
+from the workspace entirely, call
+[Delete a workspace property](/api-reference/v2/workspace-work-item-properties/delete-workspace-work-item-property)
+instead.
+:::
+
+::: warning Wrong mode is a 409, not a 404
+If the workspace manages work item types at the **project** level, this route returns `409` with the code
+`work_item_types_managed_at_project`. Detach through the project-level route instead. See
+[Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The workspace work item type to detach the property from.
+
+
+
+
+
+The property's id. A property that is not attached to this type returns `404`.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | -------------------------------------------------------------------------- |
+| `400` | `validation_error` | The request failed validation — most often a `pk` that isn't a valid UUID. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write workspace work item types. |
+| `404` | `resource_not_found` | No such workspace, type, or property — or the property isn't on this type. |
+| `409` | `work_item_types_managed_at_project` | This workspace manages work item types at the project level. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Re-attaching is a POST away
+Detaching is reversible: send the same id back through
+[Attach properties to a workspace type](/api-reference/v2/workspace-work-item-type-properties/attach-workspace-type-property).
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-type-properties/get-workspace-type-property.md b/docs/api-reference/v2/workspace-work-item-type-properties/get-workspace-type-property.md
new file mode 100644
index 00000000..c25a5b7c
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-type-properties/get-workspace-type-property.md
@@ -0,0 +1,188 @@
+---
+title: Get a property on a workspace type
+description: Retrieve one custom property attached to a workspace-level work item type with the Plane v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get type property, workspace work item type, custom property, GET property by id
+---
+
+# Get a property on a workspace type
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-types/{type_id}/properties/{pk}/
+
+
+
+
+
+Retrieve one property as it is attached to a workspace-level work item type. Use it to read a single
+property's configuration — its `property_type`, whether it is required, its options — without pulling the
+whole list.
+
+This route also answers the membership question: a `200` means the property is attached to this type, and a
+`404` means it is not. A property that exists in the workspace but is not on this type returns `404`, the same
+as an id that does not exist at all.
+
+Reads are unaffected by the workspace's
+[work item type mode](/api-reference/v2/work-item-type-modes).
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The workspace work item type the property is attached to.
+
+
+
+
+
+The property's id.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | -------------------------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read workspace work item types. |
+| `404` | `resource_not_found` | No such workspace, type, or property — or the property isn't on this type. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Editing happens on the catalog
+This route is read-only. To change a property's label, requiredness, or options, `PATCH` it on the workspace
+catalog with
+[Update a workspace property](/api-reference/v2/workspace-work-item-properties/update-workspace-work-item-property).
+Every type the property is attached to sees the change.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-type-properties/list-workspace-type-properties.md b/docs/api-reference/v2/workspace-work-item-type-properties/list-workspace-type-properties.md
new file mode 100644
index 00000000..2e658e29
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-type-properties/list-workspace-type-properties.md
@@ -0,0 +1,297 @@
+---
+title: List properties on a workspace type
+description: List the custom properties attached to a workspace-level work item type with the Plane v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list type properties, workspace work item type, custom properties, GET type properties, pagination
+---
+
+# List properties on a workspace type
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-types/{type_id}/properties/
+
+
+
+
+
+Return the custom properties attached to one workspace-level work item type, as a paginated list. Reach for it
+when you are building a form for that type, or checking what a type already carries before attaching more.
+
+Reads are unaffected by the workspace's
+[work item type mode](/api-reference/v2/work-item-type-modes) — this route answers even when the workspace
+manages types at the project level.
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is
+`my-team`.
+
+
+
+
+
+The workspace work item type whose properties you want. Get it from
+[List workspace work item types](/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types).
+
+
+
+
+
+
+
+
+### Query Parameters
+
+This list takes no filters or search term — a type's property set is small and returned whole. Check your spelling on
+`order_by` and `paginate`: neither is validated, so an unrecognized `order_by` silently falls back to the default
+ordering and anything other than `paginate=cursor` silently uses offset pagination.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `sort_order` , `-sort_order` — the display order Plane uses for the property list. This is the order you
+ want when rendering a form; note that `sort_order` itself is not returned on the property object.
+- `created_at` , `-created_at` — when each property was created
+- `id` , `-id`
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most types carry fewer than 20 properties, so one page is usually the
+whole set.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the
+response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead
+of `next` and `total_count`. Omit it for the default offset envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from
+the response.
+
+
+
+
+
+::: tip Listing is not the property catalog
+This returns only the properties attached to one type. To see everything defined in the workspace, including
+properties not yet on any type, use
+[List workspace properties](/api-reference/v2/workspace-work-item-properties/list-workspace-work-item-properties).
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-type-properties/overview.md b/docs/api-reference/v2/workspace-work-item-type-properties/overview.md
new file mode 100644
index 00000000..39142bcf
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-type-properties/overview.md
@@ -0,0 +1,198 @@
+---
+title: Workspace type properties overview
+description: The Plane API v2 workspace type-property resource. Which properties a workspace-level work item type shows, the attach/detach model, and the work-item-type mode rule.
+keywords: plane api v2, workspace work item type properties, attach property to type, detach property, workspace work item types, custom properties
+---
+
+# Workspace type properties overview
+
+A **workspace work item type** decides which custom properties appear on the work items that use it. This
+resource is the link between the two: it lists the properties currently on a type, and it attaches and
+detaches them.
+
+```text
+/api/v2/workspaces/{slug}/work-item-types/{type_id}/properties/
+```
+
+The properties themselves are not created here. They live in the workspace property catalog at
+[`/work-item-properties/`](/api-reference/v2/workspace-work-item-properties/overview), and one property can be
+attached to as many types as you like. That is the point of the split: define `Severity` once for the
+workspace, then put it on `Bug`, `Incident`, and `Escalation`.
+
+Because the catalog is shared, **detaching never deletes**. `DELETE` on this route removes the property from
+the type and leaves the property itself — and every other type that uses it — untouched. To remove a property
+from the workspace entirely, use
+[Delete a workspace property](/api-reference/v2/workspace-work-item-properties/delete-workspace-work-item-property).
+
+::: warning Writes here require workspace mode
+A workspace manages work item types in exactly one mode: project-level or workspace-level. Attaching or
+detaching on this route while the workspace is in **project** mode returns `409` with the code
+`work_item_types_managed_at_project` — the capability exists, it just lives on the project surface. Reads are
+unaffected by mode. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+
+## The property object
+
+Reads on this resource return the full property object, the same shape the workspace property catalog returns.
+Every field is read-only here — this route only changes which properties a type carries, never what a property
+is.
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the property. This is the id you send in `properties` to attach it, and the `pk` you
+ use to detach it.
+
+- `name` _string_
+
+ Read-only companion to `display_name`. You never set it directly.
+
+- `display_name` _string_
+
+ The label shown wherever the property is rendered.
+
+- `description` _string_
+
+ Free-form explanation of what the property captures. May be `null`.
+
+- `property_type` _string_
+
+ What kind of value the property holds. One of `TEXT`, `DATETIME`, `DECIMAL`, `BOOLEAN`, `OPTION`,
+ `RELATION`, `URL`, `EMAIL`, `FILE`, or `FORMULA`.
+
+- `relation_type` _string_
+
+ For a `RELATION` property, what it points at — one of `ISSUE`, `USER`, `RELEASE`, or `RICH_TEXT`. `null` for
+ every other property type.
+
+- `is_required` _boolean_
+
+ Whether a value must be supplied for this property.
+
+- `is_multi` _boolean_
+
+ Whether the property accepts more than one value.
+
+- `is_active` _boolean_
+
+ Whether the property is in use. A property can be retired by setting this to `false` on the catalog
+ resource, without deleting it.
+
+- `default_value` _array of string_
+
+ The value applied when none is supplied. Always an array, even when `is_multi` is `false`.
+
+- `options` _array of any_
+
+ The choices for an `OPTION` property. Manage them through
+ [Property options](/api-reference/v2/workspace-work-item-property-options/overview).
+
+- `settings` _any_
+
+ Type-specific configuration. Its shape depends on `property_type`.
+
+- `validation_rules` _any_
+
+ Type-specific validation configuration.
+
+- `logo_props` _any_
+
+ Presentation data for the property's icon.
+
+- `external_id` , `external_source` _string_
+
+ Correlation fields for sync and import. Together they let you map a property to a record in another system
+ and find it again later. Either may be `null`.
+
+- `created_at` _string (date-time)_
+
+ When the property was created.
+
+::: tip There is no separate link object
+The response is the property, not a join record. If you need to know whether a given property is on a type,
+`GET …/properties/{pk}/` — a `404` means it is not attached, and a `200` means it is.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | ---------------------------------------------------------------------- | ------------------------------- |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-types/{type_id}/properties/` | List the properties on a type |
+| `POST` | `/api/v2/workspaces/{slug}/work-item-types/{type_id}/properties/` | Attach existing properties |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-types/{type_id}/properties/{pk}/` | Get one property on a type |
+| `DELETE` | `/api/v2/workspaces/{slug}/work-item-types/{type_id}/properties/{pk}/` | Detach a property from the type |
+
+There is no `POST` that creates a property here and no `PATCH` that edits one. Attaching takes ids of
+properties that already exist, and editing a property is done on the catalog resource.
+
+## Attaching properties
+
+`POST` takes a single field, `properties`, holding the ids of properties to attach:
+
+```json
+{ "properties": ["a7e3f1d0-5c92-4b68-8f31-2d4a6b9e0c15", "3fb6c0d8-4e21-49a7-b5c3-90ad72e14f6b"] }
+```
+
+The response echoes the ids that are now on the type. An id that does not exist in this workspace is rejected
+with `400 validation_error` — a cross-tenant id is never silently ignored.
+
+## Where a property applies
+
+Attaching a property to a type is a statement about the type, which lives at the workspace level. When you
+need narrower scoping — this property on these projects only, or across a chosen set of types — model it with
+[Property contexts](/api-reference/v2/work-item-property-contexts/overview) instead.
diff --git a/docs/api-reference/v2/workspace-work-item-types/create-workspace-work-item-type.md b/docs/api-reference/v2/workspace-work-item-types/create-workspace-work-item-type.md
new file mode 100644
index 00000000..c786db72
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-types/create-workspace-work-item-type.md
@@ -0,0 +1,206 @@
+---
+title: Create a workspace work item type
+description: Create a workspace-level work item type in Plane with the v2 REST API. Body parameters, workspace mode requirement, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, create workspace work item type, issue type, workspace mode, POST work item types
+---
+
+# Create a workspace work item type
+
+
+ POST
+ /api/v2/workspaces/{slug}/work-item-types/
+
+
+
+
+
+Define a work item type for the whole workspace. The type becomes available to projects working from the workspace list, so one call gives every team the same `Bug` or `Incident` rather than each project inventing its own.
+
+::: warning Workspace mode required
+This write only succeeds while the workspace manages work item types at the workspace level. In project mode it returns `409` with code `work_item_types_managed_at_project` — create the type on the project endpoint instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+
+
+
+
+Display name for the type, for example `Incident`. Maximum 255 characters. Reusing a name that already exists in the workspace returns `409`.
+
+
+
+
+
+What this type is for. It is shown next to the type wherever someone picks it, so write it for the person choosing.
+
+
+
+
+
+Whether the type can be selected. Send `false` to create a type that exists but stays out of pickers until you are ready to roll it out.
+
+
+
+
+
+Your system's identifier for this type, for sync and import correlation. Maximum 255 characters, and accepts `null`. Write-only — it is not returned on read, so keep your own mapping to the Plane `id`.
+
+
+
+
+
+The system `external_id` came from, for example `jira`. Maximum 255 characters, and accepts `null`. Write-only, like `external_id` — an `external_id` is only unique within its source, so record both.
+
+
+
+
+
+`is_default`, `is_epic`, `level`, and `logo_props` are read-only. A type created here is a standard work item type; promote it to the workspace default with [Mark a type as default](/api-reference/v2/workspace-work-item-types/mark-default-workspace-work-item-type).
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | ----------------------------------------------------------------------------------------- |
+| `400` | `validation_error` | `name` is missing, empty, or longer than 255 characters. See `errors[]`. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write workspace types. |
+| `404` | `resource_not_found` | No such workspace, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | The workspace manages types at the project level. Use the project endpoint. |
+| `409` | `conflict` | The type can't be created as requested — most often a name already in use. Read `detail`. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Next steps
+A new type starts with no custom properties. [Attach the properties it should collect](/api-reference/v2/workspace-work-item-type-properties/attach-workspace-type-property), then bring it into the projects that need it with [Import work item types](/api-reference/v2/work-item-types/import-work-item-types), passing the `id` returned above.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-types/delete-workspace-work-item-type.md b/docs/api-reference/v2/workspace-work-item-types/delete-workspace-work-item-type.md
new file mode 100644
index 00000000..fbe8839d
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-types/delete-workspace-work-item-type.md
@@ -0,0 +1,146 @@
+---
+title: Delete a workspace work item type
+description: Delete a workspace-level work item type in Plane with the v2 REST API. Workspace mode requirement, 204 response, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, delete workspace work item type, remove issue type, workspace mode, DELETE work item type
+---
+
+# Delete a workspace work item type
+
+
+
+Remove a work item type from the workspace. Because the type is shared, the removal is felt by every project working from the workspace list — check what still uses it before you call this.
+
+A successful delete returns `204` with an empty body. There is nothing to parse; branch on the status code.
+
+::: warning Workspace mode required
+This write only succeeds while the workspace manages work item types at the workspace level. In project mode it returns `409` with code `work_item_types_managed_at_project`. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The id of the work item type to delete.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | -------------------------------------------------------------------------------- |
+| `400` | `validation_error` | The request can't be processed as sent. See `errors[]`. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write workspace types. |
+| `404` | `resource_not_found` | No such type or workspace, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | The workspace manages types at the project level. Use the project endpoint. |
+| `409` | `conflict` | The type can't be deleted in its current state — the response `detail` says why. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info A repeat delete returns 404
+Delete is not idempotent in its response: the second call for the same id returns `404 resource_not_found`, because the type is already gone. Treat `404` on retry as success rather than as an error worth alerting on.
+:::
+
+
+
+::: tip Consider deactivating instead
+If the type is only being phased out, `is_active: false` via [Update](/api-reference/v2/workspace-work-item-types/update-workspace-work-item-type) takes it out of pickers while keeping it resolvable for the work items that already carry it — and you can undo it.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-types/get-workspace-work-item-type.md b/docs/api-reference/v2/workspace-work-item-types/get-workspace-work-item-type.md
new file mode 100644
index 00000000..8583d884
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-types/get-workspace-work-item-type.md
@@ -0,0 +1,150 @@
+---
+title: Get a workspace work item type
+description: Retrieve a single workspace-level work item type from Plane with the v2 REST API. Path parameters, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, get workspace work item type, issue type by id, workspace mode, GET work item type
+---
+
+# Get a workspace work item type
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-types/{pk}/
+
+
+
+
+
+Retrieve one workspace-level type by id. Reach for this when you already hold a type id — from a work item's `type_id`, a webhook payload, or a stored mapping — and need its current name, description, or active flag.
+
+::: info Reads work in either mode
+Retrieval is unaffected by [work item type modes](/api-reference/v2/work-item-type-modes). A workspace type stays readable here even when the workspace manages types at the project level; only writes are mode-gated.
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The id of the work item type to retrieve. A project-level type id returns `404` here — this endpoint only resolves types defined on the workspace.
+
+
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | -------------------- | ----------------------------------------------------------- |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't read this workspace's types. |
+| `404` | `resource_not_found` | No such type or workspace, or it's outside your tenant. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info No expansion, and no external ids
+Work item types don't accept `?expand=` — the response is the flat object shown here. `external_id` and `external_source` are write-only and never come back, so resolve a type by the Plane `id` you stored at create time.
+:::
+
+::: info Existence is never leaked
+A type outside your tenant returns `404`, not `403`. If a cached id starts returning `404`, re-read the workspace's set with [List workspace work item types](/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types) rather than retrying.
+:::
+
+
diff --git a/docs/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types.md b/docs/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types.md
new file mode 100644
index 00000000..7276c60f
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types.md
@@ -0,0 +1,249 @@
+---
+title: List workspace work item types
+description: List the workspace-level work item types in a Plane workspace with the v2 REST API. Ordering, pagination, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, list workspace work item types, issue types, workspace mode, pagination, GET work item types
+---
+
+# List workspace work item types
+
+
+ GET
+ /api/v2/workspaces/{slug}/work-item-types/
+
+
+
+
+
+Return the workspace's work item types as a paginated list. This is where you resolve a type name such as `Bug` to the `id` you need — to set `type_id` on a work item, to attach properties to the type, or to hand to a project [import](/api-reference/v2/work-item-types/import-work-item-types).
+
+There is no project segment in the path: these are the types defined once for the whole workspace.
+
+::: info Reads work in either mode
+Listing is unaffected by [work item type modes](/api-reference/v2/work-item-type-modes). You can read the workspace list even while the workspace manages types at the project level — only writes are mode-gated.
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+
+
+
+### Query Parameters
+
+This endpoint takes ordering and pagination only — there are no filter or search parameters. A workspace's type list is short by design, so read a page and match on `name` or `id` in your own code.
+
+
+
+
+
+### Ordering
+
+
+
+
+
+Field to sort by. Prefix with `-` for descending.
+
+- `name` , `-name` — alphabetical, which is what you usually want for a picker
+- `level` , `-level` — the type's level in the work item hierarchy
+- `created_at` , `-created_at` — when each type was added
+- `id` , `-id`
+
+A value outside this list is not rejected — it quietly falls back to the default ordering. Check your spelling: a typo surfaces as a differently-sorted page, not as an error.
+
+
+
+
+
+
+
+
+### Pagination
+
+
+
+
+
+Page size. Defaults to 50, maximum 200. Most workspaces define fewer than 20 types, so one page is usually the whole set.
+
+
+
+
+
+Number of rows to skip from the start of the result set. Maximum 10000. Read the `next` value from the response rather than computing offsets yourself.
+
+
+
+
+
+Set to `cursor` to opt into the COUNT-free keyset envelope, which returns `next_cursor` and `has_more` instead of `next` and `total_count`. Omit it for the default offset envelope.
+
+
+
+
+
+Defaults to `true`. Set to `false` to skip the `COUNT(*)` behind `total_count`; the field is then omitted from the response.
+
+
+
+
+
+::: tip Inactive types still list
+`is_active: false` retires a type from pickers, but it keeps appearing in this list. Filter on `is_active` in your own code before you show the list to a user, and keep inactive types resolvable so historic work items still render with a name.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-types/mark-default-workspace-work-item-type.md b/docs/api-reference/v2/workspace-work-item-types/mark-default-workspace-work-item-type.md
new file mode 100644
index 00000000..1fc64508
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-types/mark-default-workspace-work-item-type.md
@@ -0,0 +1,160 @@
+---
+title: Mark a workspace work item type as default
+description: Promote a workspace-level work item type to the workspace default in Plane with the v2 REST API. Workspace mode requirement, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, mark default work item type, workspace default issue type, workspace mode, POST mark-default
+---
+
+# Mark a workspace work item type as default
+
+
+ POST
+ /api/v2/workspaces/{slug}/work-item-types/{pk}/mark-default/
+
+
+
+
+
+Promote a type to the workspace default — the type a work item gets when none is supplied. Exactly one type holds the flag, so marking this one clears it on the type that held it before.
+
+`is_default` is read-only on create and update, which is why this is its own call. **Send no body.**
+
+::: warning Workspace mode required
+This write only succeeds while the workspace manages work item types at the workspace level. In project mode it returns `409` with code `work_item_types_managed_at_project` — set the default on the project endpoint instead. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The id of the work item type to make the default.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+None. The type is identified entirely by `pk`; send an empty request.
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | --------------------------------------------------------------------------------- |
+| `400` | `validation_error` | The type can't be made default as requested. See `errors[]`. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write workspace types. |
+| `404` | `resource_not_found` | No such type or workspace, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | The workspace manages types at the project level. Use the project endpoint. |
+| `409` | `conflict` | The default can't be moved in the current state — the response `detail` says why. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: info Confirm the swap in one read
+The response is the promoted type, not the pair. If you need to show the change, re-read the set with [List workspace work item types](/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types) — the previous default now reports `is_default: false`.
+:::
diff --git a/docs/api-reference/v2/workspace-work-item-types/overview.md b/docs/api-reference/v2/workspace-work-item-types/overview.md
new file mode 100644
index 00000000..6222110e
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-types/overview.md
@@ -0,0 +1,146 @@
+---
+title: Workspace work item types overview
+description: The Plane API v2 workspace work item type object. Attributes, workspace mode versus project mode, defaults, and how projects pick up workspace-level types.
+keywords: plane api v2, workspace work item types, issue types, workspace mode, work item type modes, shared taxonomy
+---
+
+# Workspace work item types overview
+
+A work item type classifies work — `Bug`, `Task`, `Feature`, `Incident` — and carries the set of custom properties that work items of that type collect.
+
+When a workspace manages types at the **workspace level**, a type is defined once for the whole workspace and made available to projects, instead of being recreated project by project. One `Bug` definition, one set of properties, every project reading from the same list.
+
+These endpoints hang off the workspace and have no project segment:
+
+```text
+/api/v2/workspaces/{slug}/work-item-types/
+```
+
+::: warning Writes require workspace mode
+A workspace manages work item types in exactly one mode: project-level or workspace-level. Creating, updating, deleting, or marking a default here works only while the workspace is in **workspace mode**. In project mode the same calls return `409` with code `work_item_types_managed_at_project` — the capability exists, it just lives on the project endpoints. **Reads are unaffected by mode.** See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+
+## The work item type object
+
+### Attributes
+
+- `id` _string (uuid)_
+
+ Unique identifier for the type. This is the id you send as `type_id` on a work item, and the id you pass when importing types into a project.
+
+- `name` _string_
+
+ Display name, for example `Bug`. Maximum 255 characters.
+
+- `description` _string_
+
+ What this type is for. Shown next to the type wherever it is picked in Plane, so it is worth writing for the people choosing, not for your integration.
+
+- `is_active` _boolean_
+
+ Whether the type can currently be selected. Deactivating a type retires it from pickers without deleting it or touching the work items already using it.
+
+- `is_default` _boolean_
+
+ Whether this is the workspace's default type. Read-only on create and update — change it with [Mark a type as default](/api-reference/v2/workspace-work-item-types/mark-default-workspace-work-item-type).
+
+- `is_epic` _boolean_
+
+ Whether the type is an epic type. Read-only, and there is no body parameter for it: every type you create through this API is a standard work item type.
+
+- `level` _number_
+
+ The type's level in the work item hierarchy. Read-only, and one of the values you can sort by with `order_by`.
+
+- `logo_props` _any_
+
+ Free-form icon metadata Plane uses to render the type's badge. Managed in the app — read-only over the API.
+
+- `created_at` _string (date-time)_
+
+ When the type was created.
+
+::: info `external_id` is write-only
+You can send `external_id` and `external_source` when creating or updating a type so an import can correlate it with a record in your own system, but neither field comes back on read. Keep your own mapping from your id to the Plane `id`.
+:::
+
+
+
+## Endpoints
+
+| Method | Path | Description |
+| -------- | -------------------------------------------------------------- | ---------------------- |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-types/` | List types |
+| `POST` | `/api/v2/workspaces/{slug}/work-item-types/` | Create a type |
+| `GET` | `/api/v2/workspaces/{slug}/work-item-types/{pk}/` | Get a type |
+| `PATCH` | `/api/v2/workspaces/{slug}/work-item-types/{pk}/` | Update a type |
+| `DELETE` | `/api/v2/workspaces/{slug}/work-item-types/{pk}/` | Delete a type |
+| `POST` | `/api/v2/workspaces/{slug}/work-item-types/{pk}/mark-default/` | Mark a type as default |
+
+There is no `enable` and no `import` action at the workspace level. Both are project-level operations — see [Work item types (project)](/api-reference/v2/work-item-types/overview).
+
+## Workspace mode versus project mode
+
+| | Project mode | Workspace mode |
+| -------------------------- | -------------------------------------------------------------- | ------------------------------------------ |
+| Where a type is defined | Once per project, on `/projects/{project_id}/work-item-types/` | Once per workspace, on `/work-item-types/` |
+| Same type in ten projects | Ten separate records to create and keep in step | One record the projects share |
+| Renaming or editing a type | Repeat the edit in every project | Edit once |
+| Cross-project reporting | Group by name and hope the names match | Group by the type `id` |
+| Per-project variation | A project can shape its own types freely | Projects work from the workspace list |
+
+Choose **workspace mode** when the same taxonomy should hold across many projects — a support org where `Incident` must mean the same thing everywhere, or a company that reports on bug counts across every team. Choose **project mode** when teams are genuinely different and each one should be free to invent its own categories.
+
+The trade-off is consistency against autonomy, and it is a workspace-wide decision: both surfaces exist, but only one of them accepts writes at a time.
+
+## How a project gets workspace types
+
+Defining a type here makes it available to the workspace; a project surfaces it once the project is working from the workspace list. To bring a specific set of workspace types into a project, send their ids to the project-level import endpoint:
+
+```text
+POST /api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/import/
+```
+
+Collect the ids from [List workspace work item types](/api-reference/v2/workspace-work-item-types/list-workspace-work-item-types), then see [Import work item types](/api-reference/v2/work-item-types/import-work-item-types) for the request shape.
+
+## Defaults
+
+Exactly one type is the workspace default — the one a work item gets when no type is supplied. `is_default` is read-only on create and update, so promoting a different type is its own call:
+
+```text
+POST /api/v2/workspaces/{slug}/work-item-types/{pk}/mark-default/
+```
+
+Marking a type as default clears the flag on the type that held it.
+
+## Retiring a type
+
+Deactivating (`is_active: false`) and deleting are different moves:
+
+- **Deactivate** when the type should stop appearing in pickers but existing work items keep their classification. Reversible — send `is_active: true` to bring it back.
+- **Delete** when the type should be gone. `DELETE` returns `204` with an empty body. A type that can't be removed in its current state returns `409`; read the `detail` for the reason.
diff --git a/docs/api-reference/v2/workspace-work-item-types/update-workspace-work-item-type.md b/docs/api-reference/v2/workspace-work-item-types/update-workspace-work-item-type.md
new file mode 100644
index 00000000..4e2f96d3
--- /dev/null
+++ b/docs/api-reference/v2/workspace-work-item-types/update-workspace-work-item-type.md
@@ -0,0 +1,210 @@
+---
+title: Update a workspace work item type
+description: Update a workspace-level work item type in Plane with the v2 REST API. Partial PATCH body, workspace mode requirement, OAuth scopes, error codes, and code examples.
+keywords: plane api v2, update workspace work item type, rename issue type, deactivate type, workspace mode, PATCH work item type
+---
+
+# Update a workspace work item type
+
+
+
+Rename a type, rewrite its description, or retire it with `is_active: false`. The edit lands once and every project working from the workspace list sees it — that is the point of managing types at the workspace level.
+
+The update is partial: omitted fields are left untouched, and omitting a field is not the same as sending `null`. There is no `PUT`.
+
+::: warning Workspace mode required
+This write only succeeds while the workspace manages work item types at the workspace level. In project mode it returns `409` with code `work_item_types_managed_at_project`. See [Work item type modes](/api-reference/v2/work-item-type-modes).
+:::
+
+
+
+### Path Parameters
+
+
+
+
+
+The workspace slug. It appears in your Plane URLs — in `https://app.plane.so/my-team/projects/`, the slug is `my-team`.
+
+
+
+
+
+The id of the work item type to update.
+
+
+
+
+
+
+
+
+### Body Parameters
+
+Every field is optional — send only what changes.
+
+
+
+
+
+New display name. Maximum 255 characters. Renaming is safe for integrations that key on `id`; anything matching on the old name breaks, which is a reason to key on `id`.
+
+
+
+
+
+New description of what the type is for.
+
+
+
+
+
+Send `false` to retire the type from pickers without deleting it — work items already classified with it keep their type. Send `true` to bring it back.
+
+
+
+
+
+Your system's identifier for this type. Maximum 255 characters. Send `null` to clear it. Write-only — it is not returned on read.
+
+
+
+
+
+The system `external_id` came from, for example `jira`. Maximum 255 characters. Send `null` to clear it. Write-only, like `external_id`.
+
+
+
+
+
+`is_default`, `is_epic`, `level`, and `logo_props` are read-only — sending them has no effect. To change which type is the default, call [Mark a type as default](/api-reference/v2/workspace-work-item-types/mark-default-workspace-work-item-type).
+
+
+
+### Errors
+
+| Status | Code | Cause |
+| ------ | ------------------------------------ | ---------------------------------------------------------------------------- |
+| `400` | `validation_error` | An invalid value — for example a `name` over 255 characters. See `errors[]`. |
+| `401` | `unauthorized` | Missing or invalid credentials. |
+| `403` | `forbidden` | Your role or token scope can't write workspace types. |
+| `404` | `resource_not_found` | No such type or workspace, or it's outside your tenant. |
+| `409` | `work_item_types_managed_at_project` | The workspace manages types at the project level. Use the project endpoint. |
+| `409` | `conflict` | The change collides with another type — most often a name already in use. |
+| `429` | `rate_limited` | Throttled. Honor the `Retry-After` header before retrying. |
+
+
+
+::: tip Deactivate before you delete
+`is_active: false` is the reversible move: the type stops appearing in pickers, existing work items keep their classification, and you can undo it with one call. Reach for [Delete](/api-reference/v2/workspace-work-item-types/delete-workspace-work-item-type) only when the type should be gone for good.
+:::
diff --git a/docs/dev-tools/build-plane-app/choose-token-flow.md b/docs/dev-tools/build-plane-app/choose-token-flow.md
index ecd5b029..d7b9ce68 100644
--- a/docs/dev-tools/build-plane-app/choose-token-flow.md
+++ b/docs/dev-tools/build-plane-app/choose-token-flow.md
@@ -217,4 +217,4 @@ GET https://api.plane.so/api/v1/workspaces/{workspace_slug}/projects/
Authorization: Bearer YOUR_TOKEN
```
-See the [API Reference](/api-reference/introduction) for available endpoints.
+See the [API Reference](/api-reference/v1/introduction) for available endpoints.
diff --git a/docs/dev-tools/build-plane-app/examples.md b/docs/dev-tools/build-plane-app/examples.md
index f20fc3da..776c6855 100644
--- a/docs/dev-tools/build-plane-app/examples.md
+++ b/docs/dev-tools/build-plane-app/examples.md
@@ -236,7 +236,7 @@ if __name__ == "__main__":
## Next Steps
- [Build an Agent](/dev-tools/agents/overview) - Create AI agents that respond to @mentions
-- [API Reference](/api-reference/introduction) - Explore the full Plane API
+- [API Reference](/api-reference/v1/introduction) - Explore the full Plane API
- [Webhook Events](/dev-tools/intro-webhooks) - All webhook event types
- [Example: PRD Agent](https://github.com/makeplane/prd-agent) - Complete agent implementation
diff --git a/docs/dev-tools/build-plane-app/overview.md b/docs/dev-tools/build-plane-app/overview.md
index a1e8600d..92ed6597 100644
--- a/docs/dev-tools/build-plane-app/overview.md
+++ b/docs/dev-tools/build-plane-app/overview.md
@@ -125,6 +125,6 @@ See full working implementations:
## Quick links
-- [API Reference](/api-reference/introduction) - Explore all available endpoints
+- [API Reference](/api-reference/v1/introduction) - Explore all available endpoints
- [Build an Agent](/dev-tools/agents/overview) - Create AI agents for Plane
- [Webhook Events](/dev-tools/intro-webhooks) - All webhook event types
diff --git a/docs/index.md b/docs/index.md
index c10dbb1a..ca7d2497 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -18,7 +18,7 @@ aside: false