List 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
slug:requiredstringThe workspace slug. It appears in your Plane URLs — in https://app.plane.so/my-team/projects/, the slug is my-team.
project_id:requiredstring (uuid)The project whose modules you want.
Query Parameters
Filters
status:optionalstringReturn 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.
lead_id:optionalstring (uuid)Return only modules led by this user. Match is exact on the module's lead_id.
external_id:optionalstringReturn only modules carrying this external identifier. Pair it with external_source to resolve a record you imported from another system.
external_source:optionalstringReturn only modules that came from this system, for example github or jira.
Search
search:optionalstringFree-text match on the module name.
Ordering
order_by:optionalstringField to sort by. Prefix with - for descending.
sort_order,-sort_ordercreated_at,-created_atid,-id
Defaults to sort_order.
Pagination
per_page:optionalintegerPage size. Defaults to 50, maximum 200.
offset:optionalintegerNumber of rows to skip from the start of the result set. Maximum 10000 — go deeper with cursor pagination.
paginate:optionalstringSet to cursor to opt into the COUNT-free keyset envelope, then follow next_cursor with ?cursor=.
count:optionalbooleanSet to false to skip the count query and omit total_count from the offset envelope. Defaults to true.
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.
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. |
curl -X GET \
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/modules/?status=in-progress&per_page=50" \
-H "X-Api-Key: $PLANE_API_KEY"import requests
response = requests.get(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/modules/",
headers={"X-Api-Key": "your-api-key"},
params={"status": "in-progress", "per_page": 50},
)
print(response.json())const params = new URLSearchParams({ status: "in-progress", per_page: "50" });
const response = await fetch(
`https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/modules/?${params}`,
{
headers: { "X-Api-Key": "your-api-key" },
}
);
const data = await response.json();{
"data": [
{
"id": "7c1f3d90-2a64-4e58-9b0d-3fa1c7e28b45",
"name": "Billing revamp",
"description": "Rework subscription billing end to end.",
"status": "in-progress",
"start_date": "2026-01-05",
"target_date": "2026-02-27",
"lead_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"member_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430", "9d3e1f27-8b4c-4a06-95f1-2c7ea45b0d18"],
"sort_order": 65535.0,
"logo_props": {},
"external_id": null,
"external_source": null,
"archived_at": null,
"created_at": "2026-01-14T09:22:41.478363Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
},
{
"id": "3ea77b18-5c9d-4f02-8a61-b0d4c9e5137a",
"name": "Search relevance",
"description": "",
"status": "in-progress",
"start_date": null,
"target_date": null,
"lead_id": null,
"member_ids": [],
"sort_order": 98302.5,
"logo_props": {},
"external_id": null,
"external_source": null,
"archived_at": null,
"created_at": "2026-01-19T14:03:07.201884Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
}
],
"next": null,
"previous": null,
"total_count": 2,
"pagination": { "style": "offset" }
}{
"data": [
{
"id": "7c1f3d90-2a64-4e58-9b0d-3fa1c7e28b45",
"name": "Billing revamp",
"description": "Rework subscription billing end to end.",
"status": "in-progress",
"start_date": "2026-01-05",
"target_date": "2026-02-27",
"lead_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"member_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430", "9d3e1f27-8b4c-4a06-95f1-2c7ea45b0d18"],
"sort_order": 65535.0,
"logo_props": {},
"external_id": null,
"external_source": null,
"archived_at": null,
"created_at": "2026-01-14T09:22:41.478363Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
}
],
"next_cursor": "b3A9MTcx",
"has_more": true,
"pagination": { "style": "cursor" }
}
