Download OpenAPI specification:
Programmatic access to the projects, tasks, comments and members you already work with in Projoodle. It is the same application underneath: your role, your project membership and any plan freeze apply exactly as they do in the browser.
curl -H "Authorization: Bearer pj_..." \
https://app.projoodle.com/api/v1/projects
Every response is JSON. Requests carry no cookies and never receive a session cookie, so the token alone decides what a call may do.
A token is bound to one membership in one project. It cannot see or change another project, even when your account belongs to several. Create a separate token per project you want to reach.
| Scope | Allows |
|---|---|
read |
Reading projects, tasks, comments and members |
readwrite |
The above, plus creating and changing tasks and comments |
A scope never widens your own permissions. A read token cannot write even
when your role would allow it, and a readwrite token cannot write where an
archived project, a frozen column or your role would stop you in the browser
either. Revoking a token takes effect on the very next request.
There is a second way in, and it is the one to use for ChatGPT or another
MCP client: Projoodle speaks the Model Context Protocol at
https://app.projoodle.com/mcp, with OAuth 2.1 (authorization code, PKCE
S256) instead of a token you copy by hand.
The difference is worth knowing before you pick one:
/mcp only - an OAuth access
token is deliberately rejected on this REST surface.OAuth uses finer-grained scopes than the two above:
| Scope | Allows |
|---|---|
projoodle.read |
Reading projects, tasks, comments, members and labels |
projoodle.tasks.write |
Creating and changing tasks, including repetition |
projoodle.comments.write |
Adding comments |
projoodle.projects.write |
Creating projects, project settings, Kanban columns and label names |
They are discoverable at
https://app.projoodle.com/.well-known/oauth-authorization-server, so a
client that speaks the protocol finds them without being told.
The same sentence applies here as above: a scope never widens your own permissions. Your role, your project membership and any plan freeze decide what happens, exactly as they do in the browser.
Some features belong to Projoodle PRO, on this API exactly as in the
browser: text search (q on the task lists) and repeating tasks. A call
that needs one of them without PRO on the project or account returns
403 with the code plan_feature_required. Ending an existing series
with frequency none always works, so a lapsed plan never traps a task in
a loop.
| Status | Meaning |
|---|---|
401 |
Token missing, expired or revoked |
403 |
Scope, role, archive or freeze rules forbid this call |
404 |
The project or task does not exist, or this token cannot reach it |
421 |
Wrong host - check the base URL |
422 |
The request body is malformed or fails validation |
429 |
Too many requests; slow down and retry |
Unexpected failures return a generic error together with a correlation ID,
both in the body and in the X-Correlation-ID header. Quote that ID when
reporting a problem: it identifies the server-side log entry, which never
contains your token.
List endpoints are paginated - pass limit and offset. Projoodle's task
model currently has no priority and no parent/subtask relation, so those
fields are deliberately absent rather than stored outside the shared
application model.
Treat a token like a password: it acts with your permissions inside its project. Keep it out of chats, tickets, commits, screenshots and shell history, prefer a short expiry, and revoke what you no longer use.
Tokens are stored only as SHA-256 hashes and stay bound to the membership they were created for. Account-bound OAuth access tokens exist for the MCP endpoint only and are not accepted on this REST surface.
Only account-bound OAuth connections may create a project: a service token is tied to one existing membership and could not reach the new project afterwards. The project starts with one TODO column and no description.
The plan limit is decided inside the same serialisable transaction that
inserts the project, so it cannot be raced; reaching it returns 422
plan_limit_reached.
| title required | string [ 1 .. 100 ] characters |
{- "title": "string"
}{- "project": {
- "id": "string",
- "title": "string",
- "description": "string",
- "status": 0,
- "archived": true,
- "kanban_states": [
- {
- "id": 0,
- "title": "string",
- "position": 0
}
]
}
}| projectId required | string^[0-9A-Fa-f]{32}$ Projoodle project UUID |
{- "project": {
- "id": "string",
- "title": "string",
- "description": "string",
- "status": 0,
- "archived": true,
- "kanban_states": [
- {
- "id": 0,
- "title": "string",
- "position": 0
}
]
}
}Only the project lead may change these. The admin role is deliberately not enough: the same rule as in the Projoodle interface, because the owner row carries the personal project link.
| projectId required | string^[0-9A-Fa-f]{32}$ Projoodle project UUID |
| title | string [ 1 .. 100 ] characters |
| description | string <= 500 characters |
| default_view | string Enum: "list" "board" |
{- "title": "string",
- "description": "string",
- "default_view": "list"
}{- "project": {
- "id": "string",
- "title": "string",
- "description": "string",
- "status": 0,
- "archived": true,
- "kanban_states": [
- {
- "id": 0,
- "title": "string",
- "position": 0
}
]
}
}The plan limit is enforced inside the same serialisable transaction
that inserts the column, so it cannot be raced. Reaching it returns
422 plan_limit_reached.
| projectId required | string^[0-9A-Fa-f]{32}$ Projoodle project UUID |
| title required | string [ 1 .. 50 ] characters |
{- "title": "string"
}{- "project": {
- "id": "string",
- "title": "string",
- "description": "string",
- "status": 0,
- "archived": true,
- "kanban_states": [
- {
- "id": 0,
- "title": "string",
- "position": 0
}
]
}
}| kanbanStateId required | integer [ 1 .. 2147483647 ] |
| title required | string [ 1 .. 50 ] characters |
{- "title": "string"
}{- "project": {
- "id": "string",
- "title": "string",
- "description": "string",
- "status": 0,
- "archived": true,
- "kanban_states": [
- {
- "id": 0,
- "title": "string",
- "position": 0
}
]
}
}A task carries its label as a plain string. This list shows which
values the project actually offers, so a client does not have to guess.
The display name can be overridden per project; name returns the
project's own wording and falls back to the global one.
| projectId required | string^[0-9A-Fa-f]{32}$ Projoodle project UUID |
{- "items": [
- {
- "id": 0,
- "label": "string",
- "name": "string"
}
]
}Labels are a fixed global set; a project cannot add its own. What can be changed is the project's display name for one of them. Commas are stripped, because a task stores its labels comma-separated.
| projectId required | string^[0-9A-Fa-f]{32}$ Projoodle project UUID |
| labelId required | integer >= 1 |
| name required | string [ 1 .. 50 ] characters |
{- "name": "string"
}{- "items": [
- { }
]
}| projectId required | string^[0-9A-Fa-f]{32}$ Projoodle project UUID |
| status | string Enum: "open" "done" Friendly alias for the existing completed flag |
| completed | integer Enum: 0 1 Existing Projoodle completion flag; takes precedence over status |
| kanban_state_id | integer [ 1 .. 2147483647 ] |
| assignee | string^[0-9A-Fa-f]{32}$ Project-user UUID |
| q | string <= 200 characters Case-insensitive title and description search |
| limit | integer [ 1 .. 100 ] Default: 50 |
| offset | integer >= 0 Default: 0 |
{- "items": [
- {
- "id": "string",
- "project_id": "string",
- "title": "string",
- "description": "string",
- "status": "open",
- "kanban_state_id": 0,
- "owner_id": "string",
- "assignees": [
- "string"
], - "labels": "string",
- "start_date": "string",
- "due_date": "string",
- "recurrence": {
- "repeats": true,
- "frequency": "string",
- "active": true,
- "next_occurrence": "string",
- "interval": 0,
- "repeat_until": "string",
- "notify_members": true
}, - "dependencies": [
- "string"
], - "sort_order": 0,
- "completed_at": "string",
- "completed_by": "string",
- "created_at": "string",
- "updated_at": "string"
}
], - "pagination": {
- "limit": 0,
- "offset": 0,
- "total": 0
}
}| projectId required | string^[0-9A-Fa-f]{32}$ Projoodle project UUID |
| title required | string [ 1 .. 200 ] characters |
| description | string <= 100000 characters |
| kanban_state_id | integer [ 1 .. 2147483647 ] |
| assignees | Array of strings <= 100 items [ items^[0-9A-Fa-f]{32}$ ] |
| labels | string <= 500 characters |
| start_date | string <date> |
| due_date | string <date> |
| dependencies | Array of strings <= 13 items [ items^[0-9A-Fa-f]{32}$ ] |
{- "title": "string",
- "description": "string",
- "kanban_state_id": 1,
- "assignees": [
- "string"
], - "labels": "string",
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "dependencies": [
- "string"
]
}{- "task": {
- "id": "string",
- "project_id": "string",
- "title": "string",
- "description": "string",
- "status": "open",
- "kanban_state_id": 0,
- "owner_id": "string",
- "assignees": [
- "string"
], - "labels": "string",
- "start_date": "string",
- "due_date": "string",
- "recurrence": {
- "repeats": true,
- "frequency": "string",
- "active": true,
- "next_occurrence": "string",
- "interval": 0,
- "repeat_until": "string",
- "notify_members": true
}, - "dependencies": [
- "string"
], - "sort_order": 0,
- "completed_at": "string",
- "completed_by": "string",
- "created_at": "string",
- "updated_at": "string"
}
}Applies the same role, freeze and archive checks per project as the per-project listing, so a hit can only come from a project the caller may currently enter. Results are sorted by due date; tasks without a due date come last.
| q | string <= 200 characters Case-insensitive title and description search |
| status | string Enum: "open" "done" Defaults to open |
| assignee | string^[0-9A-Fa-f]{32}$ Project-user UUID |
| project_id | string^[0-9A-Fa-f]{32}$ Restrict the search to one project instead of all visible ones |
| due_before | string <date> Inclusive ISO date; tasks without a due date are excluded |
| due_after | string <date> Inclusive ISO date; tasks without a due date are excluded |
| limit | integer [ 1 .. 100 ] Default: 50 |
| offset | integer >= 0 Default: 0 |
{- "items": [
- {
- "id": "string",
- "project_id": "string",
- "title": "string",
- "description": "string",
- "status": "open",
- "kanban_state_id": 0,
- "owner_id": "string",
- "assignees": [
- "string"
], - "labels": "string",
- "start_date": "string",
- "due_date": "string",
- "recurrence": {
- "repeats": true,
- "frequency": "string",
- "active": true,
- "next_occurrence": "string",
- "interval": 0,
- "repeat_until": "string",
- "notify_members": true
}, - "dependencies": [
- "string"
], - "sort_order": 0,
- "completed_at": "string",
- "completed_by": "string",
- "created_at": "string",
- "updated_at": "string"
}
], - "pagination": {
- "limit": 0,
- "offset": 0,
- "total": 0
}
}| taskId required | string^[0-9A-Fa-f]{32}$ Projoodle task UUID |
{- "task": {
- "id": "string",
- "project_id": "string",
- "title": "string",
- "description": "string",
- "status": "open",
- "kanban_state_id": 0,
- "owner_id": "string",
- "assignees": [
- "string"
], - "labels": "string",
- "start_date": "string",
- "due_date": "string",
- "recurrence": {
- "repeats": true,
- "frequency": "string",
- "active": true,
- "next_occurrence": "string",
- "interval": 0,
- "repeat_until": "string",
- "notify_members": true
}, - "dependencies": [
- "string"
], - "sort_order": 0,
- "completed_at": "string",
- "completed_by": "string",
- "created_at": "string",
- "updated_at": "string"
}
}Ordinary fields use Projoodle's existing edit logic. status uses the
existing completion/reopen transition. kanban_state_id uses the same
move logic as Kanban drag-and-drop. Fields may be combined in one request.
| taskId required | string^[0-9A-Fa-f]{32}$ Projoodle task UUID |
object Set frequency none to end an existing series | |
| title | string [ 1 .. 200 ] characters |
| description | string <= 100000 characters |
| status | string Enum: "open" "done" |
| kanban_state_id | integer [ 1 .. 2147483647 ] |
| assignees | Array of strings <= 100 items [ items^[0-9A-Fa-f]{32}$ ] |
| labels | string <= 500 characters |
| start_date | string <date> |
| due_date | string <date> |
| dependencies | Array of strings <= 13 items [ items^[0-9A-Fa-f]{32}$ ] |
{- "recurrence": {
- "frequency": "none",
- "interval": 1,
- "repeat_until": "2019-08-24",
- "notify_members": true
}, - "title": "string",
- "description": "string",
- "status": "open",
- "kanban_state_id": 1,
- "assignees": [
- "string"
], - "labels": "string",
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "dependencies": [
- "string"
]
}{- "task": {
- "id": "string",
- "project_id": "string",
- "title": "string",
- "description": "string",
- "status": "open",
- "kanban_state_id": 0,
- "owner_id": "string",
- "assignees": [
- "string"
], - "labels": "string",
- "start_date": "string",
- "due_date": "string",
- "recurrence": {
- "repeats": true,
- "frequency": "string",
- "active": true,
- "next_occurrence": "string",
- "interval": 0,
- "repeat_until": "string",
- "notify_members": true
}, - "dependencies": [
- "string"
], - "sort_order": 0,
- "completed_at": "string",
- "completed_by": "string",
- "created_at": "string",
- "updated_at": "string"
}
}| taskId required | string^[0-9A-Fa-f]{32}$ Projoodle task UUID |
{- "items": [
- {
- "id": "string",
- "comment": "string",
- "author": {
- "id": 0,
- "display_name": "string"
}, - "created_at": "string"
}
]
}| taskId required | string^[0-9A-Fa-f]{32}$ Projoodle task UUID |
| comment required | string [ 1 .. 20000 ] characters |
{- "comment": "string"
}{- "id": "string",
- "comment": "string"
}Email addresses and account data are never returned.
| project_id | string Optional visible project UUID; omit to aggregate visible projects |
{- "items": [
- {
- "id": "string",
- "display_name": "string",
- "role": "admin",
- "is_owner": true,
- "project_id": "string"
}
]
}