Developers
Recipes
Ready-to-use examples that combine the Tylode API with tools you already use. Each one is complete: copy it, fill in your IDs and token, and run it.
What you need for every recipe
A personal access token. Create one in Settings → API and webhooks → API and plugins → Create token and copy it straight away; it's only shown once. Tokens need the Pro plan. See Authentication.
Your workspace ID. Copy it from Settings → Workspace → Workspace details → Workspace ID → Copy, or ask the API:
bashcurl https://app.tylode.com/api/v1/workspaces \ -H "Authorization: Bearer tylode_pat_xxx"Use the
idof the workspace you want.
Keep your token secret: it can do everything you can do in the app. Store it in environment variables or your platform's secrets, never in code you publish or in a web page. All requests together can make up to 60 calls a minute (see Errors and conventions).
Log an expense with Apple Shortcuts
Log an expense from your iPhone in a few seconds: the shortcut asks for the amount, the concept and the category, and adds a row to the Movimientos table of your Finances page. It uses the Databases API, which accepts column names and option labels, so you don't have to deal with IDs.
Before you build it
If you don't have it yet, create the Finances page from the templates gallery (see Templates). It creates a table called Movimientos with the columns Fecha, Concepto, Tipo, Categoría and Importe. If you've renamed them, use your names below.
Find the table's ID. From a computer:
bashcurl "https://app.tylode.com/api/v1/databases?workspace_id=WORKSPACE_ID" \ -H "Authorization: Bearer tylode_pat_xxx"Copy the
idof the entry whosenameisMovimientos. No computer at hand? Make a throwaway shortcut with Get Contents of URL pointing at the same URL, add the headerAuthorizationwithBearer tylode_pat_xxx, follow it with Quick Look, run it and copy the ID from the result.
Build the shortcut
In the Shortcuts app, create a new shortcut called "Log expense" and add these actions in order:
Ask for Input: input type Number, prompt "Amount".
Ask for Input: input type Text, prompt "Concept".
List: add the items
Casa,Comida,Transporte,Ocio,SaludandOtros.Choose from List: choose from the List of the previous step, prompt "Category".
Format Date: date Current Date, date format Custom, format string
yyyy-MM-dd.Get Contents of URL:
URL:
https://app.tylode.com/api/v1/databases/DATABASE_ID/rowsMethod: POST
Headers:
Authorization→Bearer tylode_pat_xxx, andContent-Type→application/jsonRequest Body: JSON. Add one field
propertiesof type Dictionary, and inside it these Text fields:Key Value Fechathe Formatted Date variable Conceptothe Provided Input from step 2 TipoGastoCategoríathe Chosen Item Importethe Provided Input from step 1
Get Dictionary Value: get the value for key
errorin Contents of URL.If the Dictionary Value has any value: Show Alert with the Dictionary Value. Otherwise: Show Notification "Expense saved".
Step 6 sends exactly this request:
{
"properties": {
"Fecha": "2026-09-23",
"Concepto": "Coffee with Marta",
"Tipo": "Gasto",
"Categoría": "Comida",
"Importe": "2,40"
}
}Sending the amount as text is fine: the API accepts a decimal comma, so 2,40 is stored as the number 2.4. If something doesn't match, for example a category that doesn't exist, the alert in step 8 shows an error that lists the valid options, so you can fix the shortcut from your phone.
Add the shortcut to your Home Screen or run it with Siri ("Log expense"). To log income, duplicate it and change Tipo to Ingreso.
Send a daily agenda to Slack
Every weekday morning, post today's calendar events and the cards due today to a Slack channel. It uses the Calendar API and the Boards API, and a Slack incoming webhook (create one in Slack for the channel you want and copy its URL).
The script
Save this as agenda-slack.mjs. It needs Node.js 18 or later and has no dependencies.
// agenda-slack.mjs
// Env: TYLODE_TOKEN, TYLODE_WORKSPACE_ID, SLACK_WEBHOOK_URL, TZ (e.g. Europe/Madrid)
const BASE = "https://app.tylode.com/api/v1";
const { TYLODE_TOKEN, TYLODE_WORKSPACE_ID, SLACK_WEBHOOK_URL } = process.env;
async function tylode(path) {
const res = await fetch(BASE + path, { headers: { Authorization: `Bearer ${TYLODE_TOKEN}` } });
const json = await res.json();
if (!res.ok) throw new Error(`${res.status} ${path}: ${json.error}`);
return json.data;
}
// Today, in the time zone set in TZ
const start = new Date();
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(end.getDate() + 1);
const today = start.toLocaleDateString("sv-SE"); // YYYY-MM-DD
// 1. Today's events (recurring events come back expanded)
const params = new URLSearchParams({
workspace_id: TYLODE_WORKSPACE_ID,
from: start.toISOString(),
to: end.toISOString(),
});
const events = (await tylode(`/calendar/events?${params}`)).filter((e) => new Date(e.start_at) < end);
// 2. Cards due today, across every board
const boards = await tylode(`/boards?workspace_id=${TYLODE_WORKSPACE_ID}`);
const due = [];
for (const board of boards) {
const cards = await tylode(`/boards/${board.id}/cards`);
for (const card of cards) if (card.due_date === today) due.push({ ...card, board: board.name });
}
// 3. Build the message
const hhmm = (iso) => new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
const lines = [`*Agenda for ${start.toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long" })}*`];
if (events.length === 0) lines.push("No events today.");
for (const e of events) {
const when = e.all_day ? "All day" : e.end_at ? `${hhmm(e.start_at)}–${hhmm(e.end_at)}` : hhmm(e.start_at);
lines.push(`• ${when} ${e.title}`);
}
lines.push("", `*Cards due today (${due.length})*`);
for (const c of due) lines.push(`• ${c.title} _(${c.board})_`);
if (due.length === 0) lines.push("Nothing due.");
// 4. Post to Slack
const slack = await fetch(SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: lines.join("\n") }),
});
if (!slack.ok) throw new Error(`Slack answered ${slack.status}`);
console.log(`Posted ${events.length} events and ${due.length} cards.`);The script makes 2 requests plus one per board, so it stays well under the limit of 60 a minute unless you have dozens of boards. The message looks like this in Slack:
Agenda for Monday 28 September
• 09:30–10:15 Dentist
• 10:00–10:30 Team sync
Cards due today (1)
• Fix the signup email (Product)Run it with GitHub Actions
Put the script in a repository, add three repository secrets (Settings → Secrets and variables → Actions): TYLODE_TOKEN, TYLODE_WORKSPACE_ID and SLACK_WEBHOOK_URL, and add this workflow as .github/workflows/agenda.yml:
name: Daily agenda
on:
schedule:
# 06:00 UTC, Monday to Friday. GitHub schedules always run in UTC.
- cron: "0 6 * * 1-5"
workflow_dispatch: # lets you run it by hand from the Actions tab
jobs:
agenda:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: node agenda-slack.mjs
env:
TZ: Europe/Madrid
TYLODE_TOKEN: ${{ secrets.TYLODE_TOKEN }}
TYLODE_WORKSPACE_ID: ${{ secrets.TYLODE_WORKSPACE_ID }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}TZ decides what "today" means for the script; the cron line decides when it runs, in UTC. Scheduled workflows can start a few minutes late.
Or run it with cron
On any machine that's always on, keep the variables in a .env file next to the script:
# .env
TZ=Europe/Madrid
TYLODE_TOKEN=tylode_pat_xxx
TYLODE_WORKSPACE_ID=WORKSPACE_ID
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZThen add a line with crontab -e. This one runs at 08:00 in the machine's time zone, Monday to Friday:
0 8 * * 1-5 cd "$HOME/tylode-agenda" && set -a && . ./.env && set +a && node agenda-slack.mjs >> agenda.log 2>&1Turn form submissions into board cards
A contact form on your website creates a card on a Leads board for each submission. The browser never sees your token: the form posts to a tiny serverless function, and only that function talks to Tylode. It uses Create a card.
1. Find the board ID
curl "https://app.tylode.com/api/v1/boards?workspace_id=WORKSPACE_ID" \
-H "Authorization: Bearer tylode_pat_xxx"Copy the id of your Leads board. Cards go into the board's first list. To use another list, take its list_id from a card that's already there (GET /boards/BOARD_ID/cards) and add it to the request in step 3.
2. The form
<!-- contact.html -->
<form action="/api/lead" method="post">
<label>Name <input name="name" required></label>
<label>Email <input name="email" type="email" required></label>
<label>Message <textarea name="message" rows="4"></textarea></label>
<!-- Spam trap: people don't see it, bots fill it in -->
<input name="website" tabindex="-1" autocomplete="off" style="position:absolute; left:-9999px" aria-hidden="true">
<button type="submit">Send</button>
</form>3. The function
This version is for Vercel: save it as api/lead.js in a project whose package.json has "type": "module", and set the environment variables TYLODE_TOKEN and TYLODE_LEADS_BOARD_ID. It uses Web-standard Request and Response, so on other platforms (Netlify, Cloudflare Workers…) only the way you export the handler changes.
// api/lead.js
export async function POST(request) {
const form = await request.formData();
// Bots fill in the hidden field: pretend it worked and do nothing
if (form.get("website")) return Response.redirect(new URL("/thanks.html", request.url), 303);
const name = String(form.get("name") ?? "").trim().slice(0, 200);
const email = String(form.get("email") ?? "").trim().slice(0, 200);
const message = String(form.get("message") ?? "").trim().slice(0, 5000);
if (!name || !email) return new Response("Name and email are required.", { status: 400 });
const res = await fetch(
`https://app.tylode.com/api/v1/boards/${process.env.TYLODE_LEADS_BOARD_ID}/cards`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TYLODE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: `${name} <${email}>`,
description: message,
// list_id: "LIST_ID", // optional: a specific list
}),
},
);
if (!res.ok) {
console.error("Tylode answered", res.status, await res.text());
return new Response("We couldn't send your message. Please try again later.", { status: 502 });
}
return Response.redirect(new URL("/thanks.html", request.url), 303);
}Cards are created as the owner of the token, and each one fires the card.created webhook, so you can also get a Slack message for every new lead. If your form gets a lot of traffic, remember the limit of 60 requests a minute per user.
Back up your pages
This script saves every page you can see, in every workspace you belong to, as JSON files on your computer. It uses GET /workspaces and the Pages API.
// backup-pages.mjs (Node.js 18+; env: TYLODE_TOKEN)
import { mkdir, writeFile } from "node:fs/promises";
const BASE = "https://app.tylode.com/api/v1";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function tylode(path) {
const res = await fetch(BASE + path, {
headers: { Authorization: `Bearer ${process.env.TYLODE_TOKEN}` },
});
const json = await res.json();
if (!res.ok) throw new Error(`${res.status} ${path}: ${json.error}`);
await sleep(1100); // stay under 60 requests a minute
return json.data;
}
const stamp = new Date().toISOString().slice(0, 10);
const workspaces = await tylode("/workspaces");
for (const ws of workspaces) {
const dir = `backup-${stamp}/${ws.slug}`;
await mkdir(dir, { recursive: true });
// The list has titles and the tree (parent_page_id), but no content
const pages = await tylode(`/pages?workspace_id=${ws.id}`);
await writeFile(`${dir}/_index.json`, JSON.stringify(pages, null, 2));
// One request per page to get its content
for (const summary of pages) {
const page = await tylode(`/pages/${summary.id}`);
await writeFile(`${dir}/${page.id}.json`, JSON.stringify(page, null, 2));
console.log(`${ws.name} / ${page.title}`);
}
}TYLODE_TOKEN=tylode_pat_xxx node backup-pages.mjsYou get one folder per workspace with an _index.json (every page's title, icon and parent, to rebuild the tree) and one file per page with its full block JSON. At one request a second, 300 pages take about five minutes.
A few honest notes:
- This is a JSON backup, not Markdown. Page content is block JSON (see Page content format). Turning it into Markdown means writing your own converter for the block types you use. If you just need a Markdown copy of a few pages, use Export .md in the app (see Export).
- To restore a page, create a new one with
POST /pagesand send the savedcontentwithPATCH /pages/:id. Block IDs can be reused as they are. - What's not included: database rows (add them with
GET /databases/:id/rowsfrom the Databases API), attached files, comments and page history.
Post webhook events to Discord or Slack
To get a message in a channel whenever something happens in Tylode (a page is created, a card moves, someone joins), you don't need to poll the API: create a webhook in Settings → API and webhooks → Webhooks. Every delivery includes a text field with a ready-made sentence, which is what Slack incoming webhooks expect, so you can paste a Slack incoming webhook URL as the destination. Discord expects the text in a field called content, so point the Tylode webhook at a small function of yours that verifies the signature and forwards text to your Discord webhook as content.
The Webhooks page has everything you need for both: the list of events, the payload, and signature verification in Node.js and Python.
Ask Claude about your workspace via MCP
Connect Claude (or any MCP client) to Tylode and ask in plain language: "What's on my calendar this week?", "Find the pricing page and summarize it", "Create a card on the Product board to fix the signup email". Claude uses the same API as these recipes, with your token and your permissions.
With Claude Code, one command connects it:
claude mcp add --transport http tylode https://app.tylode.com/api/mcp --header "Authorization: Bearer tylode_pat_xxx"Setup for Claude Desktop, Cursor and other clients, and the full list of tools, is in MCP.