Get the workspace
get/v1/workspaces/{workspace_id}
Answers 200, or an error: 401, 403, 404, 422, 429.
Developers
A versioned REST API for your workspace's tasks, projects and comments, and signed webhooks that tell your service the moment something changes.
Every request carries an API token in the Authorization header. A workspace admin creates an agent and its tokens in Settings, Agents. A token belongs to one workspace and holds scopes: workspace:read for every read, and tasks:write to create and change tasks and comments. The base URL is https://api-issuelane.cublya.com/v1.
curl https://api-issuelane.cublya.com/v1/workspaces/$WORKSPACE_ID/tasks?limit=50 \
-H "Authorization: Bearer $ISSUELANE_TOKEN"Changes made with a token are recorded under the agent’s name in every task’s history. A browser session is not accepted here; use a token.
Every list answers { "data": [...], "nextCursor": "..." }. To read the next page, send the same request with cursor set to nextCursor; it is null on the last page. Keep the filters and the order the same while paging: a cursor from a different list is refused. Pages hold up to 100 items, 50 unless you set limit.
To keep a copy in sync, list tasks with order=CREATED once, then poll with updated_since, or better, subscribe to webhooks.
Every error has the same shape. Switch on type, show message, and quote requestId if you report a problem. A 422 lists the refused fields in errors.
{
"error": {
"type": "validation_failed",
"message": "title cannot be null",
"requestId": "01J…",
"errors": [{ "field": "title", "message": "title cannot be null" }]
}
}The types are unauthorized (401), forbidden (403), not_found (404), conflict (409), payload_too_large (413), validation_failed (422), rate_limited (429), internal_error (500) and unavailable (503).
Send an Idempotency-Key header holding a UUID you generate once per intended write. If the connection drops and you retry with the same key, you get the first result back and nothing is written twice. Reusing a key for a different request is a 409.
Each token may make 600 requests and 60 writes a minute. All the tokens of one workspace share 300 writes a minute. The windows are fixed and 60 seconds long. Over a limit the answer is 429 with a Retry-After header in seconds; wait that long, then retry.
A workspace admin adds endpoints in Settings, Webhooks, and picks the events each one is sent. Every delivery is a POST with a JSON body:
{
"id": "msg_5f0c7a1e9b2d4c6e8a0b1c2d3e4f5a6b",
"type": "task.updated",
"timestamp": "2026-10-28T19:42:07.512301Z",
"data": {
"workspaceId": "4b0e1c7e-…",
"seq": 1842,
"entityId": "9d3f2a10-…",
"actorId": "1c5e8b22-…",
"object": { "id": "9d3f2a10-…", "identifier": "WEB-42", "title": "…", "stateId": "…" }
}
}data.object has the same fields as the matching API object. If a body would be larger than 256 KB, object is empty and data.truncated is true; read the resource through the API.
The events:
task.createdtask.updatedtask.deletedcomment.createdcomment.updatedcomment.deletedproject.createdproject.updatedlabel.createdlabel.updatedlabel.deletedstate.createdstate.updatedtask_relation.createdtask_relation.deletedattachment.createdattachment.deletedmember.createdmember.deletedwebhook.test is sent when an admin presses Send test.
Each delivery carries the Standard Webhooks headers webhook-id, webhook-timestamp and webhook-signature. The signature is v1, followed by a base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the bytes after the secret’s whsec_ prefix. Any Standard Webhooks library verifies it, or:
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody is the request body exactly as received, before any JSON parsing.
export function verify(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
// Refuse anything older or newer than five minutes: it may be a replay.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest("base64");
return headers["webhook-signature"].split(" ").some((entry) => {
const [version, signature] = entry.split(",");
return (
version === "v1" &&
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
});
}For receivers that already check it, X-Hub-Signature-256 carries sha256= and a hex HMAC-SHA256 of the body alone, keyed with the whole secret as shown. It does not cover the timestamp, so check webhook-timestamp yourself.
Answer with any 2xx within 10 seconds. Anything else, redirects included, is a failure and is retried after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 10 hours: 8 attempts over about a day. Delivery is at least once and not ordered, so use webhook-id to ignore a message you have already handled and data.seq to order changes to one workspace. An endpoint that fails 20 times in a row for more than 24 hours is turned off, and the settings page says why. The delivery log keeps 30 days of attempts, and any delivery can be sent again from it.
Endpoints must be public https URLs. Private, loopback, link-local and reserved addresses are refused when the endpoint is saved, and checked again on every delivery after the name is resolved, so a name that later points inward is refused too.
The machine-readable description is served at https://api-issuelane.cublya.com/v1/openapi.json (OpenAPI 3.1). Version 1 only grows: fields and endpoints are added, never removed or changed. A breaking change would be /v2, beside this one.
get/v1/workspaces/{workspace_id}
Answers 200, or an error: 401, 403, 404, 422, 429.
get/v1/workspaces/{workspace_id}/members
Every person in the workspace, by name. Not paged.
Answers 200, or an error: 401, 403, 404, 422, 429.
get/v1/workspaces/{workspace_id}/labels
Every label, or with `project_id` the shared labels plus that project's own. Not paged.
| project_id? | query | string (uuid) |
Answers 200, or an error: 401, 403, 404, 422, 429.
get/v1/workspaces/{workspace_id}/projects
Not paged. Archived projects only when `include_archived` is true.
| include_archived? | query | boolean |
Answers 200, or an error: 401, 403, 404, 422, 429.
get/v1/workspaces/{workspace_id}/projects/{project_id}
| project_id | path | string (uuid) |
Answers 200, or an error: 401, 403, 404, 422, 429.
get/v1/workspaces/{workspace_id}/projects/{project_id}/sprints
The latest start first, undated sprints last. Not paged.
| project_id | path | string (uuid) |
Answers 200, or an error: 401, 403, 404, 422, 429.
get/v1/workspaces/{workspace_id}/projects/{project_id}/states
In workflow order. Not paged.
| project_id | path | string (uuid) |
Answers 200, or an error: 401, 403, 404, 422, 429.
get/v1/workspaces/{workspace_id}/tasks
Tasks across the workspace, narrowed by every filter given (they are ANDed) and paged by an opaque cursor. Filters narrow the whole workspace, not the page. A filter naming something the workspace does not have is a 422, not an empty page. To page, send the same filters and order with each `cursor`; a cursor from a different list is refused.
| project_id? | query | string (uuid) | Only tasks in this project. |
| state_id? | query | string (uuid) | Only tasks in this workflow state. |
| state_category? | query | StateCategory | Only tasks whose state is in this category. |
| assignee_id? | query | string (uuid) | Only tasks assigned to this person. |
| priority? | query | TaskPriority | |
| label_id? | query | string (uuid) | Only tasks carrying this label. |
| parent_id? | query | string (uuid) | Only the sub-tasks of this task. |
| updated_since? | query | string (date-time) | Only tasks changed at or after this instant. Include an offset. |
| include_archived? | query | boolean | |
| order? | query | TaskOrder | |
| limit? | query | integer | How many items a page holds, 1 to 100. |
| cursor? | query | string | The `nextCursor` of the previous page. Send the same filters with it. |
Answers 200, or an error: 401, 403, 404, 422, 429.
post/v1/workspaces/{workspace_id}/tasks
Needs the `tasks:write` scope. Send an `Idempotency-Key` to make retries safe.
| Idempotency-Key? | header | string (uuid) | A UUID naming this write. Retrying with the same key returns the first result instead of writing again. |
Answers 201, or an error: 401, 403, 404, 409, 422, 429, 503.
get/v1/workspaces/{workspace_id}/tasks/{task_id}
| task_id | path | string (uuid) |
Answers 200, or an error: 401, 403, 404, 422, 429.
patch/v1/workspaces/{workspace_id}/tasks/{task_id}
Changes only the fields sent. Archive with `archived: true`; the public API does not delete. Needs the `tasks:write` scope.
| task_id | path | string (uuid) | |
| Idempotency-Key? | header | string (uuid) | A UUID naming this write. Retrying with the same key returns the first result instead of writing again. |
Answers 200, or an error: 401, 403, 404, 409, 422, 429, 503.
get/v1/workspaces/{workspace_id}/tasks/{task_id}/comments
Oldest first, paged by cursor. A deleted comment stays in the list with a null body.
| task_id | path | string (uuid) | |
| limit? | query | integer | How many items a page holds, 1 to 100. |
| cursor? | query | string | The `nextCursor` of the previous page. Send the same filters with it. |
Answers 200, or an error: 401, 403, 404, 422, 429.
post/v1/workspaces/{workspace_id}/tasks/{task_id}/comments
Needs the `tasks:write` scope. Mentions in the body notify people as they do in the app.
| task_id | path | string (uuid) | |
| Idempotency-Key? | header | string (uuid) | A UUID naming this write. Retrying with the same key returns the first result instead of writing again. |
Answers 201, or an error: 401, 403, 404, 409, 422, 429, 503.
| authorId | string (uuid) | |
| body | string or null | Markdown; null once deleted. |
| createdAt | string (date-time) | |
| deletedAt | string (date-time) or null | |
| editedAt | string (date-time) or null | |
| id | string (uuid) | |
| parentId | string (uuid) or null | The comment this replies to. |
| taskId | string (uuid) | |
| updatedAt | string (date-time) | |
| workspaceId | string (uuid) |
| body | string | Markdown. |
| parentId? | string (uuid) or null | The comment to reply to. |
| data | Comment[] | |
| nextCursor | string or null | Pass as `cursor` for the next page; null on the last. |
| color | string or null | |
| createdAt | string (date-time) | |
| groupId | string (uuid) or null | |
| id | string (uuid) | |
| name | string | |
| projectId | string (uuid) or null | Null for a label every project shares. |
| workspaceId | string (uuid) |
| data | Label[] | |
| nextCursor | string or null | Pass as `cursor` for the next page; null on the last. |
A person in the workspace. Agents are not members.
| string or null | ||
| id | string (uuid) | |
| joinedAt | string (date-time) | |
| name | string or null | |
| role | MemberRole |
| data | Member[] | |
| nextCursor | string or null | Pass as `cursor` for the next page; null on the last. |
A person's role in the workspace.
One of OWNERADMINMEMBERGUEST
| archivedAt | string (date-time) or null | |
| createdAt | string (date-time) | |
| description | string or null | |
| id | string (uuid) | |
| key | string | The prefix of the project's task identifiers. |
| leadId | string (uuid) or null | |
| name | string | |
| slug | string | |
| startsAt | string (date) or null | |
| targetAt | string (date) or null | |
| workspaceId | string (uuid) |
| data | Project[] | |
| nextCursor | string or null | Pass as `cursor` for the next page; null on the last. |
| createdAt | string (date-time) | |
| description | string or null | |
| endsAt | string (date-time) or null | |
| id | string (uuid) | |
| name | string | |
| projectId | string (uuid) | |
| startsAt | string (date-time) or null | |
| status | SprintStatus | |
| updatedAt | string (date-time) | |
| workspaceId | string (uuid) |
| data | Sprint[] | |
| nextCursor | string or null | Pass as `cursor` for the next page; null on the last. |
Where a sprint is in its life.
One of PLANNEDACTIVECOMPLETED
One step of a project's workflow.
| category | StateCategory | |
| color | string or null | |
| createdAt | string (date-time) | |
| id | string (uuid) | |
| isDefault | boolean | |
| name | string | |
| position | integer | |
| projectId | string (uuid) | |
| workspaceId | string (uuid) |
The fixed category every workflow state maps onto. States are named freely per project; read the category, never the name, to tell open work from done.
One of BACKLOGUNSTARTEDSTARTEDCOMPLETEDCANCELED
| data | State[] | |
| nextCursor | string or null | Pass as `cursor` for the next page; null on the last. |
| acceptanceCriteria | string or null | Markdown. |
| archivedAt | string (date-time) or null | |
| assigneeId | string (uuid) or null | Always a person. |
| canceledAt | string (date-time) or null | |
| completedAt | string (date-time) or null | |
| createdAt | string (date-time) | |
| creatorId | string (uuid) | |
| delegateId | string (uuid) or null | The agent working on it, if any. |
| description | string or null | Markdown. |
| dueDate | string (date) or null | |
| estimate | integer or null | |
| id | string (uuid) | |
| identifier | string | The human key, such as `WEB-42`. |
| labelIds | string (uuid)[] | |
| number | integer | |
| parentId | string (uuid) or null | |
| priority | TaskPriority | |
| projectId | string (uuid) | |
| sprintId | string (uuid) or null | |
| startedAt | string (date-time) or null | |
| stateId | string (uuid) | |
| teamId | string (uuid) or null | |
| title | string | |
| updatedAt | string (date-time) | |
| workspaceId | string (uuid) |
| acceptanceCriteria? | string or null | |
| assigneeId? | string (uuid) or null | |
| description? | string or null | |
| dueDate? | string (date) or null | |
| estimate? | integer or null | |
| labelIds? | string (uuid)[] | |
| parentId? | string (uuid) or null | |
| priority? | TaskPriority | |
| projectId | string (uuid) | |
| sprintId? | string (uuid) or null | |
| stateId? | string (uuid) or null | Defaults to the project's default state. |
| title | string |
| data | Task[] | |
| nextCursor | string or null | Pass as `cursor` for the next page; null on the last. |
How urgent a task is. NONE is the default for a task nobody has ranked.
One of NONEURGENTHIGHMEDIUMLOW
Only the fields sent change. Null clears a field that may be empty; title, state, priority, labels and archived cannot be null.
| acceptanceCriteria? | string or null | |
| archived? | boolean or null | |
| assigneeId? | string (uuid) or null | |
| description? | string or null | |
| dueDate? | string (date) or null | |
| estimate? | integer or null | |
| labelIds? | string (uuid)[] or null | Replaces the whole set. |
| parentId? | string (uuid) or null | |
| priority? | TaskPriority or null | |
| sprintId? | string (uuid) or null | |
| stateId? | string (uuid) or null | |
| title? | string or null |
| createdAt | string (date-time) | |
| id | string (uuid) | |
| name | string | |
| slug | string |