Plate ← Docs

API Reference

REST API for reading and writing workspace data from external tools and integrations.

Base URL https://plate.to/api

Authentication

Two methods are supported. API keys are for server-to-server integrations. OAuth Bearer tokens are issued by the MCP OAuth flow and represent an authenticated user.

API key — pass in the X-API-Key header. Generate keys in Workspace Settings → API. Always has full read/write access.

# API key
curl https://plate.to/api/v1/workspace \
  -H "X-API-Key: plt_your_key_here"

Bearer token — pass a JWT from the MCP OAuth flow in the Authorization header, plus the target workspace ID in X-Workspace-Id. Scope is read (GET only) or write (all methods).

# Bearer token
curl https://plate.to/api/v1/workspace \
  -H "Authorization: Bearer <token>" \
  -H "X-Workspace-Id: <workspaceId>"
API keys grant full access to your workspace. Never commit them to source control. Only workspace owners and members can create or manage keys — guests cannot.

Errors

StatusMeaning
400Missing or invalid fields
401Missing or invalid API key / Bearer token
403Action not allowed (e.g. free plan limit, read-only scope, not a workspace member)
404Resource not found
500Internal server error
{ "error": "Invalid API key" }

Workspace

GET /v1/workspace

Returns info about the workspace linked to your API key.

{
  "id": "ws_abc",
  "name": "Acme Corp",
  "urlId": "acme",
  "avatarUrl": null,
  "taskPrefix": "SCA"
}
GET /v1/members

Returns all workspace members. Use userId as assigneeId when creating or updating tasks.

[{
  "userId": "user_abc",
  "name": "Jane Smith",
  "email": "jane@acme.com",
  "role": "member",
  "avatarUrl": null
}]

role is "owner", "member", or "guest".

Statuses

GET /v1/statuses

Returns all task statuses in the workspace, ordered by position.

[{
  "id": "status_abc",
  "name": "In Review",
  "color": "#FFFABB",
  "systemType": null,
  "order": 2
}]

systemType is "todo" for the default start status, "done" for the completion status, or null for custom statuses.

Projects

GET /v1/projects

Returns all projects in the workspace.

[{ "id": "proj_abc", "name": "Website Redesign", "description": null, "taskPrefix": "SCA" }]
POST /v1/projects

Creates a new project with a default "To Do" section. Returns 403 if the workspace is on the free plan and has reached the 3-project limit.

FieldTypeDescription
name*stringProject name
descriptionoptionalstringProject description
# Returns 201
{ "id": "proj_xyz", "defaultListId": "sec_abc" }
PATCH /v1/projects/:id

Updates a project's name or description. At least one field required.

FieldTypeDescription
nameoptionalstringNew project name
descriptionoptionalstring | nullNew description, or null to clear
{ "id": "proj_abc" }

Sections

GET /v1/projects/:projectId/sections

Returns all sections in a project, ordered by position.

[{ "id": "sec_abc", "name": "To Do", "order": 0, "projectId": "proj_abc" }]
POST /v1/sections

Creates a new section at the end of a project.

FieldTypeDescription
projectId*stringTarget project
name*stringSection name
# Returns 201
{ "id": "sec_xyz" }
PATCH /v1/sections/:id

Renames a section.

FieldTypeDescription
name*stringNew name

Tasks

Each task has a number field. Combine it with taskPrefix from GET /v1/projects and urlId from GET /v1/workspace to build the task URL: https://plate.to/{urlId}/task/{taskPrefix}-{number}

GET /v1/projects/:projectId/tasks

Returns all tasks in a project.

[{
  "id": "task_abc",
  "number": 42,
  "name": "Design homepage hero",
  "isCompleted": false,
  "description": null,
  "listId": "sec_abc",
  "assigneeId": null,
  "statusId": null,
  "order": 3,
  "createdAt": "2026-06-01T10:00:00.000Z"
}]

order is the task's display position within its section (lower = higher in the list). description is null when empty, or a rich text node array when set.

GET /v1/tasks/:id

Returns a single task with full details, including description, labels, and projectId.

{
  "id": "task_abc",
  "number": 42,
  "name": "Design homepage hero",
  "isCompleted": false,
  "description": null,
  "listId": "sec_abc",
  "projectId": "proj_abc",
  "assigneeId": null,
  "statusId": null,
  "labels": [],
  "createdAt": "2026-06-01T10:00:00.000Z"
}

description is null when empty, or a rich text node array when set. To extract plain text, concatenate the text fields from all leaf nodes. Labels are read-only via the API.

POST /v1/tasks

Creates a new task in a section. Returns 400 if listId does not belong to the specified projectId. Returns 403 if the workspace is on the free plan and has reached the 300-task limit. To set a description, call PATCH /v1/tasks/:id after creation.

FieldTypeDescription
projectId*stringTarget project
listId*stringTarget section (must belong to projectId)
name*stringTask name
assigneeIdoptionalstringAssignee user ID from GET /v1/members
# Returns 201
{ "id": "task_xyz", "number": 42 }
PATCH /v1/tasks/:id

Updates a task. Only the fields you send are changed. At least one field is required (otherwise 400).

FieldTypeDescription
nameoptionalstringNew name
listIdoptionalstringMove task to a different section. Must belong to the same project. Returns 400 if the section belongs to a different project.
isCompletedoptionalbooleanCompletion state. Also auto-sets statusId to the matching system status.
statusIdoptionalstringStatus ID from GET /v1/statuses. Also updates isCompleted based on the status systemType.
assigneeIdoptionalstring | nullAssignee user ID from GET /v1/members, or null to unassign.
descriptionoptionalstring | JSON | nullRich text body. Pass a markdown string, a Plate.js node array, or null to clear.

description format

Pass a markdown string (recommended) or a raw Plate.js node array.

Supported markdown syntax: # / ## / ### headings, - bullet lists, 1. numbered lists, **bold**, *italic*, ***bold italic***, ~~strikethrough~~, `inline code`, > blockquote.

# Markdown string (recommended)
{ "description": "## Steps to reproduce\n\n- Open settings\n- Click Profile\n\n**Expected:** profile page opens" }

# Raw Plate node array (advanced)
{ "description": [{ "type": "p", "children": [{ "text": "Your text here" }] }] }

# Clear the description
{ "description": null }
DELETE /v1/tasks/:id

Deletes a task and all its comments. Returns 204 on success.

Comments

POST /v1/tasks/:id/comments

Adds a comment to a task. When using an API key the comment appears under the key name; when using a Bearer token it appears as the authenticated user. Returns 201 on success.

FieldTypeDescription
text*stringComment text. Supports markdown: # headings, - bullets, 1. numbered lists, **bold**, *italic*, `code`, > blockquote. Blank lines separate blocks.
# Plain text
{ "text": "Fixed the issue in projectsSaga.ts:287." }

# With markdown formatting
{ "text": "## Root cause\n\nThe worker was missing an `await`.\n\n- Fixed in projectsSaga.ts:287\n- Added unit test" }

# Returns 201
{ "id": "comment_xyz" }
DELETE /v1/comments/:id

Deletes a comment. Returns 204 on success.

Attachments

Uploading a file is a two-step process: get a signed URL, upload the file directly to Firebase Storage, then register the attachment on the task.

POST /v1/attachments/upload

Returns a resumable upload URL for uploading a file directly to Firebase Storage (valid for 7 days). PUT the file to this URL with the exact Content-Type and Content-Length headers.

FieldTypeDescription
contentType*stringMIME type of the file (e.g. image/png)
size*numberFile size in bytes. Rejected if over plan limit (25 MB free / 100 MB pro).
# Step 1: get upload URL
{ "uploadUrl": "https://storage.googleapis.com/...", "attachmentId": "att_abc" }

# Step 2: upload the file directly to GCS
curl -X PUT "<uploadUrl>" \
  -H "Content-Type: image/png" \
  -H "Content-Length: <size in bytes>" \
  --data-binary @file.png
POST /v1/tasks/:id/attachments

Registers an already-uploaded file as an attachment on a task. Call this after a successful PUT to the signed URL. Returns the permanent download URL.

FieldTypeDescription
attachmentId*stringID returned by POST /v1/attachments/upload
name*stringFile name (e.g. screenshot.png)
contentType*stringMIME type
size*numberFile size in bytes
{ "id": "att_abc", "url": "https://firebasestorage.googleapis.com/..." }

Webhooks

Webhooks let Plate push real-time notifications to your server whenever tasks are created, updated, or completed — no polling required. See the Webhooks reference for payload format, signature verification, and retry behavior.

POST /v1/webhooks

Registers a new webhook endpoint. Plate will send a signed HTTP POST to url for each matching event. Returns 201 on success.

FieldTypeDescription
url*stringHTTPS endpoint that will receive event payloads
secret*stringShared secret used to sign payloads. Keep it private — Plate never returns it again.
eventsoptionalstring[]Which events to deliver: task.created, task.updated, task.completed. Omit or send [] to subscribe to all three.
{ "id": "wh_abc", "url": "https://example.com/hooks/plate", "events": ["task.created", "task.updated", "task.completed"] }
GET /v1/webhooks

Returns all registered webhook endpoints for the workspace. Secrets are never included in the response.

[
  {
    "id": "wh_abc",
    "url": "https://example.com/hooks/plate",
    "events": ["task.created", "task.updated", "task.completed"],
    "createdAt": "2024-01-15T10:00:00.000Z"
  }
]
DELETE /v1/webhooks/:id

Deletes a webhook endpoint. Returns 204 on success.

Questions? Write to us at hello@plate.to