Base & Tables API
Manage bases, tables, fields, and records. This router provides full CRUD operations for your database infrastructure.
Endpoints
/api/v1/tables/createDatabaseCreate a new database (base). Params: name (required), description, icon, color (hex like "#3B82F6"). Returns the created database with its ID.
Parameters
name | stringrequired | Name for the new database/base |
description | string | Optional description of the database |
icon | string | Icon name (e.g., "database", "folder") |
color | string | Hex color code (e.g., "#3B82F6") |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/createDatabase \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "example",
"description": "example",
"icon": "example"
}'/api/v1/tables/listDatabasesList all databases (bases) for the current user. Returns array with id, name, description, tables[]. Use this first to get database IDs.
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/listDatabases \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{}'/api/v1/tables/getDatabaseByIdGet a specific database by ID. Param "id" is a database ID from listDatabases. Returns database with its tables.
Parameters
id | stringrequired | Database ID from listDatabases |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getDatabaseById \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example"
}'/api/v1/tables/updateDatabaseUpdate a database. Params: id (database ID), name, description, icon, color. Only provided fields are updated.
Parameters
id | stringrequired | Database ID from listDatabases |
name | string | New name for the database |
description | string | New description |
icon | string | New icon name |
color | string | New hex color code |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateDatabase \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example",
"name": "example",
"description": "example"
}'/api/v1/tables/deleteDatabaseDelete a database and all its tables. Param "id" is the database ID. WARNING: This is permanent.
Parameters
id | stringrequired | Database ID to delete. WARNING: This permanently deletes all tables and data. |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/deleteDatabase \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example"
}'/api/v1/tables/createTableCreate a new table. Params: databaseId (required, from listDatabases), name (required), description, icon, color. A table a site's pages use goes in that site's linked base (site_getProject → linkedBase.id); a site reads another base's table only through an account function (functions.invokeAccount; automations_createFunction + grants.bases + accessRules.execute). Recreating tables in another base, or copying or moving rows between bases, is work beyond the request: ask the person first. A base another site links: offer the account-function route (no copy, recommended) or the move. A base no site links (no account function reaches it): offer the copy into this site's base (tables_duplicateTable with includeRows: true; recommended, the only way the site can show it) or leaving it.
Parameters
databaseId | stringrequired | Database ID from listDatabases where the table will be created |
name | stringrequired | Name for the new table |
description | string | Optional description of the table |
icon | string | Icon name (e.g., "table", "users") |
color | string | Hex color code (e.g., "#10B981") |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/createTable \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"databaseId": "example",
"name": "example",
"description": "example"
}'/api/v1/tables/listTablesList all tables across all databases. Returns array with id, name, databaseId. Use getTableById for full details.
Parameters
databaseId | string | Limit to one base — omit for all tables across all bases |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/listTables \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"databaseId": "example"
}'/api/v1/tables/getViewsByTableIdAlias for getViews. Param "tableId".
Parameters
tableId | stringrequired | Table ID from listTables |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getViewsByTableId \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example"
}'/api/v1/tables/getFieldsByTableIdGet all fields/columns for a table. Param "tableId" from listTables. Returns array with field id, name, fieldType, options.
Parameters
tableId | stringrequired | Table ID from listTables |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getFieldsByTableId \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example"
}'/api/v1/tables/getTableByIdGet table with fields and views. Param "id" is a table ID from listTables. Returns table with fields[], views[], database info.
Parameters
id | stringrequired | Table ID from listTables |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getTableById \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example"
}'/api/v1/tables/updateTableUpdate table properties. Params: id (table ID), name, description, icon, color. Renaming a table a function with Full data access reads by name (or renaming another into that name) is prepared as a card a person taps: the answer carries pendingChange and nothing changes until they approve. A function with Full data access whose code reaches the tables counts as reading every table of its base.
Parameters
id | stringrequired | Table ID from listTables |
name | string | New name for the table |
description | string | New description |
icon | string | New icon name |
color | string | New hex color code |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateTable \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example",
"name": "example",
"description": "example"
}'/api/v1/tables/deleteTableDelete a table and all its rows. Param "id" is the table ID. WARNING: This is permanent. Deleting a table a function with Full data access reads by name is prepared as a card a person taps (pendingChange; it can't be undone once applied).
Parameters
id | stringrequired | Table ID to delete. WARNING: This permanently deletes all rows and fields. |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/deleteTable \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example"
}'/api/v1/tables/createFieldCreate a new field/column. Params: tableId (required), name (required), fieldType (required: "text", "longtext", "number", "email", "date", "datetime", "checkbox", "select", "multiselect", "url", "phone", "currency", "percent", "rating", "duration", "file", "link", "lookup", "rollup", "formula", "button", "status", "slug", "hash", "color", "json", "secret", "created_time", "updated_time" — note: file uploads use "file", there is no "attachment" type; "secret" = encrypted at rest and MASKED in every read (grid, API, exports, even function reads); nobody reads it back, the owner included, it can only be replaced — right for sensitive data IN records, but code cannot use a secret field's value, so API keys belong in env/Secrets, not here). Optional: description, unique, defaultValue, options (for select/multiselect: a FLAT array [{value, label, color?}] — label is required, color is #RRGGBB; no "choices" wrapper and no "id"). Note: "required" is not supported — pass false or omit it.
Parameters
tableId | stringrequired | Table ID from listTables |
name | stringrequired | Name for the new field/column |
fieldType | stringrequired | Field type: "text", "number", "select", "multiselect", "date", "datetime", "checkbox", "url", "email", "phone", "file", "formula", "rating", "currency", "percent", "duration", "status", "button", "link", "lookup", "rollup". READ-ONLY computed types (writes to them are dropped): "hash" = Hash ID, a read-only stable per-row identifier — it does NOT hash a value you supply (to store a hashed value, compute it yourself and use text/secret); "created_time"/"updated_time" = system timestamps; "formula"/"lookup"/"rollup" = computed. Numbers are exact only up to 2^53 — unsafe integers are rejected with guidance; store long IDs, account numbers, and IMEIs as text. |
description | string | Optional description of the field |
required | boolean | Must be false or omitted. Required fields are not supported - UI creates blank rows first. |
unique | boolean | Whether values must be unique (default: false) |
defaultValue | any | Default value for new rows |
options | array | For select/multiselect: [{value: "opt1", label: "Option 1", color: "#3b82f6"}] |
statusLabels | array | For status: [{value: "todo", label: "To Do", color: "#6b7280"}] |
formulaConfig | object | For formula. Values of a formula without NOW()/TODAY() are stored on the row, recalculated when inputs change (also through links) and filterable everywhere; with NOW()/TODAY() they are display-only and not filterable |
buttonConfig | object | For button: {actionType: "flow", flowId: "xxx"} |
numberFormatConfig | object | For number: {decimalPlaces: 2, useThousandSeparator: true} |
currencyFormatConfig | object | For currency: {currencySymbol: "$", symbolPosition: "before"} |
percentFormatConfig | object | For percent: {decimalPlaces: 1} |
lookupConfig | object | For lookup. Its COUNT counts NON-EMPTY VALUES (a rollup COUNT counts linked records). Scalar (non-chained) aggregations are filterable everywhere (view filters, filterRows, entities.filter()) |
rollupConfig | object | For rollup. COUNT counts LINKED RECORDS, empty values included (COUNT_FILLED = lookup COUNT). Scalar (non-chained) aggregations are filterable everywhere (view filters, filterRows, entities.filter()) |
linkedTableId | string | For link field: target table ID |
linkType | string | Link relationship type |
displayOrder | string | Fractional order string (e.g., "a0", "b0") |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/createField \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"name": "example",
"fieldType": "example"
}'/api/v1/tables/updateFieldUpdate field properties. Params: id (field ID from getFieldsByTableId), name, fieldType, description, unique, options (flat [{value, label, color?}]), etc. Same field-type set as createField; "required" must be false or omitted. Renaming, retyping, or changing what it links to or looks up, a field of a table a function with Full data access reads by name is prepared as a card a person taps (pendingChange): send that change on its own.
Parameters
id | stringrequired | Field ID from getFieldsByTableId |
name | string | New name for the field |
fieldType | string | Field type |
description | string | New description |
required | boolean | Must be false or omitted. Required fields are not supported - UI creates blank rows first. |
unique | boolean | Whether values must be unique |
defaultValue | any | Default value for new rows |
options | array | For select/multiselect: [{value: "opt1", label: "Option 1", color: "#3b82f6"}] |
statusLabels | array | For status: [{value: "todo", label: "To Do", color: "#6b7280"}] |
formulaConfig | object | For formula. Values of a formula without NOW()/TODAY() are stored on the row, recalculated when inputs change (also through links) and filterable everywhere; with NOW()/TODAY() they are display-only and not filterable |
linkedTableId | string | For link field: target table ID |
linkType | string | Link relationship type |
linkDisplayMode | string | How to display linked records |
linkDisplayFieldId | string | Which field to show from linked records |
allowMultipleLinks | boolean | Whether multiple links are allowed |
linkOnDelete | string | For link fields: what happens to linking rows when a linked record is deleted. RESTRICT blocks the delete; CASCADE deletes the linking rows too (chains, capped at 500 rows per delete); SET_NULL (default) just removes the connection. |
buttonConfig | object | For button: {actionType: "flow", flowId: "xxx"} |
numberFormatConfig | object | For number: {decimalPlaces: 2, useThousandSeparator: true} |
currencyFormatConfig | object | For currency: {currencySymbol: "$", symbolPosition: "before"} |
percentFormatConfig | object | For percent: {decimalPlaces: 1} |
lookupConfig | object | For lookup. Its COUNT counts NON-EMPTY VALUES (a rollup COUNT counts linked records). Scalar (non-chained) aggregations are filterable everywhere (view filters, filterRows, entities.filter()) |
rollupConfig | object | For rollup. COUNT counts LINKED RECORDS, empty values included (COUNT_FILLED = lookup COUNT). Scalar (non-chained) aggregations are filterable everywhere (view filters, filterRows, entities.filter()) |
displayOrder | number | Display order position |
isVisible | boolean | Whether field is visible |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateField \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example",
"name": "example",
"fieldType": "example"
}'/api/v1/tables/deleteFieldDelete a field/column. Param "id" is the field ID. WARNING: Deletes all data in this column. Deleting a field of a table a function with Full data access reads by name is prepared as a card a person taps (pendingChange; it can't be undone once applied).
Parameters
id | stringrequired | Field ID to delete. WARNING: This permanently deletes all data in this column. |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/deleteField \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example"
}'/api/v1/tables/createRowCreate a new row. Params: tableId (required), data (object keyed by field ID from getFieldsByTableId — field NAMES are also accepted and auto-normalized, but IDs are safest since names can change). Example: {tableId: "xxx", data: {"fieldId1": "text value", "fieldId2": 123}}.
Parameters
tableId | stringrequired | Table ID from listTables |
data | objectrequired | Object where keys are FIELD IDs from getFieldsByTableId, values are cell values. Example: {"fieldId1": "text", "fieldId2": 123} |
rowOrder | string | Fractional order string for positioning. Leave empty to append at end. |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/createRow \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"data": {},
"rowOrder": "example"
}'/api/v1/tables/reportAnswer a numbers question about a table in one call (counts, totals, averages per slice) — never write SQL. Params: tableId; measures (1-8: {as, fn: count|countDistinct|sum|avg|min|max|median|ratio, field: {fieldId}, filter?, numerator?/denominator? (ratio: the "as" of two other measures), decimals? 0-6}); filter? (which rows count — the view filter language {logic:"and"|"or", conditions:[{fieldId, operator, value}]}, up to 30 conditions, a nested group counting as one, 3 levels); groupBy? (up to 3: {as, field: {fieldId}, bucket?: day|week|month|quarter|year|hour|dayOfWeek for date fields}); having? ({measure, operator, value}); sort? ({by: an "as", direction}); limit? (1-1000, default 100) and offset? (pages, hasMore says more exist); percentOfTotal? (the "as" of a count or sum: each slice's share of ALL slices, 0-1); timeZone? (IANA, default UTC; week starts Monday). A measure with a filter counts only its rows (win rate = ratio of a filtered count over count). Empty values form one null slice. Returns {columns, rows: [{groups, measures, percent?}], hasMore, total, timeZone, truncatedAt?}. Not yet possible: grouping through a linked record, lookup/rollup fields, running totals, period-over-period in one report, cohorts, two-level aggregates (use two reports). Sign-in and secret fields can never be used. Example: {tableId, filter:{logic:"and",conditions:[{fieldId:"<Status id>",operator:"equals",value:"Paid"}]}, groupBy:[{as:"month",field:{fieldId:"<Created id>"},bucket:"month"}], measures:[{as:"orders",fn:"count"},{as:"revenue",fn:"sum",field:{fieldId:"<Amount id>"}}]}.
Parameters
filter | any | Which rows count: {logic: "and"|"or", conditions: [{fieldId, operator, value}]} — the view filter language |
groupBy | array | Up to 3 slices |
measures | arrayrequired | 1 to 8 numbers to work out per slice |
having | array | Keep only slices whose measure passes (up to 4) |
sort | array | Order by group or measure names (up to 3) |
limit | integer | Slices per answer, 1-1000 (default 100) |
offset | integer | Slices to skip, for the next page (max 100000) |
percentOfTotal | string | The `as` of a count or sum measure: adds each slice's share of the whole (0-1) |
timeZone | string | IANA time zone for date slices, e.g. "Europe/Berlin" (default UTC) |
tableId | stringrequired | Table ID from listTables |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/report \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"filter": {},
"groupBy": {},
"measures": {}
}'/api/v1/tables/getRowCountGet total row count. Param "tableId". Returns {count: number}.
Parameters
tableId | stringrequired | Table ID from listTables |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getRowCount \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example"
}'/api/v1/tables/getRowsGet rows from a table. Params: tableId (required), limit (default 100), offset (default 0). Returns {rows: [{id, data: {fieldId: value}}], total}. Data keys are field IDs, not names.
Parameters
tableId | stringrequired | Table ID from listTables |
limit | number | Rows to return, 1-1000 (default: 100). Values outside the range are REJECTED, not clamped — page with offset for more |
offset | number | Number of rows to skip for pagination |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getRows \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"limit": 0,
"offset": 0
}'/api/v1/tables/updateRowUpdate a row. Params: id (row ID from getRows), data (partial object keyed by field ID; exact field names are also accepted). Only provided fields are updated.
Parameters
id | stringrequired | Row ID from getRows |
data | objectrequired | Object with field IDs as keys and new values. Only provided fields are updated. |
metadata | object | Optional metadata to merge with existing |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateRow \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example",
"data": {},
"metadata": {}
}'/api/v1/tables/restoreRowUndo a row delete: bring back a row this same person deleted (with deleteRow or bulkDeleteRows) in the last ten minutes, at its old id, exactly as it was (its values are the copy kept when it was deleted). Params: tableId, rowId (the deleted row's id), rowOrder? (a row position the grid made, e.g. one taken from getRows; anything else is refused; default: where it was). Each delete can be undone once. A row deleted by someone else, longer ago, or never deleted answers "Row not found". Returns { id, tableId, data, rowOrder, createdAt, updatedAt } (secret values masked).
Parameters
tableId | stringrequired | The table the row was deleted from |
rowId | stringrequired | The deleted row's id |
rowOrder | string | Where to put it back (a row position the grid made; a bad one is refused); default: where it was |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/restoreRow \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"rowId": "example",
"rowOrder": "example"
}'/api/v1/tables/deleteRowDelete a row. Param "id" is the row ID.
Parameters
id | stringrequired | Row ID to delete |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/deleteRow \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example"
}'/api/v1/tables/bulkDeleteRowsDelete multiple rows. Params: tableId, rowIds (array of row IDs).
Parameters
tableId | stringrequired | |
rowIds | arrayrequired |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/bulkDeleteRows \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"rowIds": {}
}'/api/v1/tables/createViewCreate a new view. Params: tableId (required), name (required), viewType (required: "grid", "kanban", "calendar", "gallery", "form"), description, filterConfig, sortConfig, groupConfig, visibleFields (array of field IDs).
Parameters
tableId | stringrequired | |
name | stringrequired | |
viewType | stringrequired | |
description | string | |
filterConfig | any | |
sortConfig | any | |
groupConfig | any | |
visibleFields | array | |
columnWidths | any | |
isDefault | boolean |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/createView \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"name": "example",
"viewType": "example"
}'/api/v1/tables/updateViewUpdate view settings. Params: id (view ID), name, filterConfig, sortConfig, groupConfig, visibleFields, etc.
Parameters
id | stringrequired | |
name | string | |
description | string | |
filterConfig | any | |
sortConfig | any | |
groupConfig | any | |
visibleFields | array | |
columnWidths | any | |
isDefault | boolean | |
metadata | any |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateView \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example",
"name": "example",
"description": "example"
}'/api/v1/tables/getViewsList all views for a table. Param "tableId". Returns array with id, name, viewType, filterConfig, sortConfig.
Parameters
tableId | stringrequired |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getViews \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example"
}'/api/v1/tables/reorderViewChange view position. Params: viewId, newOrderIndex (string).
Parameters
viewId | stringrequired | |
newOrderIndex | stringrequired |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/reorderView \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"viewId": "example",
"newOrderIndex": "example"
}'/api/v1/tables/duplicateViewCopy a view. Params: id (view ID to copy), name (optional, for the copy).
Parameters
id | stringrequired | |
name | string |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/duplicateView \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example",
"name": "example"
}'/api/v1/tables/setDefaultViewSet which view opens by default. Params: id (view ID), tableId.
Parameters
id | stringrequired | |
tableId | stringrequired |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/setDefaultView \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example",
"tableId": "example"
}'/api/v1/tables/deleteViewDelete a view. Param "id" is the view ID.
Parameters
id | stringrequired |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/deleteView \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "example"
}'/api/v1/tables/updateFieldOrderChange field position. Params: fieldId, newOrder (fractional string).
Parameters
fieldId | stringrequired | |
newOrder | stringrequired |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateFieldOrder \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"fieldId": "example",
"newOrder": "example"
}'/api/v1/tables/updateTableOrderChange table position in database. Params: tableId, newOrder (string like "a0", "b0" for fractional ordering).
Parameters
tableId | stringrequired | |
newOrder | stringrequired |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateTableOrder \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"newOrder": "example"
}'/api/v1/tables/updateFieldWidthSet column width in pixels. Params: fieldId, width (number, e.g., 200).
Parameters
fieldId | stringrequired | |
width | numberrequired |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateFieldWidth \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"fieldId": "example",
"width": 0
}'/api/v1/tables/updateRowOrderChange row position. Params: rowId, newOrder (fractional string).
Parameters
rowId | stringrequired | Row ID to reorder |
newOrder | stringrequired | New fractional order string |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/updateRowOrder \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"rowId": "example",
"newOrder": "example"
}'/api/v1/tables/bulkUpdateRowsUpdate multiple rows in one call. Params: tableId, updates (array of {rowId, data}). Each update merges its data with the existing row — fields not included are preserved, identical semantics to updateRow. More efficient than calling updateRow in a loop.
Parameters
tableId | stringrequired | Table ID from listTables |
updates | arrayrequired | Array of row updates |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/bulkUpdateRows \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"updates": {}
}'/api/v1/tables/exportTableExport all table data. Param "tableId". Returns {tableName, fields[], rows[]} with human-readable field names as keys.
Parameters
tableId | stringrequired | Table ID to export |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/exportTable \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example"
}'/api/v1/tables/importRowsBulk import rows. Params: tableId, rows (array of data objects), matchFieldsByName (default true - use field NAMES as keys instead of IDs), fieldDefinitions (optional, to create new fields). When matchFieldsByName=true, use {"Field Name": value} instead of {"fieldId": value}. Send about 5,000 rows a call, fewer for a wide table: a call takes at most 100,000 items in one list and 1,000,000 values in all. A file in several calls: call it with checkOnly=true and firstRowNumber for every part first (nothing is written), then import the parts, so a wrong row anywhere imports nothing, except values that clash between parts or a limit reached by the parts together. Copying tables or rows into another base is work beyond most requests: only when the person asked for the copy.
Parameters
tableId | stringrequired | Table ID from listTables |
rows | arrayrequired | Array of row data. When matchFieldsByName=true, use field NAMES as keys. When false, use field IDs. LINKED-RECORD columns resolve per value: sibling "<Field> (id)" column → row IDs → JSON-array string (comma-safe) → exact label → unique case-insensitive label; unresolvable values are returned in unresolvedLinks (row still imports, link skipped — never guessed, never auto-created). Relations are written both directions. |
matchFieldsByName | boolean | If true, match columns by field name. If false, use field IDs as keys (default: true) |
fieldDefinitions | array | Optional field definitions to create new fields during import |
checkOnly | boolean | Check these rows exactly as an import would, writing nothing (no rows, no columns). To import a file in several calls, check every part first, then import them: a wrong row anywhere then imports nothing, except values that clash between parts or a limit reached by the parts together. |
firstRowNumber | integer | The file's row number of rows[0] when a file goes in several calls (default 1), so an error names the row in the file. |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/importRows \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"rows": {},
"matchFieldsByName": true
}'/api/v1/tables/exportBaseExport one whole base (all its tables, fields, rows and views) as one file in the format tables_importBase takes. Param "databaseId" (from listDatabases). Returns { formatVersion, exportedAt, base, tables: [{ name, fields, rows, views }] }, the same file the Export button saves. Each row carries its id (__rowId) so an import can put linked records back; a secret column comes back blank. The file is at most 256 KB: a bigger base is refused with its size, so read it one table at a time with tables_getRows instead. For one table only use tables_exportTable. Copying tables or rows into another base is work beyond most requests: only when the person asked for the copy.
Parameters
databaseId | stringrequired | The base to export (an id from listDatabases) |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/exportBase \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"databaseId": "example"
}'/api/v1/tables/importBaseMake a new base from a file tables_exportBase gave. Params: data (required: the whole file, exactly as exported), name (optional: the new base's name; default the file's own name). It always makes a new base with all-new ids in this workspace and never changes, overwrites or deletes an existing base, table or row; a base of the same name becomes a second base. It counts against the plan: the number of bases, the rows a table may hold on plans with a row limit, and the database storage; when a limit is reached nothing is made and the answer says which. Returns { databaseId, tablesCreated, linkedRecordsRestored, unresolvedLinks, message }; unresolvedLinks lists link cells that named a row the file does not hold (nothing is linked for them). Copying tables or rows into another base is work beyond most requests: only when the person asked for the copy.
Parameters
name | string | Name for the new base (default: the file's own name) |
data | objectrequired | The whole file tables_exportBase gave, exactly as exported |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/importBase \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "example",
"data": {}
}'/api/v1/tables/filterRowsFilter rows by field values using the SDK operator syntax (postgres-side, scales to large tables — uses a GIN index on data jsonb). Returns { rows, total, tableId, tableName, limit, offset }. Params: - tableId (required) - filter (object): keys are field IDs OR field names (case-insensitive). Multiple keys are AND-ed. Each value is a plain primitive (equality) or an operator dict. - sort: rowOrder, createdAt, updatedAt or any field name/id, optionally prefixed with "-" for descending. Numbers sort numerically, text case-insensitively, blanks last. Default: rowOrder asc. - limit: default 100, max 1000 - offset: default 0 - includeReadable: include human-readable field-name keys in output (default false; turn on only when needed). Operators: $eq, $ne, $gt, $gte, $lt, $lte, $between, $contains, $startsWith, $endsWith, $regex, $in, $nin, $not, $isEmpty, $isNotEmpty. Combine whole filters with { "$or": [...] } and { "$and": [...] }. Compare two fields of a row with $eqField/$neField/$gtField/$gteField/$ltField/$lteField. Examples: - { "Status": "todo" } → equality - { "Priority": { "$gt": 5 } } → numeric comparison - { "Title": { "$contains": "urgent" } } → substring (case-insensitive) - { "Status": { "$in": ["todo", "in-progress"] } } → multi-value - { "DueDate": { "$isNotEmpty": true } } → presence check - { "Status": "done", "Assignee": "Alice" } → AND across keys Validation: unknown field keys or unknown operators throw 400 with the list of valid options — typos surface immediately rather than returning all rows. Caveat: $gt/$gte/$lt/$lte on text fields that store numeric-looking strings ("16", "8") compare LEXICOGRAPHICALLY, so "8" > "10" is true. Use these operators only on fields whose values are stored as proper numbers, or post-filter client-side for text-stored numerics. Same operator dict as the published-app runtime SDK's entities.<Table>.filter().
Parameters
tableId | stringrequired | Table ID to filter |
filter | object | Filter object. Keys are field IDs OR field names (case-insensitive). Multiple keys are AND-ed; { "$or": [ {...}, {...} ] } and { "$and": [...] } group full filter objects and nest arbitrarily. Each value is either a plain primitive (equality), or an operator dict. Operators: $eq, $ne, $gt, $gte, $lt, $lte, $between ([min,max] inclusive), $contains, $startsWith, $endsWith, $regex (Postgres syntax, case-sensitive — prefix (?i) for insensitive), $in, $nin (blanks count as not-in-list), $not (complement of an inner operator dict), $isEmpty, $isNotEmpty. Field-to-field: $eqField/$neField/$gtField/$gteField/$ltField/$lteField compare against ANOTHER field of the same row by name/id — { "Spent": { "$gtField": "Budget" } } finds over-budget rows. Example: { "Status": "todo", "Priority": { "$between": [5, 9] }, "$or": [{ "Tags": { "$contains": "urgent" } }, { "Due": { "$isEmpty": true } }] } Validation: unknown field keys, unknown operators, and operator/value type mismatches throw 400 with the valid options — typos surface immediately instead of returning all (or wrong) rows. Semantics: $contains is case-insensitive and matches LITERALLY (%, _ and \ are escaped); $eq is exact and case-sensitive with NO type coercion ({Score: "50"} does not match the number 50); { field: null } means "is empty"; $in: [] matches NOTHING. Field types: multiselect filters by MEMBERSHIP ($contains value, $in = any-of, plain scalar = has that tag). created_time/updated_time compare as dates ($gte/$lt ranges; ISO strings). Link fields filter by linked ROW ID via the relation table ($contains id, $in [ids], $isEmpty/$isNotEmpty). Caveat: $gt/$gte/$lt/$lte on text fields that store numeric-looking strings ("16", "8") use LEXICOGRAPHIC comparison, so "8" > "10" is true. Use these operators only on fields whose values are stored as proper numbers, or filter then sort/compare client-side for text-stored numerics. |
sort | string | Sort spec — rowOrder, createdAt, updatedAt, or ANY FIELD NAME/ID, optionally prefixed with "-" for descending (e.g. "-Total"). Defaults to rowOrder asc. Numbers sort numerically, text case-insensitively, blanks always last; scalar lookup/rollup and self-contained formulas sort server-side too. Link fields, volatile formulas and list aggregations are not sortable (clear 400). Unknown keys throw 400 — never silently ignored. |
limit | number | Max rows per page (default: 100, max: 1000) |
offset | number | Skip results for pagination |
includeReadable | boolean | Include human-readable field-name keys in output (extra cost — set true only when you need it; default false) |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/filterRows \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"filter": {},
"sort": "example"
}'/api/v1/tables/searchRowsSubstring search across all field values in a table (postgres-side, no row cap). Params: tableId (required), query (required, search string), limit (default 50, max 500), offset, includeReadable (default true). Returns matching rows with optional human-readable field names (dataReadable). For typed field-level filters, use filterRows.
Parameters
tableId | stringrequired | Table ID to search in |
query | stringrequired | Search query string. Matches anywhere in the row's serialized data (field IDs and values). |
limit | number | Max results per page (default: 50, max: 500) |
offset | number | Skip results for pagination |
includeReadable | boolean | Include human-readable field names in output |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/searchRows \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"query": "example",
"limit": 0
}'/api/v1/tables/upsertRowCreate or update row based on match field. Params: tableId (required), matchFieldId (required, field ID to match on), matchValue (required), data (row data keyed by field ID; exact field names are also accepted), includeReadable (default true). Returns {id, action: "created"|"updated", data, dataReadable}.
Parameters
tableId | stringrequired | Table ID |
matchFieldId | stringrequired | Field ID to match on (must be unique or will update first match) |
matchValue | any | Value to match against |
data | objectrequired | Row data with field IDs as keys |
includeReadable | boolean | Include human-readable output |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/upsertRow \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"matchFieldId": "example",
"matchValue": {}
}'/api/v1/tables/getTableSchemaGet table structure/schema. Params: tableId (required), format ("json"|"array"|"markdown", default "json"). Returns field definitions useful for understanding table layout before creating/updating rows.
Parameters
tableId | stringrequired | Table ID |
format | string | Output format: json (full), array (simple), markdown (documentation) |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getTableSchema \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"format": "example"
}'/api/v1/tables/duplicateTableClone a table with structure and optionally data. Params: tableId (source), newName (required), targetDatabaseId (optional, defaults to same database), includeRows (default false), includeViews (default true). Returns new table info. Copying tables or rows into another base is work beyond most requests: only when the person asked for the copy.
Parameters
tableId | stringrequired | Source table ID to duplicate |
newName | stringrequired | Name for the new table |
targetDatabaseId | string | Target database ID (defaults to same database) |
includeRows | boolean | Copy all rows from source table |
includeViews | boolean | Copy views configuration |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/duplicateTable \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"newName": "example",
"targetDatabaseId": "example"
}'/api/v1/tables/getHelpGet focused help for Tables API. Call when a parameter shape is unclear (one call per topic). Topics: - "overview": Structure, quick start, key concepts - "fields": Field types and how to create them - "rows": Creating/updating rows (uses field IDs!) - "views": View types and creation - "linked": Linking tables together Example: getHelp({topic: "rows"}) before creating rows.
Parameters
topic | string | Topic: "overview", "fields", "rows", "views", "linked", "buttons" |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getHelp \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"topic": "example"
}'/api/v1/tables/setAccessRulesSet access rules for a table in published apps. Controls who can create/read/update/delete rows. Params: - tableId: Table ID (required) - accessRules: Object with rules per operation (create, read, update, delete) - trackCreator: boolean (default true) - Track who creates each row Each operation's value is a CONDITION: a single rule, a flat array (OR), or a boolean group. This lets you express ANY combination. Rule types (leaves): - public: Anyone (anonymous users). Always say its level: on a table it has one meaning today, written { "type": "public", "scope": "everywhere" } (on a file or flow the default is "scope": "home" with its sites: see files_setAccessRules / flow_setAccessRules). Two newer levels: "Only through this site" ({ "type": "only_through_site" }: anyone, but only from this site's own pages) and "Anyone on the internet" ({ "type": "anyone_on_internet" }: anyone, anywhere). They can be saved only once Serenities switches them on (until then a save naming one is refused and says so); from then on a public leaf that says no level is refused naming both. When a person asks for "public", ask which one they mean; never pick for them: suggest "Only through this site", and write "Anyone on the internet" only when they ask for exactly that. - authenticated: Any logged-in app user - creator: Only the user who created the row - field_match: { type: "field_match", rowField, userField, op? } - row.data[rowField] OP user.profileData[userField] (op default eq). Compared by the FIELD's type: a number field compares numbers (the profile's "5" is 5; a row holding the text "5" is not), a text field compares text, a multiselect or link field is membership. A profile value that can't be the field's type, ordering (gt/lt…) on a non-number field, or a field the table doesn't have matches no row. - user_property: { type: "user_property", field, value?, op? } - user.profileData[field] OP value - has_role: { type: "has_role", roleId: "<uuid>" } - matches users assigned this AppBuilderRole. Get role IDs from listProjectRoles. Always send roleId: a { role: "name" } is saved with that role's id beside it (so renaming the role keeps the rule), and is refused once name rules are switched off; "user" / "admin" (the built-in classification) stay names. - email: { type: "email", email: "person@example.com" } - one specific user - has_entitlement: { type: "has_entitlement", key } - members holding that paid entitlement - function: { type: "function", functionId: "<id>" } - access decided by a backend function returning true/false (NOT allowed on pages). It must be a switched-on CODE function of this site: one that is off, a data function, or fails to run can't decide: it never lets anyone in, and a rule that needs its answer (all / not / deny) refuses. It may only read, with the visitor's rights: ctx.tables reads see exactly what the table's read and field rules give that visitor, ctx.files.list/getUrl only the files that visitor may read, plus ctx.settings.get; a function rule inside it can't decide (no gate runs inside a gate) — no writes, email, posting, agents, events or AI, and no network but the bridge (no outside sites); do that work in the function or flow the rule protects - relationship: { type: "relationship", linkFieldId: "<id>", match: { via: "creator" } } - ReBAC: access via a RELATED record. Follow the link field linkFieldId on THIS table to the linked table; allow if a related row "belongs to" the user. match is one of: { via:"creator" } (related row's creator is the user), { via:"field_match", rowField, userField, op? } (related.data[rowField] OP user.profileData[userField]), or { via:"user_property", field, value?, op? }. Get link field IDs from getFieldsByTableId (fields where linkType/linkedTableId is set). Tables only — NOT allowed on pages (needs a DB join the edge can't do). Use for "vendor sees only orders for products they own", "member reads their team's projects". - policy: { "policy": "<policyId>" } - reference a reusable AccessPolicy (see listAccessPolicies/createAccessPolicy); expanded at eval. Missing/cyclic ⇒ deny. Operators (op) for user_property/field_match: eq, ne, gt, gte, lt, lte, in, nin, contains, isEmpty, isNotEmpty. Use arrays for in/nin: { type:"user_property", field:"tier", op:"in", value:["pro","enterprise"] }. Groups (combine conditions): - { "any": [condA, condB] } → OR (a flat array is shorthand for this) - { "all": [condA, condB] } → AND (every condition must pass) - { "not": cond } → NOT - { "deny": cond } → DENY-WINS: if cond matches, the WHOLE check fails, overriding every allow (e.g. "everyone paid EXCEPT banned"). Use { all: [ <allows>, { deny: { type:"user_property", field:"banned", value:true } } ] }. Groups nest arbitrarily: { all: [ { any: [tierPro, tierEnt] }, { type:"has_role", roleId } ] }. IMPORTANT: App owner always has full access. No rules / empty = deny (secure by default). A read rule saved empty ([] — "only the owner reads") also gives site functions' ctx.tables 0 rows for members and visitors. With NO read rule, a site function's ctx.tables read for a member or visitor is refused (only the owner reads); set a "creator" read rule so members read the rows they created. Field rules (accessRules.fields[<fieldId>] = { read?, write? }): same conditions. A rule that compares a record (creator, field_match, relationship) is judged on EACH row — "only the creator sees Notes" hides Notes on others' rows; a write sets the field only where the rule passes on the row the write leaves (link/unlink of a link field too). has_entitlement, function and policy work there as well. Example - Contact form (anyone submits, only owner reads): { "tableId": "xxx", "accessRules": { "create": [{"type":"public","scope":"everywhere"}], "read": [], "update": [], "delete": [] } } Example - Personal data (users see only their own): { "tableId": "xxx", "accessRules": { "create": [{"type":"authenticated"}], "read": [{"type":"creator"}], "update": [{"type":"creator"}], "delete": [{"type":"creator"}] } } Example - "(Pro OR Enterprise) AND age >= 18" can read: { "tableId": "xxx", "accessRules": { "read": { "all": [ { "any": [ {"type":"user_property","field":"tier","value":"pro"}, {"type":"user_property","field":"tier","value":"enterprise"} ] }, {"type":"user_property","field":"age","op":"gte","value":18} ] } } } Example - Vendor sees only Orders for Products they own (ReBAC, "product" = a link field on Orders → Products): { "tableId": "<ordersTableId>", "accessRules": { "read": {"type":"relationship","linkFieldId":"<productLinkFieldId>","match":{"via":"creator"}} } } Example - Paying members EXCEPT banned users (deny-wins): { "tableId": "xxx", "accessRules": { "read": { "all": [ {"type":"user_property","field":"tier","value":"pro"}, { "deny": {"type":"user_property","field":"banned","value":true} } ] } } }
Parameters
tableId | stringrequired | Table ID from listTables |
accessRules | objectrequired | Access rules per operation (create/read/update/delete). Each is a Condition: a single rule, a legacy flat array (OR), or an { all | any | not } group. Rule types: public, authenticated, creator, field_match, user_property (with op: eq/ne/gt/gte/lt/lte/in/nin/contains/isEmpty/isNotEmpty), has_role, email, function. AND/OR/NOT groups + operators enable any combination. |
trackCreator | boolean | Whether to track who creates each row. OMIT to keep the table's current setting — it used to default to true, so a call that only changed a read rule silently re-enabled creator tracking. |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/setAccessRules \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example",
"accessRules": {},
"trackCreator": true
}'/api/v1/tables/getAccessRulesGet current access rules for a table. Returns rules, trackCreator setting, and help with presets. Params: tableId (required) Returns: - tableId, tableName - accessRules: Current rules per operation - trackCreator: Whether row creators are tracked - help: Rule type explanations and preset examples
Parameters
tableId | stringrequired | Table ID from listTables |
Example Request
curl -X POST https://app.serenitiesai.com/api/v1/tables/getAccessRules \
-H "Authorization: Bearer mk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"tableId": "example"
}'