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/ |
| Auth | An API key in the X-Api-Key header |
| Format | JSON in, JSON out |
| Rate limit | 60 requests per minute per key |
| Trailing slashes | Required. See the warning below. |
Trailing slashes matter. Every route ends in
/. A request without the trailing slash gets a301redirect, and most HTTP clients silently turn the redirectedPOST/PATCHinto aGET— 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.
- Open your profile settings → API tokens (
/settings/profile/api-tokens/). - Click to add a token, give it a title and an optional description.
- Choose an expiry — 1 month, 3 months, 1 year, a custom date, or never expires.
- 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}/….
| Resource | Route | Methods |
|---|---|---|
| Projects | workspaces/{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 search | workspaces/{slug}/work-items/search/ | GET |
| Work item by key | workspaces/{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 |
| Stickies | workspaces/{slug}/stickies/ | GET, POST, PATCH, DELETE |
| Invitations | workspaces/{slug}/invitations/ | GET, POST, DELETE |
| Assets | workspaces/{slug}/assets/ · assets/user-assets/ | GET, POST, PATCH, DELETE |
| You | users/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 thenext_cursorfrom 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
| Parameter | What it does |
|---|---|
fields | Comma-separated list — return only these fields |
expand | Comma-separated list — inline related objects instead of just IDs |
order_by | Field to sort by; prefix with - for descending (e.g. -created_at) |
external_id / external_source | Look 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 windowX-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." }
| Code | Meaning |
|---|---|
400 | Invalid payload, missing required key, or a validation failure |
401 | Missing, revoked or expired API key |
403 | Your key is valid, but you don't have permission for that project or action |
404 | The workspace, project or object doesn't exist — or you can't see it |
429 | Rate limit exceeded |
500 | Something 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.
Related
- Connecting Claude & AI — let Claude call the API for you
- Import from Jira — for bulk migration, don't script it yourself
- Keyboard shortcuts
- Troubleshooting