API

Coco Kanban has a REST API. Anything you can do to projects, work items, cycles and modules in the app, you can do from a script, a CI job, or your own tool.

The API is part of the core platform — it's available on every plan.


The basics

Base path/api/v1/ on your instance — e.g. https://your-instance.example.com/api/v1/
AuthAn API key in the X-Api-Key header
FormatJSON in, JSON out
Rate limit60 requests per minute per key
Trailing slashesRequired. See the warning below.

Trailing slashes matter. Every route ends in /. A request without the trailing slash gets a 301 redirect, and most HTTP clients silently turn the redirected POST/PATCH into a GET — so your write looks like it "did nothing". Always include the final slash.


Getting a key

Keys are personal. Everything the key does is attributed to you, and it can only reach the workspaces you're already a member of.

  1. Open your profile settings → API tokens (/settings/profile/api-tokens/).
  2. Click to add a token, give it a title and an optional description.
  3. Choose an expiry — 1 month, 3 months, 1 year, a custom date, or never expires.
  4. Copy the key. It's shown once. Store it somewhere safe.

📷 Screenshot: the API tokens page in profile settings, with the create-token dialog open showing the title, description and expiry fields.

If you're setting up Claude or another MCP client, Workspace settings → Connect to Claude mints a key for you and hands you the config — see Connecting Claude & AI.

You can revoke a key at any time from the same page. Revoking takes effect immediately.


Authentication

Send the key in the X-Api-Key header on every request:

X-Api-Key: <your-key>

There is no OAuth flow and no session cookie for the API. A missing, revoked or expired key gets a 401.


Your first request

List the projects in a workspace. acme is your workspace slug — the bit in your URL after the domain.

curl -s "https://your-instance.example.com/api/v1/workspaces/acme/projects/" \
  -H "X-Api-Key: $COCO_API_KEY"

Create a work item. You need the project's UUID — take it from the response above, or from the project URL in the app.

curl -s -X POST \
  "https://your-instance.example.com/api/v1/workspaces/acme/projects/8d8b1a3e-0000-0000-0000-000000000000/work-items/" \
  -H "X-Api-Key: $COCO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Login times out on mobile",
        "description_html": "<p>Reported by two customers this morning.</p>",
        "priority": "high"
      }'

Only name is required. priority must be one of urgent, high, medium, low, none. state, assignees and labels take UUIDs — fetch them from the states / members / labels endpoints first.


What you can reach

All project-scoped routes live under workspaces/{slug}/projects/{project_id}/….

ResourceRouteMethods
Projectsworkspaces/{slug}/projects/ · …/{id}/GET, POST, PATCH, DELETE
Project archive…/projects/{id}/archive/POST, DELETE
Project summary…/projects/{id}/summary/GET
Work items…/projects/{id}/work-items/ · …/{id}/GET, POST, PATCH, DELETE
Work-item searchworkspaces/{slug}/work-items/search/GET
Work item by keyworkspaces/{slug}/work-items/{PROJ}-{123}/GET
Comments…/work-items/{id}/comments/ · …/{id}/GET, POST, PATCH, DELETE
Links…/work-items/{id}/links/ · …/{id}/GET, POST, PATCH, DELETE
Attachments…/work-items/{id}/attachments/ · …/{id}/GET, POST, PATCH, DELETE
Activity…/work-items/{id}/activities/GET
Relations…/work-items/{id}/relations/GET, POST
Cycles…/projects/{id}/cycles/ · …/{id}/GET, POST, PATCH, DELETE
Cycle work items…/cycles/{id}/cycle-issues/GET, POST, DELETE
Transfer work items…/cycles/{id}/transfer-issues/POST
Cycle archive…/cycles/{id}/archive/ · …/archived-cycles/GET, POST, DELETE
Modules…/projects/{id}/modules/ · …/{id}/GET, POST, PATCH, DELETE
Module work items…/modules/{id}/module-issues/GET, POST, DELETE
Module archive…/modules/{id}/archive/ · …/archived-modules/GET, POST, DELETE
States…/projects/{id}/states/ · …/{id}/GET, POST, PATCH, DELETE
Labels…/projects/{id}/labels/ · …/{id}/GET, POST, PATCH, DELETE
Intake…/projects/{id}/intake-issues/ · …/{id}/GET, POST, PATCH, DELETE
Members…/projects/{id}/members/ · workspaces/{slug}/members/GET, POST, PATCH, DELETE
Stickiesworkspaces/{slug}/stickies/GET, POST, PATCH, DELETE
Invitationsworkspaces/{slug}/invitations/GET, POST, DELETE
Assetsworkspaces/{slug}/assets/ · assets/user-assets/GET, POST, PATCH, DELETE
Youusers/me/GET

An older …/issues/… spelling of the work-item routes still works, but work-items is the current one — use it.


Pagination

List endpoints are cursor-paginated. Two query parameters:

  • per_page — how many results (default 1000, max 1000)
  • cursor — pass back the next_cursor from the previous response

A list response looks like this:

{
  "total_count": 248,
  "count": 100,
  "total_pages": 3,
  "total_results": 248,
  "next_cursor": "100:1:0",
  "prev_cursor": "100:0:1",
  "next_page_results": true,
  "prev_page_results": false,
  "results": [ … ]
}

Loop while next_page_results is true, feeding next_cursor back in as cursor.

Shaping the response

ParameterWhat it does
fieldsComma-separated list — return only these fields
expandComma-separated list — inline related objects instead of just IDs
order_byField to sort by; prefix with - for descending (e.g. -created_at)
external_id / external_sourceLook items up by an ID from another system

external_id + external_source are the pair you want when syncing from somewhere else: create with them set, and you can find (and avoid duplicating) the same record later.


Rate limits

60 requests per minute, per API key. Every response carries:

  • X-RateLimit-Remaining — requests left in the current window
  • X-RateLimit-Reset — Unix timestamp when the window resets

Go over and you get 429 Too Many Requests. Back off until the reset time.

If you're bulk-importing, batch your work and add a small delay between calls — 60/min goes quickly when you're creating a few thousand work items. For a big one-off migration, use the Jira importer instead; it's built for it.


Errors

Errors come back as JSON with an error key:

{ "error": "The requested resource does not exist." }
CodeMeaning
400Invalid payload, missing required key, or a validation failure
401Missing, revoked or expired API key
403Your key is valid, but you don't have permission for that project or action
404The workspace, project or object doesn't exist — or you can't see it
429Rate limit exceeded
500Something broke on our side

A 403 and a 404 are both worth checking against the app: the API enforces exactly the same permissions you have in the UI, so if you can't see a project in the sidebar, your key can't see it either.


Good to know

  • The key is you. It carries your permissions — no more, no less. It doesn't become an admin key.
  • Keys don't expire by default if you choose "never expires". For anything running in CI or a shared script, set an expiry and rotate it.
  • Deletes are real deletes. There's no undo on DELETE.
  • UUIDs everywhere. Projects, work items, states and labels are all identified by UUID, not by the human-readable key you see in the UI. The one exception is workspaces/{slug}/work-items/{PROJ}-{123}/, which looks an item up by its project identifier and number.
  • Attachments are two steps — you ask for an upload slot, then upload the file, then confirm. Read the attachment endpoint's response carefully rather than assuming a single multipart POST.