Create, read, edit, move and delete, plus the two batch routes.
The card is the first-class entity: tenantId is required at creation and immutable from then on. Before sending columnId, check which column set holds for the card.
POST/api/cards
Role: MemberWorkspace
Creates a card, from scratch or from a template.
title and columnId stop being required once templateId arrives: the composition is “whatever came in the request wins, the template fills the rest”. Whatever is still empty after that is refused with 422.
Request body
Field
Type
Description
tenantIdrequired
cuid
Required and IMMUTABLE: the card is born inside a workspace and never leaves it.
templateId
cuid | null
The template that fills whatever the body did not bring. It must be from the same workspace.
title
string
description
string
columnId
cuid
Must belong to the set in effect for this card; see columnScope.
projectId
cuid | null
null leaves the card explicitly without a project.
epicId
cuid | null
Must be an epic of the SAME project as the card.
branch
string | null
A free field. An empty string is normalised to null.
dueAt
ISO 8601 | null
ISO 8601 instant. Without dueAtZone, the due date does not carry the zone it was declared in.
dueAtZone
IANA | null
The zone the due date was DECLARED in (America/Sao_Paulo). It is what makes the same day hold for every reader.
tagIds
cuid[]
The FINAL list of labels, all from the same workspace.
Request
curl-s-X POST http://localhost:3000/api/cards \-H"Authorization: Bearer $TOKEN"\-H"Content-Type: application/json"\-d'{
"tenantId": "cmsp4djx60002p801o7ybpkv7",
"title": "Publicar a documentação",
"description": "Site estático com a referência da API.",
"columnId": "cmttg1vdl000tlf01uuqhz8ad",
"projectId": "cmttg1vjy000zlf015g097nhi",
"branch": "feature/api-documentation",
"tagIds": ["cmttg1vnk0011lf01q0eswms5"]
}'
const response =awaitfetch("http://localhost:3000/api/cards",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${token}`,},body: JSON.stringify({
tenantId,title:"Publicar a documentação",
columnId,// The template fills in whatever the body did not bring.
templateId,}),});if(!response.ok){const{ error }=await response.json();thrownewError(error);}const card =await response.json();
Response201
{"id":"cmttg1vv00017lf01ol5bnlx2","number":198,"title":"Publicar a documentação","description":"Site estático com a referência da API.","columnId":"cmttg1vdl000tlf01uuqhz8ad","projectId":"cmttg1vjy000zlf015g097nhi","epicId":"cmttg1vs50015lf010l6k38uq","branch":"feature/api-documentation","dueAt":"2026-11-01T23:58:59.999Z","dueAtZone":"America/Sao_Paulo","favorite":false,"position":0,"createdAt":"2026-09-09T01:52:58.764Z","updatedAt":"2026-09-09T01:52:58.764Z","tags":[{"id":"cmttg1vnk0011lf01q0eswms5","name":"api","color":"mint"}],"attachments":[],"commentCount":0}
Error responses
Code
When it happens
401
No credential, or one that no longer holds. Send the session cookie or the Authorization: Bearer header.
403
The account does not reach the resource's workspace. It is 403 and not 404 on purpose: the answer reveals no workspace.
404
The resource does not exist, or has already been deleted.
422
The given epic does not belong to the card's project. The rule holds in both directions: changing the card's project is checked too.
422
The body did not pass the schema. The details field carries zod's fieldErrors and formErrors, field by field.
The card is born in the first column of the set and at the top, with a sequential number that is never reused, and creation already writes the genesis of the history. Entering an epic with a date target may inherit its due date.
GET/api/cards/[id]
Role: ViewerWorkspace
One card, with labels, attachments and the comment count.
{"id":"cmttg1vv00017lf01ol5bnlx2","number":198,"title":"Publicar a documentação","description":"Site estático com a referência da API.","columnId":"cmttg1vdl000tlf01uuqhz8ad","projectId":"cmttg1vjy000zlf015g097nhi","epicId":"cmttg1vs50015lf010l6k38uq","branch":"feature/api-documentation","dueAt":"2026-11-01T23:58:59.999Z","dueAtZone":"America/Sao_Paulo","favorite":false,"position":0,"createdAt":"2026-09-09T01:52:58.764Z","updatedAt":"2026-09-09T01:52:58.764Z","tags":[{"id":"cmttg1vnk0011lf01q0eswms5","name":"api","color":"mint"}],"attachments":[],"commentCount":0}
Error responses
Code
When it happens
401
No credential, or one that no longer holds. Send the session cookie or the Authorization: Bearer header.
403
The account does not reach the resource's workspace. It is 403 and not 404 on purpose: the answer reveals no workspace.
404
The resource does not exist, or has already been deleted.
favorite is the answer for WHOEVER ASKED: the mark is personal and lives on a row of its own, so two readers get different values for the same card.
PATCH/api/cards/[id]
Role: MemberWorkspace
Edits a card. An absent field is not touched.
Takes { dueAt } or { favorite } on their own: it is what the star and the due-date picker use, with no dedicated route for each.
Path parameters
Field
Type
Description
idrequired
cuid
Request body
Field
Type
Description
title
string
description
string
columnId
cuid
Changing column here writes history, just like dragging.
projectId
cuid | null
epicId
cuid | null
null takes the card out of the epic; the card stays in the project.
branch
string | null
dueAt
ISO 8601 | null
dueAtZone
IANA | null
Absent PRESERVES the stored zone: it does not fall back to the caller's zone.
favorite
boolean
A personal mark. No epic, due-date or history rule is triggered by this field.
tagIds
cuid[]
The FINAL list: whatever does not come here is removed from the card.
Request
# the star in the interface sends only thiscurl-s-X PATCH http://localhost:3000/api/cards/cmttg1vv00017lf01ol5bnlx2 \-H"Authorization: Bearer $TOKEN"\-H"Content-Type: application/json"\-d'{ "favorite": true }'
// Due date and zone travel together: that is what makes the day hold for everyone.awaitfetch(`http://localhost:3000/api/cards/${cardId}`,{method:"PATCH",headers:{"Content-Type":"application/json",Authorization:`Bearer ${token}`,},body: JSON.stringify({dueAt:"2026-10-15T12:00:00.000Z",dueAtZone:"America/Sao_Paulo",}),});
Response200
{"id":"cmttg1vv00017lf01ol5bnlx2","number":198,"title":"Publicar a documentação","description":"Site estático com a referência da API.","columnId":"cmttg1vdl000tlf01uuqhz8ad","projectId":"cmttg1vjy000zlf015g097nhi","epicId":"cmttg1vs50015lf010l6k38uq","branch":"feature/api-documentation","dueAt":"2026-11-01T23:58:59.999Z","dueAtZone":"America/Sao_Paulo","favorite":true,"position":0,"createdAt":"2026-09-09T01:52:58.764Z","updatedAt":"2026-09-09T01:52:58.764Z","tags":[{"id":"cmttg1vnk0011lf01q0eswms5","name":"api","color":"mint"}],"attachments":[],"commentCount":0}
Error responses
Code
When it happens
401
No credential, or one that no longer holds. Send the session cookie or the Authorization: Bearer header.
403
The account does not reach the resource's workspace. It is 403 and not 404 on purpose: the answer reveals no workspace.
404
The resource does not exist, or has already been deleted.
422
The body came empty. An absent field means do not touch, so a PATCH with no field would have no effect at all.
422
The given epic does not belong to the card's project. The rule holds in both directions: changing the card's project is checked too.
Clearing a field takes an explicit value (null). Absent means “do not touch”, and that is what lets you send a single field.
DELETE/api/cards/[id]
Role: MemberWorkspace
Deletes a card, with its comments, links, history and the files on the volume.
No credential, or one that no longer holds. Send the session cookie or the Authorization: Bearer header.
403
The account does not reach the resource's workspace. It is 403 and not 404 on purpose: the answer reveals no workspace.
404
The resource does not exist, or has already been deleted.
POST/api/cards/[id]/move
Role: MemberWorkspace
Moves the card to a column and a position: this is where dragging goes through.
The position arrives as afterCardId, and never as an index: with a filter on, the visible index does not match the column's real index.
Path parameters
Field
Type
Description
idrequired
cuid
Request body
Field
Type
Description
columnIdrequired
cuid
The destination column, from the set in effect for this card.
afterCardIdrequired
cuid | null
The card this one goes after. null drops it at the top of the column.
Request
# afterCardId null drops the card at the TOP of the columncurl-s-X POST http://localhost:3000/api/cards/cmttg1vv00017lf01ol5bnlx2/move \-H"Authorization: Bearer $TOKEN"\-H"Content-Type: application/json"\-d'{
"columnId": "cmttg1vdl000ulf01nhqhd23w",
"afterCardId": null
}'
Response200
{"id":"cmttg1vv00017lf01ol5bnlx2","number":198,"title":"Publicar a documentação","description":"Site estático com a referência da API.","columnId":"cmttg1vdl000ulf01nhqhd23w","projectId":"cmttg1vjy000zlf015g097nhi","epicId":"cmttg1vs50015lf010l6k38uq","branch":"feature/api-documentation","dueAt":"2026-11-01T23:58:59.999Z","dueAtZone":"America/Sao_Paulo","favorite":false,"position":0,"createdAt":"2026-09-09T01:52:58.764Z","updatedAt":"2026-09-09T01:52:58.764Z","tags":[{"id":"cmttg1vnk0011lf01q0eswms5","name":"api","color":"mint"}],"attachments":[],"commentCount":0}
Error responses
Code
When it happens
401
No credential, or one that no longer holds. Send the session cookie or the Authorization: Bearer header.
403
The account does not reach the resource's workspace. It is 403 and not 404 on purpose: the answer reveals no workspace.
404
The resource does not exist, or has already been deleted.
422
The column does not belong to the set in effect for this card. Which set holds is decided by columnScope.
Changing column writes a history row; reordering within the same column writes nothing.
PATCH/api/cards/bulk
Role: MemberWorkspace
Edits the details of several cards at once.
Labels travel as ADD/REMOVE rather than as the final list a single card's form sends: replacing the list would erase the labels each card already had.
Request body
Field
Type
Description
cardIdsrequired
cuid[]
From 1 to 200 cards. Every workspace involved is checked.
branch
string | null
dueAt
ISO 8601 | null
null clears the due date of the cards in the batch.
dueAtZone
IANA | null
addTagIds
cuid[]
Labels to add. An id cannot be here and in removeTagIds.
No credential, or one that no longer holds. Send the session cookie or the Authorization: Bearer header.
403
The account does not reach the resource's workspace. It is 403 and not 404 on purpose: the answer reveals no workspace.
404
The resource does not exist, or has already been deleted.
422
The batch picked no field to change, or asked to add and remove the same label.
The batch refuses as a whole: if one card breaks a rule, none is changed. Title and column are left out: those are card-by-card decisions.
PATCH/api/cards/epic
Role: MemberWorkspace
Moves cards into, out of and between epics.
The three are the same operation: writing epicId (or null) on a set of cards.
Request body
Field
Type
Description
cardIdsrequired
cuid[]
epicIdrequired
cuid | null
null takes the cards out of the epic; they stay in the project.
alignDueAt
boolean
An EXPLICIT opt-in to overwrite the date of whoever is due after the epic's target. Without it the batch refuses instead of clearing a chosen date.When absent: false