Developers
Databases API
The Databases API lets you read the columns and rows of your databases, add new rows, change cells and delete rows, writing values by column name instead of internal IDs. It's what makes it possible to log an expense from your phone or push a form into a table without opening the app.
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 only see databases 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 same tylode() helper as the Pages API:
const BASE = "https://app.tylode.com/api/v1";
const TOKEN = process.env.TYLODE_TOKEN; // tylode_pat_xxx
async function tylode(path, { method = "GET", body } = {}) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${json.error}`);
return json.data;
}How rows are stored
A row's values live in properties, an object keyed by column ID (not by column name). The value depends on the column type:
| Column type | Value in properties |
|---|---|
text, url, phone | String. |
number | Number. |
select | The option ID (not its label). |
date | String in YYYY-MM-DD format. |
checkbox | true or false. |
person | The user ID of the person. |
relation | Array of row IDs from the related database. |
formula, rollup, created_by, last_edited | Nothing: the app calculates these when it shows the table, so they aren't in properties. Use the row's updated_at for the last edit time. |
Empty cells are simply missing from properties. To turn IDs back into names and labels, use the columns from List databases (see Map IDs to names). Learn more about each type in Columns.
Endpoints
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /api/v1/databases | List a workspace's databases with their columns and options | databases:read |
| GET | /api/v1/databases/:id/rows | List a database's rows | databases:read |
| POST | /api/v1/databases/:id/rows | Add a row | databases:write |
| PATCH | /api/v1/databases/:id/rows/:rowId | Change cells in a row | databases:write |
| DELETE | /api/v1/databases/:id/rows/:rowId | Delete a row | databases:delete |
Scopes only matter for OAuth apps.
List databases
GET /api/v1/databases
Returns every database in the workspace that you can see, each with its columns in display order and the options of its select columns.
Scope: databases:read
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
workspace_id | query | string | Yes | The workspace to list. |
Each database has id, name, page_id (the page that contains it) and columns. Each column has id, name, type and options; for select columns, options is a list of { id, label, color }, and for other types it's empty.
curl "https://app.tylode.com/api/v1/databases?workspace_id=WORKSPACE_ID" \
-H "Authorization: Bearer tylode_pat_xxx"const databases = await tylode(`/databases?workspace_id=${WORKSPACE_ID}`);
for (const db of databases) console.log(db.id, db.name);Response 200
{
"data": [
{
"id": "4f8a2c6e-0d3b-4e7a-b1c5-9e2f6a0d4b18",
"name": "Movimientos",
"page_id": "8e3b7f1d-5a9c-4d2e-a6f0-1c4b8e2d7a93",
"columns": [
{ "id": "b1d4e7a0-2c5f-4a8b-9e1d-3f6a9c2e5b80", "name": "Fecha", "type": "date", "options": [] },
{ "id": "c2e5f8b1-3d6a-4b9c-8f2e-4a7b0d3f6c91", "name": "Concepto", "type": "text", "options": [] },
{
"id": "d3f6a9c2-4e7b-4c0d-9a3f-5b8c1e4a7d02",
"name": "Tipo",
"type": "select",
"options": [
{ "id": "e4a7b0d3-5f8c-4d1e-8b4a-6c9d2f5b8e13", "label": "Gasto", "color": "#b4705f" },
{ "id": "f5b8c1e4-6a9d-4e2f-9c5b-7d0e3a6c9f24", "label": "Ingreso", "color": "#6f8a52" }
]
},
{
"id": "a6c9d2f5-7b0e-4f3a-8d6c-8e1f4b7d0a35",
"name": "Categoría",
"type": "select",
"options": [
{ "id": "06b9c2e5-7a0d-4f3b-9c6e-1d4a7b0e3f52", "label": "Casa", "color": "#8a8374" },
{ "id": "17d0e3a6-8c1f-4a4b-9e7d-9f2a5c8e1b46", "label": "Comida", "color": "#7c7f8a" },
{ "id": "28e1f4b7-9d2a-4b5c-8f0e-2a5b8c1f4d63", "label": "Transporte", "color": "#8a7a63" },
{ "id": "39f2a5c8-0e3b-4c6d-9a1f-3b6c9d2a5e74", "label": "Ocio", "color": "#6f7a80" },
{ "id": "4a03b6d9-1f4c-4d7e-8b2a-4c7d0e3b6f85", "label": "Salud", "color": "#857089" },
{ "id": "5b14c7ea-2a5d-4e8f-9c3b-5d8e1f4c7a96", "label": "Ingresos", "color": "#7a8478" },
{ "id": "6c25d8fb-3b6e-4f9a-8d4c-6e9f2a5d8b07", "label": "Otros", "color": "#8a8374" }
]
},
{ "id": "b7d0e3a6-9f2a-4c5d-8a1e-0b3c6f9d2e57", "name": "Importe", "type": "number", "options": [] }
]
}
]
}Errors
| Status | When |
|---|---|
400 | workspace_id is missing. |
List rows
GET /api/v1/databases/:id/rows
Returns every row of a database, ordered by position.
Scope: databases:read
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Database ID, from List databases. |
Each row has id, properties (keyed by column ID, see How rows are stored), position, created_at and updated_at. There's no pagination or filtering: you get all rows and filter them in your code.
curl https://app.tylode.com/api/v1/databases/DATABASE_ID/rows \
-H "Authorization: Bearer tylode_pat_xxx"Response 200
{
"data": [
{
"id": "0c3f6a9d-2b5e-4a8c-b1d4-7e0a3c6f9b28",
"properties": {
"b1d4e7a0-2c5f-4a8b-9e1d-3f6a9c2e5b80": "2026-09-21",
"c2e5f8b1-3d6a-4b9c-8f2e-4a7b0d3f6c91": "Compra semanal",
"d3f6a9c2-4e7b-4c0d-9a3f-5b8c1e4a7d02": "e4a7b0d3-5f8c-4d1e-8b4a-6c9d2f5b8e13",
"a6c9d2f5-7b0e-4f3a-8d6c-8e1f4b7d0a35": "17d0e3a6-8c1f-4a4b-9e7d-9f2a5c8e1b46",
"b7d0e3a6-9f2a-4c5d-8a1e-0b3c6f9d2e57": 42.5
},
"position": 0,
"created_at": "2026-09-21T18:04:12.381204+00:00",
"updated_at": "2026-09-21T18:04:12.381204+00:00"
}
]
}A database that doesn't exist, or that you can't see, returns an empty list rather than an error.
Map IDs to names
Rows use column IDs and option IDs. Combine them with the columns from GET /databases to get something readable:
const databases = await tylode(`/databases?workspace_id=${WORKSPACE_ID}`);
const db = databases.find((d) => d.name === "Movimientos");
const rows = await tylode(`/databases/${db.id}/rows`);
// Turn { "<column id>": "<option id>" } into { "Column name": "Option label" }
function readable(row, columns) {
const out = { id: row.id };
for (const column of columns) {
const value = row.properties[column.id];
if (value === undefined) continue;
if (column.type === "select") {
const option = (column.options ?? []).find((o) => o.id === value);
out[column.name] = option ? option.label : null;
} else {
out[column.name] = value;
}
}
return out;
}
console.log(rows.map((row) => readable(row, db.columns)));
// [{ id: "0c3f6a9d-…", Fecha: "2026-09-21", Concepto: "Compra semanal",
// Tipo: "Gasto", "Categoría": "Comida", Importe: 42.5 }]Add a row
POST /api/v1/databases/:id/rows
Adds a row at the end of the table. You write values by column name (or ID) and select values by option label (or ID); the API translates them.
Scope: databases:write
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Database ID. |
properties | body | object | Yes | An object of column → value. Keys can be column IDs or column names. |
How keys are matched:
- Each key is first compared with the column IDs, then with the column names, ignoring upper and lower case and surrounding spaces.
"importe"finds the column Importe. - A key that matches no column rejects the whole request with a
400, and the error message lists the column names the table has. - Values that are
nullor""are skipped, and columns you don't send stay empty. Nothing is filled in for you: if you don't send a date, the row has no date.
How values are converted, by column type:
| Column type | What to send | What's stored |
|---|---|---|
select | The option label (case-insensitive), e.g. "comida", or the option ID. | The option ID. An unknown label is a 400 whose message lists the valid labels. |
number | A number (24.5) or a numeric string. A decimal comma works too: "24,50". | A number. Anything non-numeric is a 400. |
checkbox | true, "true", 1 or "1" to tick it. Any other value leaves it unticked. | true / false. |
date | A string in YYYY-MM-DD format, e.g. "2026-09-23". | The string as sent. It isn't validated, so stick to that format. |
text, url, phone | A string. Other values are converted to text. | A string. |
person | The person's user ID. | The user ID. |
relation, formula, rollup, created_by, last_edited | Don't send them. Relations can't be set through the API, and the other four are calculated by the app. | — |
curl -X POST https://app.tylode.com/api/v1/databases/DATABASE_ID/rows \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"Fecha": "2026-09-23",
"Concepto": "Comida con cliente",
"Tipo": "Gasto",
"Categoría": "Comida",
"Importe": "24,50"
}
}'const row = await tylode(`/databases/${DATABASE_ID}/rows`, {
method: "POST",
body: {
properties: {
Fecha: "2026-09-23",
Concepto: "Comida con cliente",
Tipo: "Gasto",
"Categoría": "Comida",
Importe: 24.5,
},
},
});
console.log(row.id);Response 201
The row as stored: keys are column IDs and select values are option IDs.
{
"data": {
"id": "7d36e9fc-4c7f-4a0b-9e5d-8f1a4b7c0d39",
"properties": {
"b1d4e7a0-2c5f-4a8b-9e1d-3f6a9c2e5b80": "2026-09-23",
"c2e5f8b1-3d6a-4b9c-8f2e-4a7b0d3f6c91": "Comida con cliente",
"d3f6a9c2-4e7b-4c0d-9a3f-5b8c1e4a7d02": "e4a7b0d3-5f8c-4d1e-8b4a-6c9d2f5b8e13",
"a6c9d2f5-7b0e-4f3a-8d6c-8e1f4b7d0a35": "17d0e3a6-8c1f-4a4b-9e7d-9f2a5c8e1b46",
"b7d0e3a6-9f2a-4c5d-8a1e-0b3c6f9d2e57": 24.5
},
"position": 3
}
}Errors
| Status | When |
|---|---|
400 | properties is missing or isn't an object. |
400 | A key doesn't match any column. The message lists the table's column names. |
400 | A select value isn't one of the column's options. The message lists the valid labels. |
400 | A number column got something that isn't a number. |
404 | The database doesn't exist, you can't see it, or it has no columns. |
500 | The row couldn't be saved. |
Because the error messages list the valid names and labels, you can fix a broken integration by reading the error, even from your phone.
Change cells in a row
PATCH /api/v1/databases/:id/rows/:rowId
Changes only the columns you send. Keys and values work the same way as when you add a row: column name or ID, option label or ID, a decimal comma in numbers, and true or "1" for checkboxes. The rest of the row stays as it is, so if someone else changes another column in the meantime, their change isn't lost.
Scope: databases:write
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Database ID. |
rowId | path | string | Yes | Row ID, from List rows. |
properties | body | object | Yes | An object of column → value, with at least one column. |
Unlike when you add a row, null and "" aren't skipped: they empty the cell. For example, { "properties": { "Fecha": null } } leaves the row without a date.
curl -X PATCH https://app.tylode.com/api/v1/databases/DATABASE_ID/rows/ROW_ID \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"Importe": "26,90",
"Categoría": "Ocio"
}
}'const row = await tylode(`/databases/${DATABASE_ID}/rows/${ROW_ID}`, {
method: "PATCH",
body: { properties: { Importe: 26.9, "Categoría": "Ocio" } },
});Response 200
The whole row as it ends up, with column IDs as keys and option IDs for select values.
{
"data": {
"id": "7d36e9fc-4c7f-4a0b-9e5d-8f1a4b7c0d39",
"properties": {
"b1d4e7a0-2c5f-4a8b-9e1d-3f6a9c2e5b80": "2026-09-23",
"c2e5f8b1-3d6a-4b9c-8f2e-4a7b0d3f6c91": "Comida con cliente",
"d3f6a9c2-4e7b-4c0d-9a3f-5b8c1e4a7d02": "e4a7b0d3-5f8c-4d1e-8b4a-6c9d2f5b8e13",
"a6c9d2f5-7b0e-4f3a-8d6c-8e1f4b7d0a35": "39f2a5c8-0e3b-4c6d-9a1f-3b6c9d2a5e74",
"b7d0e3a6-9f2a-4c5d-8a1e-0b3c6f9d2e57": 26.9
},
"position": 3
}
}Errors
| Status | When |
|---|---|
400 | The database or row ID isn't valid. |
400 | properties is missing, isn't an object or is empty. |
400 | A key doesn't match any column, a select value isn't one of the options, or a number column got something that isn't a number. As when adding, the message lists what's there. |
403 | You can see the row but not change it (for example, because your role is read-only). |
404 | The database or the row doesn't exist, you can't see it, or the database has no columns. |
Delete a row
DELETE /api/v1/databases/:id/rows/:rowId
Deletes a row from a database. Rows have no trash: it can't be recovered.
Scope: databases:delete
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Database ID. |
rowId | path | string | Yes | Row ID. |
curl -X DELETE https://app.tylode.com/api/v1/databases/DATABASE_ID/rows/ROW_ID \
-H "Authorization: Bearer tylode_pat_xxx"Response 200
{ "data": { "id": "7d36e9fc-4c7f-4a0b-9e5d-8f1a4b7c0d39", "papelera": false } }Errors
| Status | When |
|---|---|
400 | The database or row ID isn't valid. |
404 | The row doesn't exist, isn't in that database, or you can't delete it. |
Example: log an expense in your Finances table
The Finances template (see Templates) creates a page with a database called Transactions and these columns: Date (date), Description (text), Type (select: Expense, Income), Category (select: Home, Food, Transport, Leisure, Health, Income, Other) and Amount (number). In another translated language it creates them in that language (in Spanish: Movimientos: Fecha, Concepto, Tipo, Categoría, Importe); the column keys are the same in all of them. If you've renamed the columns or options, use your names in the requests below.
Find the database ID:
bashcurl "https://app.tylode.com/api/v1/databases?workspace_id=WORKSPACE_ID" \ -H "Authorization: Bearer tylode_pat_xxx"Look for the entry whose
nameisMovimientosand copy itsid. You only need to do this once.Add the expense:
jsconst DATABASE_ID = "DATABASE_ID"; async function logExpense({ concept, amount, category, date = new Date() }) { return tylode(`/databases/${DATABASE_ID}/rows`, { method: "POST", body: { properties: { Fecha: date.toLocaleDateString("sv-SE"), // YYYY-MM-DD in your local time Concepto: concept, Tipo: "Gasto", "Categoría": category, Importe: amount, }, }, }); } await logExpense({ concept: "Taxi al aeropuerto", amount: "32,80", category: "Transporte" });Open the Finanzas page in the app: the new row is at the bottom of the table.
The sv-SE locale is just a quick way to get a YYYY-MM-DD date in your own time zone. To do the same thing from an iPhone without writing code, follow the Apple Shortcuts recipe.
What the Databases API doesn't do
- There's no endpoint to create or delete databases and columns. Do that in the app (see Databases).
- Views, filters and sorts aren't exposed; you get the raw rows.
- Relation values can be read but not written.