Developers
Boards API
The Boards API lets you list the boards in a workspace, read their lists and cards, create new cards, change them or move them to another list, and delete them. It's the quickest way to turn things that happen elsewhere (a form, an email, an alert) into cards your team can work on.
Before you start
- Base URL:
https://app.tylode.com/api/v1 - Send your token in the
Authorization: Bearer tylode_pat_xxxheader. Personal access tokens need the Pro plan. See Authentication. - Get your workspace ID with
GET /api/v1/workspaces, or in the app under Settings → Workspace → Workspace details → Workspace ID → Copy. - Every request acts as you, so you see the boards you can see in the app. Errors any endpoint can return (
401,403,429) are covered in Errors and conventions. - The JavaScript examples use the
tylode()helper from the Pages API.
Endpoints
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /api/v1/boards | List a workspace's boards | boards:read |
| GET | /api/v1/boards/:id/lists | List a board's lists (columns) | boards:read |
| GET | /api/v1/boards/:id/cards | List a board's cards | boards:read |
| POST | /api/v1/boards/:id/cards | Create a card | boards:write |
| PATCH | /api/v1/boards/:id/cards/:cardId | Change or move a card | boards:write |
| DELETE | /api/v1/boards/:id/cards/:cardId | Delete a card | boards:delete |
| DELETE | /api/v1/boards/:id | Move a board to the trash | boards:delete |
Scopes only matter for OAuth apps.
List boards
GET /api/v1/boards
Returns the boards in a workspace, oldest first, with their id, workspace_id and name. Boards in the trash aren't included.
Scope: boards:read
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
workspace_id | query | string | Yes | The workspace to list. |
curl "https://app.tylode.com/api/v1/boards?workspace_id=WORKSPACE_ID" \
-H "Authorization: Bearer tylode_pat_xxx"const boards = await tylode(`/boards?workspace_id=${WORKSPACE_ID}`);
const leads = boards.find((b) => b.name === "Leads");Response 200
{
"data": [
{
"id": "1a4d7f0c-3e6b-4c9e-a2f5-8b1d4e7a0c63",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"name": "Product"
},
{
"id": "5e8b1d4f-7a0c-4e3b-9d6f-2c5e8b1a4d70",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"name": "Leads"
}
]
}Errors
| Status | When |
|---|---|
400 | workspace_id is missing. |
List lists
GET /api/v1/boards/:id/lists
Returns the lists (columns) of a board in the order you see them, left to right, with id, board_id, name and position. This is where you get the list_id to create a card in a specific list or to move it to another one.
Scope: boards:read
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Board ID. |
curl https://app.tylode.com/api/v1/boards/BOARD_ID/lists \
-H "Authorization: Bearer tylode_pat_xxx"const lists = await tylode(`/boards/${BOARD_ID}/lists`);
const done = lists.find((l) => l.name === "Done");Response 200
{
"data": [
{
"id": "9b2e5c8f-1d4a-4b7e-a0c3-6f9d2b5e8a17",
"board_id": "1a4d7f0c-3e6b-4c9e-a2f5-8b1d4e7a0c63",
"name": "To do",
"position": 0
},
{
"id": "0c3f6a9d-2e5b-4c8f-b1d4-7a0e3c6f9b28",
"board_id": "1a4d7f0c-3e6b-4c9e-a2f5-8b1d4e7a0c63",
"name": "Doing",
"position": 1
},
{
"id": "3e6b9d2f-5a8c-4e1b-a4d7-0b3e6c9f2a51",
"board_id": "1a4d7f0c-3e6b-4c9e-a2f5-8b1d4e7a0c63",
"name": "Done",
"position": 2
}
]
}A board that doesn't exist, or that you can't see, returns an empty list rather than an error.
Errors
| Status | When |
|---|---|
400 | The board ID isn't valid. |
List cards
GET /api/v1/boards/:id/cards
Returns every card on a board, ordered by position.
Scope: boards:read
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Board ID. |
Each card has:
| Field | Type | Description |
|---|---|---|
id | string | Card ID. |
board_id | string | Board the card is on. |
list_id | string | List (column) the card is in. |
title | string | Card title. |
description | string | Card description (can be empty). |
due_date | string or null | Due date as YYYY-MM-DD. |
position | number | Order within its list. |
Each card has its list's list_id, not its name. To find out what each list is called, use List lists.
curl https://app.tylode.com/api/v1/boards/BOARD_ID/cards \
-H "Authorization: Bearer tylode_pat_xxx"const cards = await tylode(`/boards/${BOARD_ID}/cards`);
const today = new Date().toLocaleDateString("sv-SE"); // YYYY-MM-DD
const dueToday = cards.filter((c) => c.due_date === today);Response 200
{
"data": [
{
"id": "c4f7a0d3-6b9e-4c2f-8a5d-1e4b7c0f3a96",
"board_id": "1a4d7f0c-3e6b-4c9e-a2f5-8b1d4e7a0c63",
"list_id": "9b2e5c8f-1d4a-4b7e-a0c3-6f9d2b5e8a17",
"title": "Fix the signup email",
"description": "The link points to the old domain.",
"due_date": "2026-09-25",
"position": 0
},
{
"id": "d5a8b1e4-7c0f-4d3a-9b6e-2f5c8d1a4b07",
"board_id": "1a4d7f0c-3e6b-4c9e-a2f5-8b1d4e7a0c63",
"list_id": "0c3f6a9d-2e5b-4c8f-b1d4-7a0e3c6f9b28",
"title": "Write release notes",
"description": "",
"due_date": null,
"position": 1
}
]
}A board that doesn't exist, or that you can't see, returns an empty list rather than an error.
Create a card
POST /api/v1/boards/:id/cards
Creates a card on a board. If you don't say which list, it goes into the board's first list.
Scope: boards:write
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Board ID. |
title | body | string | Yes | Card title. |
description | body | string | No | Card description. Defaults to empty. |
list_id | body | string | No | List to put the card in. Defaults to the board's first list (the leftmost one). |
You can't set dates, assignees or other card fields when you create it; the card is created with a title, a description and a list. Set the dates afterwards with Change or move a card, and add assignees and the rest in the app (see Boards).
curl -X POST https://app.tylode.com/api/v1/boards/BOARD_ID/cards \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Call back Laura (Northwind)",
"description": "Wants a demo for 12 people. Phone: +34 600 000 000"
}'const card = await tylode(`/boards/${BOARD_ID}/cards`, {
method: "POST",
body: {
title: "Call back Laura (Northwind)",
description: "Wants a demo for 12 people. Phone: +34 600 000 000",
},
});
console.log(card.id, card.list_id);Response 201
{
"data": {
"id": "e6b9c2f5-8d1a-4e4b-a7c0-3a6d9e2b5c18",
"board_id": "5e8b1d4f-7a0c-4e3b-9d6f-2c5e8b1a4d70",
"list_id": "2d5a8c1f-4e7b-4a0d-9c3e-6b9f2c5a8d41",
"title": "Call back Laura (Northwind)",
"description": "Wants a demo for 12 people. Phone: +34 600 000 000",
"due_date": null,
"position": 0
}
}Errors
| Status | When |
|---|---|
400 | title is missing. |
400 | You didn't send list_id and the board has no lists yet. Create a list in the app first. |
400 | The card couldn't be created (for example, the list_id isn't valid or you can't edit that board). |
Change or move a card
PATCH /api/v1/boards/:id/cards/:cardId
Changes a card's title, description or dates, or moves it to another list on the same board. Only what you send changes; the rest of the card stays as it is.
Scope: boards:write
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Board ID. |
cardId | path | string | Yes | Card ID. |
title | body | string | No | New title. It can't be blank. |
description | body | string | No | New description. "" leaves it empty. |
due_date | body | string or null | No | Due date as YYYY-MM-DD, or null to remove it. |
start_date | body | string or null | No | Start date as YYYY-MM-DD, or null to remove it. |
list_id | body | string | No | List to move it to, from List lists. It has to be on the same board. The card goes to the end of that list. |
Send at least one of the body fields.
curl -X PATCH https://app.tylode.com/api/v1/boards/BOARD_ID/cards/CARD_ID \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{ "list_id": "LIST_ID", "due_date": "2026-10-16" }'// Move a card to the "Done" list
const lists = await tylode(`/boards/${BOARD_ID}/lists`);
const done = lists.find((l) => l.name === "Done");
const card = await tylode(`/boards/${BOARD_ID}/cards/${CARD_ID}`, {
method: "PATCH",
body: { list_id: done.id },
});Response 200
The card as it ends up. On top of the fields from List cards, it includes start_date.
{
"data": {
"id": "c4f7a0d3-6b9e-4c2f-8a5d-1e4b7c0f3a96",
"board_id": "1a4d7f0c-3e6b-4c9e-a2f5-8b1d4e7a0c63",
"list_id": "3e6b9d2f-5a8c-4e1b-a4d7-0b3e6c9f2a51",
"title": "Fix the signup email",
"description": "The link points to the old domain.",
"start_date": null,
"due_date": "2026-10-16",
"position": 4
}
}Errors
| Status | When |
|---|---|
400 | The board or card ID isn't valid. |
400 | You didn't send anything to change, or title is blank. |
400 | due_date or start_date isn't a YYYY-MM-DD date or null. |
400 | list_id isn't a valid ID or isn't a list on this board. |
404 | The card doesn't exist, isn't on that board, or you can't change it. |
Delete a card
DELETE /api/v1/boards/:id/cards/:cardId
Deletes a card from a board. Cards have no trash: it can't be recovered.
Scope: boards:delete
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Board ID. |
cardId | path | string | Yes | Card ID. |
curl -X DELETE https://app.tylode.com/api/v1/boards/BOARD_ID/cards/CARD_ID \
-H "Authorization: Bearer tylode_pat_xxx"Response 200
{ "data": { "id": "c4f7a0d3-6b9e-4c2f-8a5d-1e4b7c0f3a96", "papelera": false } }Errors
| Status | When |
|---|---|
400 | The board or card ID isn't valid. |
404 | The card doesn't exist, isn't on that board, or you can't delete it. |
Move a board to the trash
DELETE /api/v1/boards/:id
Moves a board to the trash with its lists and cards, the same as deleting it in the app. It can be recovered from the trash, and while it's there it doesn't appear in List boards.
Scope: boards:delete
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Board ID. |
curl -X DELETE https://app.tylode.com/api/v1/boards/BOARD_ID \
-H "Authorization: Bearer tylode_pat_xxx"Response 200
{ "data": { "id": "5e8b1d4f-7a0c-4e3b-9d6f-2c5e8b1a4d70", "papelera": true } }Errors
| Status | When |
|---|---|
400 | The ID isn't valid. |
403 | You can see the board, but your role doesn't let you move it to the trash. |
404 | The board doesn't exist, is already in the trash, or you can't see it. |
What all DELETE endpoints have in common is in Deleting: trash or permanent.
Card webhooks
Instead of polling GET /cards, subscribe to card events with a webhook (Settings → API and webhooks → Webhooks; workspace admins on a Pro workspace). They fire for changes made anywhere: in the app, through the API or from a plugin.
| Event | When | data |
|---|---|---|
card.created | A card is created | card_id, board_id, list_id, title, list_name, board_name |
card.moved | A card changes list | card_id, board_id, from_list_id, to_list_id, title, list_name, board_name |
Creating a card with POST /boards/:id/cards fires card.created too, and moving it with PATCH /boards/:id/cards/:cardId (changing list_id) fires card.moved, so avoid webhook loops if you create or move cards from a webhook handler.
Example: create a card from a form submission
A contact form on your website can drop each submission into a Leads board. The form posts to a small server-side function that holds your token and calls the API; the token never reaches the browser.
// api/lead.js: a serverless function (Web-standard Request/Response)
const BOARD_ID = process.env.TYLODE_LEADS_BOARD_ID;
export async function POST(request) {
const form = await request.formData();
const name = String(form.get("name") ?? "").trim();
const email = String(form.get("email") ?? "").trim();
const message = String(form.get("message") ?? "").trim();
if (!name || !email) return new Response("Name and email are required", { status: 400 });
const res = await fetch(`https://app.tylode.com/api/v1/boards/${BOARD_ID}/cards`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TYLODE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: `${name} <${email}>`,
description: message,
}),
});
if (!res.ok) return new Response("Couldn't save your message", { status: 502 });
return Response.redirect(new URL("/thanks", request.url), 303);
}The complete version, with the HTML form and a spam trap, is in Recipes.
Related
- Boards in the app
- Webhooks
- Calendar API, for a daily agenda that also lists cards due today
- Recipes