Database
Read and write your app's data, in development or in production.
Every project has its own SQL database (SQLite, on Cloudflare D1 — or Turso after a move) in two environments: dev, used by the agent and the development preview, and live, used by the published site. Every request names the environment in env. The app reaches the same database through Drizzle ORM in its own code (Totalum SDK — database).
The schema belongs to the app's code (src/db/schema.ts): it changes through migrations the agent writes, which apply to dev at once and to live when you publish. These endpoints never change the schema. They are free to call; the rows they read and write count as the project's database usage.
Backups, restore points, point-in-time restore and the move to Turso are on Database Backups & Restore.
#Get Schema
/api/v1/vcaas/projects/:projectId/database/schemaapi-keyFreeThe tables, columns, foreign keys and relations of one environment's database, read from the database itself, plus the display settings shown in the Totalum database explorer.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | Yes | "live" or "dev" |
Response fields
| Field | Type | Description |
|---|---|---|
data.env | string | The environment read |
data.tables[].name | string | Table name |
data.tables[].label | string | Display label |
data.tables[].kind | string | "table", "junction" (a many-to-many link table) or "system" (auth and migrations: read-only here) |
data.tables[].primaryKey | string[] | The primary key's columns in key order; [] when the table has none (its rows are addressed by rowid) |
data.tables[].columns[].name | string | Column name |
data.tables[].columns[].sqlType | string | SQLite type, e.g. TEXT, INTEGER |
data.tables[].columns[].kind | string | Value kind, e.g. text, number, datetime, options, relation |
data.tables[].columns[].label | string | Display label |
data.tables[].columns[].notNull | boolean | NOT NULL |
data.tables[].columns[].unique | boolean | Unique on its own: a one-column primary key or a one-column UNIQUE index. A column of a composite primary key is not unique on its own |
data.tables[].columns[].default | string | null | Default value expression |
data.tables[].columns[].options | string[] | undefined | Allowed values (a CHECK … IN (…) list) |
data.tables[].columns[].relation | object | undefined | { table, fkColumn, onDelete } the column references |
data.tables[].indexes[] | object | { name, columns, unique } |
data.tables[].foreignKeys[] | object | { column, table, toColumn, onDelete } |
data.tables[].rowCountEstimate | number | Rows in the table (0 unless counted) |
data.ledger | object | { count, lastCreatedAt, lastHash } of the applied migrations |
data.ddlHash | string | Hash of the whole schema: changes whenever a migration changes it |
Example request
curl -H "api-key: tlm_sk_your_key" \
"https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/schema?env=live"const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/schema?env=live", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"env": "live",
"tables": [
{
"name": "bookings",
"label": "Bookings",
"kind": "table",
"primaryKey": ["id"],
"columns": [
{ "name": "id", "sqlType": "TEXT", "kind": "text", "label": "Id", "notNull": true, "unique": true, "default": null },
{ "name": "status", "sqlType": "TEXT", "kind": "options", "label": "Status", "notNull": true, "unique": false, "default": "'pending'", "options": ["pending", "paid", "cancelled"] },
{ "name": "user_id", "sqlType": "TEXT", "kind": "relation", "label": "User", "notNull": true, "unique": false, "default": null, "relation": { "table": "users", "fkColumn": "id", "onDelete": "cascade" } }
],
"indexes": [{ "name": "bookings_user_id", "columns": ["user_id"], "unique": false }],
"foreignKeys": [{ "column": "user_id", "table": "users", "toColumn": "id", "onDelete": "cascade" }],
"rowCountEstimate": 0
}
],
"ledger": { "count": 4, "lastCreatedAt": "2026-10-08T11:15:00.000Z", "lastHash": "a41f9c" },
"ddlHash": "9c0e2b"
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
DEV_DATABASE_NOT_FOUND | 404 | env is "dev" and the project has no development database |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
#Query Rows
/api/v1/vcaas/projects/:projectId/database/rowsapi-keyFreeRows of one table, filtered, searched, sorted and paginated. Filters compile to parameterised SQL on the server; values are never interpolated.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | Yes | "live" or "dev" |
table | string | Yes | Table name |
filter | object | No | { and: [...] } or { or: [...] } of conditions { column, op, value } and nested groups. op: eq, ne, lt, lte, gt, gte, contains, startsWith, in, isNull, notNull. At most 4 levels, 50 conditions and 1,000 in values |
search | string | No | Text search, at most 200 characters |
sort | array | No | Up to 3 { column, dir: "asc" | "desc" } |
page | object | Yes | { limit, after? , offset? } — limit 1–200; after is the previous page's nextAfter (keyset, the default way to page); offset (≤ 10,000) is the alternative. Send after or offset, not both |
expand | string[] | No | Up to 5 foreign-key columns of table; each row gets the referenced row as <column>__expanded (null when missing) |
count | boolean | No | Also return total, the number of matching rows |
Response fields
| Field | Type | Description |
|---|---|---|
data.columns | array | { name, sqlType?, kind? } of each column |
data.rows | object[] | The rows, one object per row |
data.nextAfter | string | undefined | Cursor of the next page; absent on the last page |
data.total | number | undefined | Matching rows, when count was true |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"live","table":"orders","filter":{"and":[{"column":"status","op":"eq","value":"paid"},{"column":"total","op":"gte","value":100}]},"sort":[{"column":"created_at","dir":"desc"}],"page":{"limit":20}}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/my-app/database/rowsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/my-app/database/rows", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "live",
"table": "orders",
"filter": {
"and": [
{
"column": "status",
"op": "eq",
"value": "paid"
},
{
"column": "total",
"op": "gte",
"value": 100
}
]
},
"sort": [
{
"column": "created_at",
"dir": "desc"
}
],
"page": {
"limit": 20
}
}),
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"columns": [
{ "name": "id", "sqlType": "TEXT" },
{ "name": "status", "sqlType": "TEXT" },
{ "name": "total", "sqlType": "REAL" },
{ "name": "created_at", "sqlType": "TEXT" }
],
"rows": [
{ "id": "01J8ZK3W6Q2Y4N7B9C1D3F5G7H", "status": "paid", "total": 120, "created_at": "2026-09-20T10:04:11.000Z" }
],
"nextAfter": "WyIyMDI2LTA5LTIwVDEwOjA0OjExLjAwMFoiXQ"
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
TABLE_NOT_FOUND | 404 | No such table; errorDetails.suggestions lists close names |
COLUMN_NOT_FOUND | 400 | A filter, sort or expand column does not exist (or is not a foreign key, for expand) |
FILTER_TOO_COMPLEX | 400 | Filter deeper than 4 levels, more than 50 conditions or more than 1,000 in values |
VALIDATION_ERROR | 400 | Invalid body, or page.after is not a cursor of this table and sort |
DEV_DATABASE_NOT_FOUND | 404 | env is "dev" and the project has no development database |
#Insert Record
/api/v1/vcaas/projects/:projectId/database/recordsapi-keyFreeInsert one row and return it as stored.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | Yes | "live" or "dev" |
table | string | Yes | Table name |
data | object | Yes | Column → value. Columns with a default may be omitted |
Response fields
| Field | Type | Description |
|---|---|---|
data.row | object | The inserted row, every column |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"live","table":"bookings","data":{"id":"bk_1001","status":"paid","user_id":"usr_42"}}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/recordsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/records", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "live",
"table": "bookings",
"data": {
"id": "bk_1001",
"status": "paid",
"user_id": "usr_42"
}
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "row": { "id": "bk_1001", "status": "paid", "user_id": "usr_42", "created_at": "2026-10-08T12:00:00.000Z" } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
TABLE_NOT_FOUND | 404 | No such table |
COLUMN_NOT_FOUND | 400 | data names a column the table does not have |
COLUMN_REQUIRED | 400 | A NOT NULL column without a default is missing |
VALUE_NOT_ALLOWED | 400 | The value is not one of the column's allowed values |
SYSTEM_TABLE_READ_ONLY | 403 | The table is written by Totalum only (e.g. _migrations) |
#Record ids
The :recordId of the single-record routes and the id of a bulk item address exactly one row by the table's key (see primaryKey and unique in Get Schema):
- the value of
idwhen that column is unique, else of the table's one unique column — e.g.42or01J8ZK3W6Q2Y4N7B9C1D3F5G7H; - for a composite primary key, an object naming every key column and nothing else — e.g.
{"post_id":"p1","user_id":"u1"}. In the URL it is that JSON, URL-encoded:…/database/records/%7B%22post_id%22%3A%22p1%22%2C%22user_id%22%3A%22u1%22%7D; in a bulk item it may be the object itself; - for a table with no key, the row's
rowid(returned by Query Rows as therowidcolumn).
An id that does not name every key column (e.g. p1 alone on a (post_id, user_id) key) is refused with 400 RECORD_KEY_REQUIRED — it is never matched against part of the key. If an id would match more than one row (a key column whose collation lets two values compare equal), the write is refused with 400 AMBIGUOUS_RECORD_ID and nothing is changed.
#Update Record
/api/v1/vcaas/projects/:projectId/database/records/:recordIdapi-keyFreeUpdate the given columns of one row, found by its record id, and return the full row.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
recordId | string | The row's record id: its key value, or for a composite primary key the URL-encoded JSON object of every key column |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | Yes | "live" or "dev" |
table | string | Yes | Table name |
data | object | Yes | Column → new value; at least one column |
Response fields
| Field | Type | Description |
|---|---|---|
data.row | object | The updated row, every column |
Example request
curl -X PATCH \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"live","table":"bookings","data":{"status":"cancelled"}}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/records/bk_1001const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/records/bk_1001", {
method: "PATCH",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "live",
"table": "bookings",
"data": {
"status": "cancelled"
}
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "row": { "id": "bk_1001", "status": "cancelled", "user_id": "usr_42", "created_at": "2026-10-08T12:00:00.000Z" } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
RECORD_NOT_FOUND | 404 | No row has that id |
RECORD_KEY_REQUIRED | 400 | The id does not name every column of the table's key; errorDetails.keyColumns lists them |
AMBIGUOUS_RECORD_ID | 400 | The id matches more than one row; nothing was changed |
TABLE_NOT_FOUND | 404 | No such table |
COLUMN_NOT_FOUND | 400 | data names a column the table does not have |
VALUE_NOT_ALLOWED | 400 | The value is not one of the column's allowed values |
SYSTEM_TABLE_READ_ONLY | 403 | The table is written by Totalum only |
#Delete Record
/api/v1/vcaas/projects/:projectId/database/records/:recordIdapi-keyFreeDelete one row by its record id. There is no trash: a deleted row comes back only from a backup.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
recordId | string | The row's record id: its key value, or for a composite primary key the URL-encoded JSON object of every key column |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | Yes | "live" or "dev" |
table | string | Yes | Table name |
Response fields
| Field | Type | Description |
|---|---|---|
data.deleted | boolean | Always true |
data.id | string | number | object | The deleted row's id, as sent |
Example request
curl -X DELETE \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"live","table":"bookings"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/records/bk_1001const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/records/bk_1001", {
method: "DELETE",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "live",
"table": "bookings"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "deleted": true, "id": "bk_1001" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
RECORD_NOT_FOUND | 404 | No row has that id |
RECORD_KEY_REQUIRED | 400 | The id does not name every column of the table's key; errorDetails.keyColumns lists them |
AMBIGUOUS_RECORD_ID | 400 | The id matches more than one row; nothing was changed |
TABLE_NOT_FOUND | 404 | No such table |
SYSTEM_TABLE_READ_ONLY | 403 | The table is written by Totalum only |
#Bulk Write
/api/v1/vcaas/projects/:projectId/database/records/bulkapi-keyFreeInsert, update or delete up to 500 rows of one table in one atomic batch: if one item fails, none is applied.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | Yes | "live" or "dev" |
table | string | Yes | Table name |
op | string | Yes | "insert", "update" or "delete" |
items | array | Yes | 1–500 items: row objects for insert, { id, data } for update, { id } for delete — each id a record id |
Response fields
| Field | Type | Description |
|---|---|---|
data.op | string | The operation |
data.count | number | Rows inserted, updated or deleted |
data.rows | object[] | undefined | The inserted or updated rows (insert, update) |
data.ids | array | undefined | The ids actually deleted (delete) |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"dev","table":"services","op":"insert","items":[{"id":"cut","name":"Haircut","price":20},{"id":"beard","name":"Beard trim","price":12}]}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/records/bulkconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/records/bulk", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "dev",
"table": "services",
"op": "insert",
"items": [
{
"id": "cut",
"name": "Haircut",
"price": 20
},
{
"id": "beard",
"name": "Beard trim",
"price": 12
}
]
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "op": "insert", "count": 2, "rows": [{ "id": "cut", "name": "Haircut", "price": 20 }, { "id": "beard", "name": "Beard trim", "price": 12 }] } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
BULK_TOO_LARGE | 400 | More than 500 items |
RECORD_KEY_REQUIRED | 400 | An item's id does not name every column of the table's key; errorDetails.keyColumns lists them |
AMBIGUOUS_RECORD_ID | 400 | An item's id matches more than one row; nothing was changed |
SQL_ERROR | 400 | An item failed; nothing was applied |
TABLE_NOT_FOUND | 404 | No such table |
COLUMN_NOT_FOUND | 400 | An item names a column the table does not have |
SYSTEM_TABLE_READ_ONLY | 403 | The table is written by Totalum only |
#Link or Unlink Records
/api/v1/vcaas/projects/:projectId/database/relationsapi-keyFreeAdd or remove the junction-table row that links two rows in a many-to-many relation.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | Yes | "live" or "dev" |
a | object | Yes | { table, id } — one side |
b | object | Yes | { table, id } — the other side |
op | string | Yes | "link" or "unlink" |
junctionTable | string | No | The junction table to use; required when more than one connects the two tables |
Response fields
| Field | Type | Description |
|---|---|---|
data.op | string | The operation |
data.junctionTable | string | The junction table used |
data.changed | number | 1 when a row was added or removed, 0 when it already was (or was not) linked |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"live","op":"link","a":{"table":"bookings","id":"bk_1001"},"b":{"table":"services","id":"cut"}}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/relationsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/relations", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "live",
"op": "link",
"a": {
"table": "bookings",
"id": "bk_1001"
},
"b": {
"table": "services",
"id": "cut"
}
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "op": "link", "junctionTable": "booking_services", "changed": 1 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
AMBIGUOUS_RELATION | 400 | Zero or several junction tables connect the two tables; errorDetails.candidates lists them — send one as junctionTable |
NOT_A_JUNCTION_TABLE | 400 | junctionTable does not reference both tables |
RECORD_NOT_FOUND | 404 | a or b does not exist |
TABLE_NOT_FOUND | 404 | No such table |
#Run SQL
/api/v1/vcaas/projects/:projectId/database/sqlapi-keyFreeRun one SQL statement with bound parameters. Reads return at most 1,000 rows. Writes run only when asked for. Schema changes (DDL) are refused: they are migrations in the project's code.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | Yes | "live" or "dev" |
sql | string | Yes | One statement, at most 100 KB, with ? placeholders |
params | array | No | Up to 100 values bound to the placeholders |
allowWrites | boolean | No | Required for INSERT, UPDATE and DELETE |
confirmLive | boolean | No | Required for a write on live (the published site's data) |
Response fields
| Field | Type | Description |
|---|---|---|
data.columns | array | { name } of each result column |
data.rows | object[] | Result rows (at most 1,000) |
data.truncated | boolean | true when the result had more than 1,000 rows |
data.meta | object | The database's statement metadata (rows_read, rows_written, changes, duration, …) |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"live","sql":"SELECT status, SUM(total) AS total FROM orders GROUP BY status"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/my-app/database/sqlconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/my-app/database/sql", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "live",
"sql": "SELECT status, SUM(total) AS total FROM orders GROUP BY status"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "columns": [{ "name": "status" }, { "name": "total" }], "rows": [{ "status": "paid", "total": 4810 }, { "status": "refunded", "total": 120 }], "truncated": false, "meta": { "rows_read": 312, "rows_written": 0, "changes": 0, "duration": 0.4 } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
DDL_NOT_ALLOWED | 400 | The statement changes the schema — change src/db/schema.ts, then npm run db:generate and npm run db:migrate |
SQL_READ_ONLY | 403 | The statement writes and allowWrites is not true |
LIVE_WRITE_CONFIRMATION_REQUIRED | 409 | A write on live without confirmLive: true |
SYSTEM_TABLE_READ_ONLY | 403 | The statement writes _migrations |
SQL_MULTI_STATEMENT | 400 | More than one statement |
SQL_ERROR | 400 | The database rejected the statement |
#Ask Database
/api/v1/vcaas/projects/:projectId/database/askapi-keyMetered (one AI call)Turn a plain-language question into one SQL statement for this database. The statement is shown, never run: run it with Run SQL.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
question | string | Yes | What you want from the data |
questionBase64 | string | No | The question as base64 (UTF-8), instead of question |
env | string | No | "live" or "dev". Default: live once the project has been published, else dev |
Response fields
| Field | Type | Description |
|---|---|---|
data.sql | string | The statement |
data.explanation | string | What it does |
data.isReadOnly | boolean | true when the statement only reads |
data.creditsCharged | number | What the question cost |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"question":"How many bookings were paid last week?","env":"live"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/askconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/ask", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"question": "How many bookings were paid last week?",
"env": "live"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "sql": "SELECT COUNT(*) AS paid FROM bookings WHERE status = 'paid' AND created_at >= datetime('now', '-7 days')", "explanation": "Counts the bookings paid in the last 7 days.", "isReadOnly": true, "creditsCharged": 0.02 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
NL_QUERY_UNANSWERABLE | 422 | The database's tables cannot answer the question (errorMessage says why) |
DEV_DATABASE_NOT_FOUND | 404 | env is "dev" and the project has no development database |
#Get Database Usage
/api/v1/vcaas/projects/:projectId/database/usageapi-keyFreeThe database's usage and health: rows read and written and storage, in credits (today, 7 and 30 days), size, tables and row counts, the slowest queries, tips (missing indexes, large tables, backups) and whether moving to Turso is worth it.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | No | "all" (default), "live" or "dev" |
Response fields
| Field | Type | Description |
|---|---|---|
data.generatedAt | string | When this was computed |
data.envs[].env | string | "live" or "dev" |
data.envs[].provider | string | "d1" or "turso" |
data.envs[].region | string | null | Where the primary copy runs |
data.envs[].readReplicas | boolean | Whether reads are served from copies near visitors |
data.envs[].sizeBytes | number | null | Size of the database |
data.envs[].lastBackupAt | string | null | Last manual backup |
data.envs[].usage | object | { today, last7Days, last30Days }, each { rowsRead, rowsWritten, credits: { rowsRead, rowsWritten, storage, total } } |
data.envs[].daily | object[] | One row per day: { date, rowsRead, rowsWritten, requests, credits, sizeBytes } |
data.envs[].tables | object | null | { count, rows, list: [{ name, rows, estimated? }] } |
data.envs[].queries | object[] | null | The heaviest query shapes of the last 7 days: { sql, count, totalMs, rowsRead, rowsReturned, rowsWritten } |
data.turso | object | { verdict, reasons[], caveats[], location, envs, estimateSec, databaseBytes, credits, paidPlanRequired }; verdict: "recommended", "consider", "not_needed" or "on_turso" |
data.tips | object[] | { env, severity: "warn" | "info", code, message, params? } |
data.notes | object | { liveTrafficSettledNightly, queriesSince } |
Example request
curl -H "api-key: tlm_sk_your_key" \
"https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/usage?env=live"const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/usage?env=live", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"generatedAt": "2026-10-08T12:00:00.000Z",
"envs": [
{
"env": "live",
"provider": "d1",
"region": "WEUR",
"readReplicas": true,
"sizeBytes": 2457600,
"lastBackupAt": null,
"usage": {
"today": { "rowsRead": 4120, "rowsWritten": 36, "credits": { "rowsRead": 0.0002, "rowsWritten": 0.0021, "storage": 0.0001, "total": 0.0024 } },
"last7Days": { "rowsRead": 30211, "rowsWritten": 240, "credits": { "rowsRead": 0.0017, "rowsWritten": 0.0137, "storage": 0.0007, "total": 0.0161 } },
"last30Days": { "rowsRead": 30211, "rowsWritten": 240, "credits": { "rowsRead": 0.0017, "rowsWritten": 0.0137, "storage": 0.0007, "total": 0.0161 } }
},
"tables": { "count": 7, "rows": 412, "list": [{ "name": "bookings", "rows": 240 }] },
"queries": []
}
],
"turso": { "verdict": "not_needed", "reasons": [{ "code": "fits_d1", "message": "The database is small (2.5 MB of 10 GB) and its traffic fits D1 well. There is no need to move." }], "caveats": [] },
"tips": [],
"notes": { "liveTrafficSettledNightly": true, "queriesSince": "2026-10-01T12:00:00.000Z" }
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | env is not all, live or dev |
#Undo Agent Run Database Change
/api/v1/vcaas/projects/:projectId/database/undo-runapi-keyFreeUndo the latest agent run's database change: the database goes back to the restore point taken at the start of that run (a backup is taken first), then the code goes back to before the run. Only the latest run can be undone. Follow it on Get Project State.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
runId | string | Yes | The run to undo |
confirm | string | No | The project id, typed to confirm |
Response fields
| Field | Type | Description |
|---|---|---|
data.status | string | "restoring" |
data.env | string | The database restored |
data.runId | string | The run undone |
data.restoredTo | string | The restore point used |
data.previousBookmark | string | The point taken just before (restoring to it undoes the undo) |
data.preRestoreBackupId | string | The backup taken first |
data.codeVersionId | string | null | The version the code goes back to; null when the run changed no code |
data.operation | object | { kind, id } |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"runId":"run_01k6xc4n7d","confirm":"barber-bookings"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/undo-runconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/undo-run", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"runId": "run_01k6xc4n7d",
"confirm": "barber-bookings"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "env": "dev", "status": "restoring", "restoredTo": "00000085-00000001-…", "previousBookmark": "00000091-00000002-…", "preRestoreBackupId": "bkp_01k6xg", "operation": { "kind": "backupRestore", "id": "op_01k6xh" }, "runId": "run_01k6xc4n7d", "codeVersionId": "ver_01k6xh9q" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
AGENT_RUNNING | 409 | A prompt is running |
OPERATION_IN_PROGRESS | 409 | Another restore or long operation is running |
