Database
Read the schema, query with rich filters, and mutate records and relations in your project database.
The database API exposes your project's tables and records. You can read the table structure, query with filtering/sorting/pagination/aggregation and nested relations up to 6 levels deep, create, edit and delete records, and link or unlink records across a many-to-many relation. All endpoints require the api-key header and are free — reading and writing a project's own data is not metered; only agent work and infrastructure are.
#Get Database Tables Structure
/api/v1/vcaas/projects/:projectId/database/tables-structureFreeRetrieve all database table definitions including field names, types, and configuration.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Response fields
| Field | Type | Description |
|---|---|---|
data.tables | array | Array of table definitions |
data.tables[]._id | string | Table ID |
data.tables[].type | string | Table name (snake_case) |
data.tables[].label | string | Human-friendly display name |
data.tables[].description | string | Table description |
data.tables[].icon | string | FontAwesome icon class |
data.tables[].properties | object | Map of property name to property definition |
data.tables[].properties.{name}.id | string | Property ID |
data.tables[].properties.{name}.name | string | Property field name |
data.tables[].properties.{name}.propertyType | string | "string" | "number" | "date" | "options" | "file" | "long-string" | "objectReference" |
data.tables[].properties.{name}.label | string | Human-friendly field label |
data.tables[].properties.{name}.description | string | Field description |
data.tables[].properties.{name}.objectReference | object | Relationship config (if objectReference type) |
data.tables[].properties.{name}.typeExtras | object | Type-specific config (options values, date settings, etc.) |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/tables-structure{
"errors": null,
"data": {
"tables": [
{
"_id": "65f1a2b3c4d5e6f7",
"type": "customers",
"label": "Customers",
"description": "Customer records",
"icon": "fa-solid fa-users",
"properties": {
"name": {
"id": "prop_abc123",
"name": "name",
"propertyType": "string",
"label": "Full Name"
},
"email": {
"id": "prop_def456",
"name": "email",
"propertyType": "string",
"label": "Email",
"typeExtras": { "string": { "type": "link" } }
}
}
}
]
}
}{
"errors": {
"errorCode": "PROJECT_NOT_FOUND",
"errorMessage": "Project does not exist or you don't own it"
},
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
#Query Database
/api/v1/vcaas/projects/:projectId/database/queryFreeQuery records from any table with advanced filtering, sorting, pagination, aggregations, and nested related data up to 6 levels deep.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
tableName | string | Yes | The table name to query (use type from tables-structure response) |
queryOptions | object | No | Advanced query options |
queryOptions._filter | object | No | Field filters (see filter operators below) |
queryOptions._sort | object | No | Sort by fields: { fieldName: "asc" | "desc" } |
queryOptions._limit | number | No | Max records to return (default 50, max 1000) |
queryOptions._offset | number | No | Records to skip (for pagination) |
queryOptions._select | object | No | Include only specified fields: { fieldName: true }. Cannot use with _omit |
queryOptions._omit | object | No | Exclude specified fields: { fieldName: true }. Cannot use with _select |
queryOptions._count | boolean | No | Adds _count._total with total matching records (before pagination) |
queryOptions._aggregate | object | No | Aggregations: { _sum: { field: true }, _avg, _min, _max, _count } |
queryOptions._groupBy | string | string[] | No | Group results by field(s). Requires _aggregate |
queryOptions.[property] | true | object | No | Expand related data. true = all fields, object = nested queryOptions |
Filter operators (use inside _filter)
- Exact match:
{ "status": "active" } - With operator:
{ "fieldName": { "operator": value } }
| Operator | Description | Example |
|---|---|---|
gte | Greater than or equal | { "age": { "gte": 18 } } |
lte | Less than or equal | { "age": { "lte": 65 } } |
gt | Greater than | { "price": { "gt": 0 } } |
lt | Less than | { "price": { "lt": 100 } } |
ne | Not equal | { "status": { "ne": "deleted" } } |
in | Matches any value in array | { "status": { "in": ["active", "pending"] } } |
nin | Matches none in array | { "role": { "nin": ["admin", "super"] } } |
regex | Regex pattern (+options for flags) | { "email": { "regex": "@gmail", "options": "i" } } |
contains | Case-insensitive contains | { "name": { "contains": "john" } } |
startsWith | Case-insensitive starts with | { "name": { "startsWith": "J" } } |
endsWith | Case-insensitive ends with | { "email": { "endsWith": ".com" } } |
_or | OR logic (array of conditions) | { "_or": [{ "status": "active" }, { "role": "admin" }] } |
Nested queries(related data)
Expand related tables by using the property name as a key in queryOptions.
- Shorthand:
{ "orders": true }— expands all related orders - Full:
{ "orders": { "_filter": {...}, "_sort": {...}, "_limit": 5 } }— with options (max 300 children) { "orders": { "_has": true } }— only parents with at least one matching child{ "orders": { "_has": "none" } }— only parents with zero matching children{ "orders": { "_count": true } }— include child count per parent- Nesting up to 6 levels deep supported
Response fields
| Field | Type | Description |
|---|---|---|
data.results | array | Array of matching records |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tableName":"customers","queryOptions":{"_filter":{"status":"active","age":{"gte":18}},"_sort":{"createdAt":"desc"},"_limit":10,"orders":{"_filter":{"total":{"gt":50}},"_limit":5}}}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/query{
"errors": null,
"data": {
"results": [
{
"_id": "65f1a2b3c4d5e6f7a8b9c0d1",
"name": "John Doe",
"email": "john@example.com",
"status": "active",
"age": 30,
"orders": [
{ "_id": "...", "total": 120.50, "createdAt": "2026-03-09T..." }
],
"createdAt": "2026-03-10T08:00:00.000Z"
}
]
}
}{
"errors": {
"errorCode": "MISSING_TABLE_NAME",
"errorMessage": "tableName is required"
},
"data": null
}More filter examples
// OR logic
"_filter": { "_or": [{ "status": "active" }, { "role": "admin" }] }
// IN operator
"_filter": { "status": { "in": ["active", "pending"] } }
// Regex case-insensitive
"_filter": { "email": { "regex": "@gmail", "options": "i" } }
// Aggregation with groupBy
"_aggregate": { "_sum": { "total": true }, "_count": true }, "_groupBy": "status"
// Nested: only parents with children matching filter
"orders": { "_has": true, "_filter": { "total": { "gt": 100 } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
MISSING_TABLE_NAME | 400 | tableName is required |
#Create Database Record
/api/v1/vcaas/projects/:projectId/database/recordsFreeCreate a new record in any table. The _id field is auto-generated by the database — do not include it in the request body. The response returns the full created record including the generated _id and any default fields.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
tableName | string | Yes | The table name to insert into (use type from tables-structure response) |
data | object | Yes | The record properties as key-value pairs. Do NOT include _id (auto-generated) |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The full created record including the auto-generated _id and all fields |
data._id | string | The auto-generated unique identifier for the new record |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tableName":"customers","data":{"name":"John Doe","email":"john@example.com","status":"active","age":30}}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/records{
"errors": null,
"data": {
"_id": "65f1a2b3c4d5e6f7a8b9c0d1",
"name": "John Doe",
"email": "john@example.com",
"status": "active",
"age": 30,
"createdAt": "2026-03-25T10:00:00.000Z",
"updatedAt": "2026-03-25T10:00:00.000Z"
}
}{
"errors": {
"errorCode": "MISSING_DATA",
"errorMessage": "data is required and must be an object"
},
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
MISSING_TABLE_NAME | 400 | tableName is required |
MISSING_DATA | 400 | data is required and must be an object |
TABLE_NOT_FOUND | 400 | Table doesn't exist in the project |
#Edit Database Record
/api/v1/vcaas/projects/:projectId/database/records/:recordIdFreeUpdate specific fields of an existing record by its ID. Only the fields included in data will be modified — all other fields remain unchanged. The response returns the full updated record.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
recordId | string | The _id of the record to update |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
tableName | string | Yes | The table name (use type from tables-structure response) |
data | object | Yes | The properties to update as key-value pairs. Only included fields are modified |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The full updated record with all fields (including unchanged ones) |
data._id | string | The record ID |
Example request
curl -X PATCH \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tableName":"customers","data":{"email":"newemail@example.com","age":31}}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/records/65f1a2b3c4d5e6f7a8b9c0d1{
"errors": null,
"data": {
"_id": "65f1a2b3c4d5e6f7a8b9c0d1",
"name": "John Doe",
"email": "newemail@example.com",
"status": "active",
"age": 31,
"createdAt": "2026-03-25T10:00:00.000Z",
"updatedAt": "2026-03-25T10:05:00.000Z"
}
}{
"errors": {
"errorCode": "PROJECT_NOT_FOUND",
"errorMessage": "Project does not exist or you don't own it"
},
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
MISSING_RECORD_ID | 400 | recordId is required |
MISSING_TABLE_NAME | 400 | tableName is required |
MISSING_DATA | 400 | data is required and must be an object |
TABLE_NOT_FOUND | 400 | Table doesn't exist in the project |
#Delete Database Record
/api/v1/vcaas/projects/:projectId/database/records/:recordIdFreePermanently delete one record by its ID.
A DELETE with a request body is awkward for several HTTP clients, so this endpoint takes the table as a query parameter: ?tableName=customers.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
recordId | string | The _id of the record to delete |
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
tableName | string | Yes | The table name (use type from the tables-structure response) |
Response fields
| Field | Type | Description |
|---|---|---|
data.deleted | boolean | true on successful deletion |
data.recordId | string | The ID of the record that was deleted |
Example request
curl -X DELETE -H "api-key: tlm_sk_your_key" \
"https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/records/65f1a2b3c4d5e6f7a8b9c0d1?tableName=customers"{
"errors": null,
"data": {
"deleted": true,
"recordId": "65f1a2b3c4d5e6f7a8b9c0d1"
}
}{
"errors": {
"errorCode": "MISSING_TABLE_NAME",
"errorMessage": "tableName is required (query parameter)"
},
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
MISSING_RECORD_ID | 400 | recordId is required |
MISSING_TABLE_NAME | 400 | tableName is required as a query parameter |
TABLE_NOT_FOUND | 400 | Table doesn't exist in the project |
#Link Records
/api/v1/vcaas/projects/:projectId/database/records/:recordId/linkFreeLink two records across a many-to-many relation.
Totalum owns the junction table for a manyToMany relation, so links are made by asking it to add a reference — never by creating a junction row yourself.
A manyToOne or oneToMany link is an ordinary field on one of the two records: set it with Edit Database Record instead. Routing one of those through this endpoint would silently do nothing.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
recordId | string | The _id of the record on this side of the relation |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
tableName | string | Yes | The table recordId belongs to (use type from the tables-structure response) |
propertyId | string | Yes | The objectReference property on tableName that holds the relation |
referenceId | string | Yes | The _id of the record on the other side of the relation |
Response fields
| Field | Type | Description |
|---|---|---|
data.linked | boolean | true when the link exists after the call |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tableName":"customers","propertyId":"tags","referenceId":"65f1a2b3c4d5e6f7a8b9c0d2"}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/records/65f1a2b3c4d5e6f7a8b9c0d1/link{ "errors": null, "data": { "linked": true } }{
"errors": {
"errorCode": "MISSING_PROPERTY_ID",
"errorMessage": "propertyId is required (the tableLink field holding the relation)"
},
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
MISSING_RECORD_ID | 400 | recordId is required |
MISSING_TABLE_NAME | 400 | tableName is required |
MISSING_PROPERTY_ID | 400 | propertyId is required — the relation field on the table |
MISSING_REFERENCE_ID | 400 | referenceId is required — the _id of the record to link |
TABLE_NOT_FOUND | 400 | Table doesn't exist in the project |
#Unlink Records
/api/v1/vcaas/projects/:projectId/database/records/:recordId/linkFreeRemove a many-to-many link. The two records are untouched — only the link between them goes.
Removing a link that is not there succeeds. You do not need to check first.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
recordId | string | The _id of the record on this side of the relation |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
tableName | string | Yes | The table recordId belongs to (use type from the tables-structure response) |
propertyId | string | Yes | The objectReference property on tableName that holds the relation |
referenceId | string | Yes | The _id of the record on the other side of the relation |
Response fields
| Field | Type | Description |
|---|---|---|
data.unlinked | boolean | true when the link is gone after the call |
Example request
curl -X DELETE \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tableName":"customers","propertyId":"tags","referenceId":"65f1a2b3c4d5e6f7a8b9c0d2"}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/records/65f1a2b3c4d5e6f7a8b9c0d1/link{ "errors": null, "data": { "unlinked": true } }{
"errors": {
"errorCode": "PROJECT_NOT_FOUND",
"errorMessage": "Project does not exist or you don't own it"
},
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
MISSING_RECORD_ID | 400 | recordId is required |
MISSING_TABLE_NAME | 400 | tableName is required |
MISSING_PROPERTY_ID | 400 | propertyId is required — the relation field on the table |
MISSING_REFERENCE_ID | 400 | referenceId is required — the _id of the record to unlink |
TABLE_NOT_FOUND | 400 | Table doesn't exist in the project |
