API reference
Every operation on https://api.workel.com/api/public/v1. Generated from the same OpenAPI document our test suite verifies against the live route table — see the guide for auth, idempotency, rate limits, and webhook verification.
Meta
Key/workspace introspection.
/me
any valid key
Confirm a key is working and inspect its scopes and rate-limit state
Requires a valid key but no specific scope.
Example request
curl https://api.workel.com/api/public/v1/me \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"workspace": {
"id": "string",
"name": "Acme Inc"
},
"key": {
"name": "Website integration",
"scopes": [
"read:projects"
]
},
"rate_limit": {
"key": {
"limit": 25,
"remaining": 25
},
"workspace": {
"limit": 25,
"remaining": 25
},
"write": "string"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
Projects
Read-only. Never includes inbox, archived, or private projects.
/projects
read:projects
List visible projects
Excludes inbox projects, archived projects, and private projects.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size. Values ≤ 0 fall back to the default; values above the maximum are silently clamped, never rejected with a 422. Default 25, maximum 100 — both launch placeholders pending ratification, not a contract value. |
cursor | query | string | Opaque pagination cursor, taken verbatim from a previous response's |
Example request
curl https://api.workel.com/api/public/v1/projects \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"data": [
{
"id": "string",
"name": "Website integration",
"description": "Created from the website order form.",
"created_at": "2026-08-23T12:00:00Z",
"updated_at": "2026-08-23T12:00:00Z"
}
],
"meta": {
"next_cursor": "eyJpZCI6ImExYjJjM2Q0In0"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
/projects/{id}
read:projects
Fetch a single visible project
Example request
curl https://api.workel.com/api/public/v1/projects/:id \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"data": {
"id": "string",
"name": "Website integration",
"description": "Created from the website order form.",
"created_at": "2026-08-23T12:00:00Z",
"updated_at": "2026-08-23T12:00:00Z"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 404 | The id does not resolve to a visible resource. This is intentionally indistinguishable from "exists but belongs to another workspace", "exists but is private/archived/inbox", and "does not exist at all" — the API never confirms the existence of a resource the key cannot see. |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
/projects/{id}/cards
read:projects
List a project's board columns
A Workel "card" here is a board COLUMN (e.g. "To Do"/"In Progress"), not a task — so integrators can target a column id on task create.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size. Values ≤ 0 fall back to the default; values above the maximum are silently clamped, never rejected with a 422. Default 25, maximum 100 — both launch placeholders pending ratification, not a contract value. |
cursor | query | string | Opaque pagination cursor, taken verbatim from a previous response's |
Example request
curl https://api.workel.com/api/public/v1/projects/:id/cards \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"data": [
{
"id": "string",
"name": "Website integration",
"is_done": false,
"order": 1
}
],
"meta": {
"next_cursor": "eyJpZCI6ImExYjJjM2Q0In0"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 404 | The id does not resolve to a visible resource. This is intentionally indistinguishable from "exists but belongs to another workspace", "exists but is private/archived/inbox", and "does not exist at all" — the API never confirms the existence of a resource the key cannot see. |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
Members
Active workspace members. The one endpoint on this surface that exposes email addresses.
/members
read:members
List active workspace members
Only members with an active (not pending/suspended) membership are listed.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size. Values ≤ 0 fall back to the default; values above the maximum are silently clamped, never rejected with a 422. Default 25, maximum 100 — both launch placeholders pending ratification, not a contract value. |
cursor | query | string | Opaque pagination cursor, taken verbatim from a previous response's |
Example request
curl https://api.workel.com/api/public/v1/members \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"data": [
{
"id": "string",
"name": "Website integration",
"email": "alex@example.com",
"role": "owner",
"joined_at": "2026-08-23T12:00:00Z"
}
],
"meta": {
"next_cursor": "eyJpZCI6ImExYjJjM2Q0In0"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
Tasks
Read and write. Reachable only through a visible project's board.
/tasks
read:tasks
List tasks across every visible project
Parameters
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size. Values ≤ 0 fall back to the default; values above the maximum are silently clamped, never rejected with a 422. Default 25, maximum 100 — both launch placeholders pending ratification, not a contract value. |
cursor | query | string | Opaque pagination cursor, taken verbatim from a previous response's |
project_id | query | string | Narrow to one project. A foreign/private/inbox/nonexistent id simply yields zero rows. |
card_id | query | string | Narrow to one board column. |
completed | query | string true "1" "false" "0" |
|
due_before | query | string (date) | Strict |
due_after | query | string (date) | Strict |
updated_since | query | string (date-time) | Any parseable date/time string. Unparseable values are silently ignored. |
Example request
curl https://api.workel.com/api/public/v1/tasks \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"data": [
{
"id": "string",
"title": "New order #4821",
"description": "Created from the website order form.",
"project_id": "a1b2c3d4",
"card": {
"id": "string",
"name": "Website integration",
"is_done": false
},
"priority": "high",
"due_date": "2026-08-30",
"due_time": "14:30",
"progress": 40,
"completed": false,
"assignee_ids": [
"Alex Rivera"
],
"created_at": "2026-08-23T12:00:00Z",
"updated_at": "2026-08-23T12:00:00Z"
}
],
"meta": {
"next_cursor": "eyJpZCI6ImExYjJjM2Q0In0"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
/tasks
write:tasks
Create a task
Runs the full task-create recipe (tenant re-check, permission check against the target project, default-column resolution, assignee membership re-verification, activity/notification dispatch, realtime broadcast) — not a thinned-down write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string | Caller-chosen opaque token. See the "Idempotency" section of this document's description for the full contract. Stored responses expire after 24 hours — a launch placeholder pending ratification, not a contract value. |
Request body
| Field | Type | Description |
|---|---|---|
card_id | string · nullable | The board column to create the task in. Exactly one of |
project_id | string · nullable | |
title_text required | string | |
description | string · nullable | |
priority | string · nullable "low" "medium" "high" "urgent" "none" null | |
progress | integer · nullable | |
end_date | string · nullable (date) | |
end_time | string · nullable | "HH:mm". Dropped if |
user_ids | array<string> | Must all be members of the bound workspace, or the whole request is rejected with 422 |
Example request
curl -X POST https://api.workel.com/api/public/v1/tasks \
-H "Authorization: Bearer wk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: YOUR_UNIQUE_KEY" \
-d '{"project_id":"a1b2c3d4","title_text":"New order #4821"}'
Response 201
Created
{
"data": {
"id": "string",
"title": "New order #4821",
"description": "Created from the website order form.",
"project_id": "a1b2c3d4",
"card": {
"id": "string",
"name": "Website integration",
"is_done": false
},
"priority": "high",
"due_date": "2026-08-30",
"due_time": "14:30",
"progress": 40,
"completed": false,
"assignee_ids": [
"Alex Rivera"
],
"created_at": "2026-08-23T12:00:00Z",
"updated_at": "2026-08-23T12:00:00Z"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | Missing |
| 404 |
|
| 409 | POST only. The |
| 422 | Validation failure, OR the target project is an inbox project ( |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
/tasks/{id}
read:tasks
Fetch a single task
Example request
curl https://api.workel.com/api/public/v1/tasks/:id \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"data": {
"id": "string",
"title": "New order #4821",
"description": "Created from the website order form.",
"project_id": "a1b2c3d4",
"card": {
"id": "string",
"name": "Website integration",
"is_done": false
},
"priority": "high",
"due_date": "2026-08-30",
"due_time": "14:30",
"progress": 40,
"completed": false,
"assignee_ids": [
"Alex Rivera"
],
"created_at": "2026-08-23T12:00:00Z",
"updated_at": "2026-08-23T12:00:00Z"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 404 | The id does not resolve to a visible resource. This is intentionally indistinguishable from "exists but belongs to another workspace", "exists but is private/archived/inbox", and "does not exist at all" — the API never confirms the existence of a resource the key cannot see. |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
/tasks/{id}
write:tasks
Update a task
NOT idempotency-key aware (PublicApiIdempotency only inspects POST requests) — a retried PATCH with the same body is safe only because the writable fields are themselves idempotent, not because of Idempotency-Key support. Every accepted field is applied through the same TaskUpdateService the web UI's task-update path uses, so the reminder re-arm, orphaned-end_time cleanup, and recurring-task roll-forward invariants apply identically here.
Request body
| Field | Type | Description |
|---|---|---|
title_text | string · nullable | |
description | string · nullable | |
priority | string · nullable "low" "medium" "high" "urgent" "none" null | |
progress | integer · nullable | |
end_date | string · nullable (date) | |
end_time | string · nullable | |
reminder_date | string · nullable (date-time) |
Example request
curl -X PATCH https://api.workel.com/api/public/v1/tasks/:id \
-H "Authorization: Bearer wk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"title_text":"New order #4821","description":"Created from the website order form."}'
Response 200
OK
{
"data": {
"id": "string",
"title": "New order #4821",
"description": "Created from the website order form.",
"project_id": "a1b2c3d4",
"card": {
"id": "string",
"name": "Website integration",
"is_done": false
},
"priority": "high",
"due_date": "2026-08-30",
"due_time": "14:30",
"progress": 40,
"completed": false,
"assignee_ids": [
"Alex Rivera"
],
"created_at": "2026-08-23T12:00:00Z",
"updated_at": "2026-08-23T12:00:00Z"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 404 | The id does not resolve to a visible resource. This is intentionally indistinguishable from "exists but belongs to another workspace", "exists but is private/archived/inbox", and "does not exist at all" — the API never confirms the existence of a resource the key cannot see. |
| 422 | The request body or query string failed validation. |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
Task comments
Read and write. Plain text only — no @-mention syntax in v1.
/tasks/{id}/comments
read:tasks
List a task's comments
Every comment on the task, top-level and replies alike, oldest first. Exists mainly so a comment.created webhook consumer (a thin, ids-only payload) can fetch the comment it was told about.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size. Values ≤ 0 fall back to the default; values above the maximum are silently clamped, never rejected with a 422. Default 25, maximum 100 — both launch placeholders pending ratification, not a contract value. |
cursor | query | string | Opaque pagination cursor, taken verbatim from a previous response's |
Example request
curl https://api.workel.com/api/public/v1/tasks/:id/comments \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"data": [
{
"id": "string",
"body": "Created from the website order form.",
"author": {
"id": "string",
"name": "Alex Rivera"
},
"created_at": "2026-08-23T12:00:00Z"
}
],
"meta": {
"next_cursor": "eyJpZCI6ImExYjJjM2Q0In0"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 404 | The id does not resolve to a visible resource. This is intentionally indistinguishable from "exists but belongs to another workspace", "exists but is private/archived/inbox", and "does not exist at all" — the API never confirms the existence of a resource the key cannot see. |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
/tasks/{id}/comments
write:comments
Comment on a task
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string | Caller-chosen opaque token. See the "Idempotency" section of this document's description for the full contract. Stored responses expire after 24 hours — a launch placeholder pending ratification, not a contract value. |
Request body
| Field | Type | Description |
|---|---|---|
body required | string | |
mention_user_ids | any | Must not be sent at all — including as an empty array or |
Example request
curl -X POST https://api.workel.com/api/public/v1/tasks/:id/comments \
-H "Authorization: Bearer wk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: YOUR_UNIQUE_KEY" \
-d '{"body":"Created from the website order form."}'
Response 201
Created
{
"data": {
"id": "string",
"body": "Created from the website order form.",
"author": {
"id": "string",
"name": "Alex Rivera"
},
"created_at": "2026-08-23T12:00:00Z"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 404 | The id does not resolve to a visible resource. This is intentionally indistinguishable from "exists but belongs to another workspace", "exists but is private/archived/inbox", and "does not exist at all" — the API never confirms the existence of a resource the key cannot see. |
| 409 | POST only. The |
| 422 | Validation failure — including sending |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
Events
Read and write. Workspace-level and project-level calendar events.
/events
read:events
List workspace and project events
Includes events bound directly to the workspace and events on any visible project.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size. Values ≤ 0 fall back to the default; values above the maximum are silently clamped, never rejected with a 422. Default 25, maximum 100 — both launch placeholders pending ratification, not a contract value. |
cursor | query | string | Opaque pagination cursor, taken verbatim from a previous response's |
from | query | string (date) | Inclusive lower bound. Any parseable date. Unparseable → 422 |
to | query | string (date) | Inclusive upper bound. Any parseable date. Unparseable → 422 |
Example request
curl https://api.workel.com/api/public/v1/events \
-H "Authorization: Bearer wk_YOUR_KEY"
Response 200
OK
{
"data": [
{
"id": "string",
"title": "New order #4821",
"description": "Created from the website order form.",
"date": "2026-08-30",
"start_time": "14:30",
"end_time": "14:30",
"location": "string",
"project_id": "a1b2c3d4",
"workspace_id": "a1b2c3d4",
"color": "blue",
"repeat": "none",
"created_at": "2026-08-23T12:00:00Z",
"updated_at": "2026-08-23T12:00:00Z"
}
],
"meta": {
"next_cursor": "eyJpZCI6ImExYjJjM2Q0In0"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 422 | The request body or query string failed validation. |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
/events
write:events
Create an event
Unconditionally logged to the workspace/project activity feed on success, even with zero invitees — an API-originated write is exactly the case this audit trail exists for.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string | Caller-chosen opaque token. See the "Idempotency" section of this document's description for the full contract. Stored responses expire after 24 hours — a launch placeholder pending ratification, not a contract value. |
Request body
| Field | Type | Description |
|---|---|---|
title required | string | |
description | string · nullable | |
date required | string (date) | |
start_time required | string | "HH:mm". |
end_time required | string | "HH:mm", strictly after start_time. |
reminder_at | string · nullable (date-time) | |
timezone | string · nullable | A valid IANA timezone name. |
reminder_minutes_before | integer · nullable | |
repeat required | string "none" "daily" "weekly" "monthly" "yearly" | |
repeat_interval | integer · nullable | Required unless |
location | string · nullable | |
meet_link | string · nullable (uri) | |
color | string · nullable | |
order | integer · nullable | |
project_id | string · nullable | When given, must resolve to a project visible in the bound workspace (not inbox, not archived, not private) or the request is rejected with 422 — a foreign, private, inbox, or nonexistent id are all indistinguishable. Omitted → the event is workspace-level. |
invited_users | array<string> | User ids to invite. Existence is checked; an id outside the bound workspace's membership is silently dropped rather than rejected (unlike a task's |
Example request
curl -X POST https://api.workel.com/api/public/v1/events \
-H "Authorization: Bearer wk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: YOUR_UNIQUE_KEY" \
-d '{"title":"New order #4821","date":"2026-08-30","start_time":"14:30","end_time":"14:30","repeat":"none","project_id":"a1b2c3d4"}'
Response 201
Created
{
"data": {
"id": "string",
"title": "New order #4821",
"description": "Created from the website order form.",
"date": "2026-08-30",
"start_time": "14:30",
"end_time": "14:30",
"location": "string",
"project_id": "a1b2c3d4",
"workspace_id": "a1b2c3d4",
"color": "blue",
"repeat": "none",
"created_at": "2026-08-23T12:00:00Z",
"updated_at": "2026-08-23T12:00:00Z"
}
}Errors
| Status | When |
|---|---|
| 401 | No valid API key was presented (missing bearer token, malformed secret, unknown/expired token, or a key that failed its creator's active-owner/admin recheck). |
| 403 | The key is valid but disabled, or lacks the scope this operation requires, or (for |
| 409 | POST only. The |
| 422 | Validation failure, including an unresolvable |
| 429 | A limiter was exceeded: per-key, per-workspace, the tighter per-key write budget ( |
Every error is the same envelope: {"error": {"type", "code", "message", "param"?, "request_id"}}.
Webhooks
Deliveries are HTTPS POSTs signed over the exact raw body — see the guide for the Workel-Signature verification scheme, retry/backoff behavior, and endpoint management. Dedupe on Workel-Event-Id.
task.createdFired from the Task model's created Eloquent hook — every origin (this API, the web UI, an import), not only writes made through this API. Suppressed for the duration of a bulk CSV import.
Payload
{
"id": "a1b2c3d4",
"type": "task.created",
"created_at": "2026-08-23T12:00:00Z",
"data": {}
}
task.updatedFired from the shared task-update service (the same seam PATCH /tasks/{id} and the web UI's task editor both go through) whenever a field actually changes. A plain column-move drag in the web UI (no field edit) does NOT emit this — a known v1 limitation.
Payload
{
"id": "a1b2c3d4",
"type": "task.created",
"created_at": "2026-08-23T12:00:00Z",
"data": {}
}
task.completedFired from the same seam as task.updated, specifically on the progress <100 → ≥100 transition.
Payload
{
"id": "a1b2c3d4",
"type": "task.created",
"created_at": "2026-08-23T12:00:00Z",
"data": {}
}
task.deletedIds-only payload (id, card_id, project_id) — deliberately not built from TaskResource, since a deleted task's title/description are exactly what a mirror-keeping consumer does not need.
Payload
{
"id": "a1b2c3d4",
"type": "task.created",
"created_at": "2026-08-23T12:00:00Z",
"data": {}
}
comment.createdbody is stripped from the payload (free-text fields are never sent over webhooks) — fetch the full comment with GET /tasks/{id}/comments using your own scoped key.
Payload
{
"id": "a1b2c3d4",
"type": "task.created",
"created_at": "2026-08-23T12:00:00Z",
"data": {}
}
event.createdPayload
{
"id": "a1b2c3d4",
"type": "task.created",
"created_at": "2026-08-23T12:00:00Z",
"data": {}
}
project.createdPayload
{
"id": "a1b2c3d4",
"type": "task.created",
"created_at": "2026-08-23T12:00:00Z",
"data": {}
}