Api Reference
This page lists every model you can read and every action you can call. See the Quick Guide to Codecks API for tokens and first examples.
Everything listed here is covered by the stability promise. Items marked preview may change in any release, and each change is listed in the API changelog. Models, fields and actions that aren’t listed here are internal: they may work today, but can change or disappear without notice.
Basics
simple syntax example:
{
"_root": [{
"relname($query)": [...fields and relations],
"relname($query2)": [...fields and relations],
}]
}curl example:
curl 'https://api.codecks.io/' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer [TOKEN]' \
--data-binary '{"query":{"_root":[{"account":["name"]}]}}'Owners and admins create a token under Organization Settings → Integrations → API Tokens; see the API guide for what a token may reach.
Query syntax
here’s the syntax for what a $query looks like.
{fieldName1: $val, fieldName2: {"op": $op, "value": $val}}Note that {fieldName1: $val} is a shortcut for {fieldName1: {"op": "eq", "value": $val}}
The $query portion needs to be sent to JSON.stringify() or similar. For readiablity’s sake we display the non-stringified versions here.
If you want to fetch all related items you can leave out the $query portion like this:
...
"deck": [{
"cards": [...fields and relations],
}]
...Possible values for $op
eq: value | nullneq: value | nullin: arraynotIn: arraygt: ordinal valuegte: ordinal valuelt: ordinal valuelte: ordinal valueinOrNull: arraycontains: string (if field is of type string)has: value (if field is of type array)overlaps: array (if field is of type array)search: string if field is searchable (so far only available for thecontentfield within thecardmodel)
Filtering by values of relation fields
fieldName can also be used to access a relation’s properties. For a card this could be e.g {resolvables: {context: ["block", "review"], isClosed: false}}.
This would fetch all cards that have at least one non-closed resolvable with the blockorreview context
Negation:
you can also do {!resolvables: {context: ["block", "review"], isClosed: false}} to say only show cards with no such resolvables.
“in” - shortcut
relname({"projectId": [123, 234]})is equivalent to
relname({"projectId": {"op": "in", value: [123, 234]}})special fields
-
$orcombines multiple queries viaor, sub queries may not contain special fields (beside more $or and $and)relname({"$or": [{"isArchived": true}, {"status": "done"}]}) -
$andcombines multiple queries viaand, sub queries may not contain special fields (beside more $or and $and)relname({"$and": [ {"effort": {"op": "gt", value: 0}}, {"effort": {"op": "lte", value: 5}} ]}) -
$order: OrderExpressionrelname({"deckId": 123, "$order": "createdAt"})Full OrderExpression is
{"field": fieldName, "dir": "desc"|"asc"}[]Shortcuts are
{"field": fieldName, "dir": "desc"|"asc"}-> [{"field": fieldName, "dir": "desc"|"asc"}]fieldName-> [{"field": fieldName, "dir": "asc"}]-fieldName-> [{"field": fieldName, "dir": "desc"}]
-
$first: trueonly works when order is provided, return singleton
relname({"deckId": 123, "$order": "createdAt", "$first": true}) -
$limit: numberonly works when order is provided, there’s always a maximum $limit of 3000
relname({"deckId": 123, "$order": "createdAt", "$limit": 10}) -
$offset: numberonly works when limit (and thus order) is provided
relname({"deckId": 123, "$order": "createdAt", "$limit": 10, "$offset": 20})
Checking for null
null works as a value for eq and neq, and with the shortcut:
cards({"assigneeId": null})
cards({"milestoneId": {"op": "neq", "value": null}})Counting rows
Put count: or exists: in front of a list relation in a field list to get a number or a boolean instead of the rows:
{"_root": [{"account": ["count:cards", "exists:projects"]}]}A query works the same as on the relation itself:
"count:cards({\"status\": \"started\"})"The result is stored on the row under exactly the key you sent, e.g. "count:cards({\"status\":\"started\"})": 12. Only relations that return a list can be counted: count:deck on a card fails with invalid_aggregate.
Repeated relations
Each key in a field list is its own relation, so you can ask for the same relation several times with different queries, next to its counts:
{
"deck([DECK_ID])": [
"count:cards",
{"cards({\"status\": \"started\"})": ["title"]},
{"cards({\"$order\": \"-createdAt\", \"$first\": true})": ["title"]}
]
}The response stores each list under its key as you wrote it. The $first one holds a single id, or null if nothing matched. The cards themselves are listed once under card, whichever of the three returned them.
Id-list relations
A few list relations hold a plain list of ids on the row itself, e.g. a card’s childCards, attachments, inDeps and outDeps. They don’t take a $query: you can select their fields and use count: and exists:, but you can’t filter, order or limit them. Filter on the related model instead, e.g. cards({"parentCardId": "[CARD_ID]"}) for a card’s sub cards.
Root queries by id
root level queries look like this:
{
"modelname($id)": [...fields and relations]
}$id is either a string for single id models or a JSON array for compound ids.
Relations as fields
A relation that points to a single row can also be named in a field list without a nested list. "deck" instead of {"deck": [...]} returns the deck’s id.
To filter by such a relation, use its id: cards({"deckId": "[DECK_ID]"}).
Writing data
Every change goes through an action, sent as a POST with a JSON body:
curl 'https://api.codecks.io/dispatch/cards/create' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer [TOKEN]' \
--data-binary '{"content": "My card", "deckId": "[DECK_ID]"}'- Params marked required have to be sent. All others can be left out, and leaving them out never changes anything else.
- Params the action doesn’t know are ignored without an error. Check the spelling of a param when an action seems to have no effect.
- The response is listed with each action. Most actions answer without a body.
- A token that lacks a scope the action requires gets a
403naming it, e.g.requires card:write.
Scopes
Every action lists the scopes it requires, like card:write. Reading a field needs one too: most fields need <model>:read, a few need another scope, like account.billingEmail, which needs account:readAdmin.
A scope is a model and a verb. No scope includes another one: card:write doesn’t allow reading cards, so a permission that allows both includes card:read as well.
| Verb | Allows |
|---|---|
read |
reading the model’s fields |
readPublic |
reading the fields an Open Decks visitor sees too |
readAdmin |
reading fields only admins see, like account.billingEmail |
readSelf |
reading your own settings on user, like timezone |
write |
creating and changing rows |
writeOwn |
changing rows that belong to the token’s user. For a personal token, that’s you |
writeAny |
changing rows that belong to someone else |
create |
creating a project |
delete |
deleting a project, a deck or a stored file. Removing a file from a card is write |
writeAccess |
changing a project’s defaultUserAccess, i.e. whether the whole team sees it |
The Permission you pick when creating a token decides which scopes it holds. Each permission includes everything of the one before it:
A personal token never holds more than your role in the organization allows. If your role is below the token’s permission, the token acts with the permission your role allows, see Personal tokens.
Reading this reference
| nullmeans the value may benull.- A
timestampis an ISO 8601 string in UTC, like"2026-09-25T13:37:00.000Z". Adayis a date without a time, like"2026-09-25". int32is a whole number.anyis a JSON value the reference doesn’t describe further.- A type like
"a" | "b"lists the values known today. New values may be added at any time, so your code needs to handle values it doesn’t know.