Monthly Cohort — the first 100 early-access sign-ups, plus the first 60 every month after launch, get their first month freePersonal plans COHORT60
API reference

The database, over HTTP.

Every InventDB instance serves this HTTP API directly — SQL, record CRUD, pre-built queries, bulk writes, vector search, index management, the relationship graph, the workflows engine and per-record attachments. Every example below is copy-paste and fully working against a real property-management dataset (the pms namespace). Your instance also serves an interactive version at /api/docs (Scalar) and /api/docs/swagger (try-it-out), plus the raw spec at /api/openapi.json.

Base URL: https://<slug>.sandbox.inventdb.com Auth: Authorization: Bearer <JWT> Get a token: POST /api/auth/login OpenAPI 3.1: /api/openapi.json
Product family. SOAR (AI Platform) = Serverless plus the AI/LLM endpoints (SOAR only). Serverless serves everything except those. Each instance’s own /api/openapi.json is filtered to its tier.

Authentication

Session and credential endpoints for InventDB. Exchange a username and password for a JWT via login, then send that token as Authorization: Bearer <token> on every protected request. get_current_user resolves the caller's identity and role (globally or per-app), change_password rotates the caller's own credential, forgot_password kicks off an out-of-band reset, and auth_health reports whether auth enforcement is on for the instance.

POST/api/auth/loginpublic SOAR + Serverless

Exchanges a username and password for a signed JWT. Use the returned token as Authorization: Bearer <token> on every subsequent protected call, and read user for the caller's id, global role, and email. On bad credentials the endpoint returns 401 with ok:false and an error message (token is null). This is the entry point for every API session; no token is required to call it.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/auth/login' \
  -H 'content-type: application/json' \
  -d '{ "username": "june", "password": "CopperlineSOAR2026!" }'
{
  "username": "june",
  "password": "CopperlineSOAR2026!"
}

Response 200

{
  "ok": true,
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOWI0ZTIzOS0yN2ZmLTRmMzgtOTQ5Yi0zOGIzMzJiM2Y4ZWQiLCJyb2xlIjoic3VwZXJhZG1pbiIsImlhdCI6MTc4Mzk0MDQ2NiwiZXhwIjoxNzgzOTY5MjY2fQ.CbNXBqWVB2lQs06miCu3f-Gyn2J9p0HAdn1hHmc7P3E",
  "user": {
    "id": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
    "username": "june",
    "role": "superadmin",
    "email": "developer.3@inventdb.com",
    "isActive": true,
    "globalRole": "superadmin"
  }
}

Response 401

{
  "ok": false,
  "token": null,
  "user": null,
  "error": "Invalid username or password"
}
GET/api/auth/meBearer SOAR + Serverless

Returns the identity behind the bearer token: id, username, role, email, and active flag. Without ?app= the role field is the user's global role from _system.users. Pass ?app=<software_id> (for example ?app=pms) to get the per-app role from app_roles[<id>] instead — though a global superadmin/admin always wins (RBAC bypass). A missing or expired token yields 401.

Path & query parameters

NameInTypeDescription
appquerystringOptional app/software id, e.g. pms. When set, role is the per-app role and globalRole carries the raw global role. Empty role means the user is not provisioned for that app.

Request

curl -s 'https://acme.sandbox.inventdb.com/api/auth/me?app=pms' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "user": {
    "id": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
    "username": "june",
    "role": "superadmin",
    "email": "developer.3@inventdb.com",
    "isActive": true,
    "globalRole": "superadmin"
  }
}

Response 401

{
  "ok": false,
  "user": null,
  "error": "Missing or invalid bearer token"
}
POST/api/auth/change-passwordBearer SOAR + Serverless

Rotates the password of the currently authenticated user. Supply the current_password for verification and the desired new_password; both fields are snake_case. On success the endpoint returns { "ok": true }. If the current password is wrong it returns 401, and if the user record can't be found it returns 404 — both carry ok:false plus an error string.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/auth/change-password' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "current_password": "CopperlineSOAR2026!", "new_password": "MapleRidge-2026#PMS" }'
{
  "current_password": "CopperlineSOAR2026!",
  "new_password": "MapleRidge-2026#PMS"
}

Response 200

{
  "ok": true
}

Response 401

{
  "ok": false,
  "error": "Current password incorrect"
}
POST/api/auth/forgot-passwordpublic SOAR + Serverless

Requests a password-reset link for an account, identified by verified email (common path) or by username (for accounts with no email on file, relayed via cmgmt). Anonymous — no JWT required. To avoid leaking which addresses are registered, it always returns 200 with { "ok": true } whether or not a user matched; when a match is found and a delivery channel is wired, the recovery link is sent out-of-band and email_dispatched may be present.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/auth/forgot-password' \
  -H 'content-type: application/json' \
  -d '{ "email": "june@inventdb.com" }'
{
  "email": "june@inventdb.com"
}

Response 200

{
  "ok": true
}
GET/api/auth/healthpublic SOAR + Serverless

Reports whether authentication is enforced on this instance. Anonymous and cheap — use it from a client bootstrap to decide whether to prompt for login, or from a monitor to confirm the auth subsystem is up. Responds 200 with { "enabled": true } when a bearer token is required for protected routes.

Request

curl -s 'https://acme.sandbox.inventdb.com/api/auth/health'

Response 200

{
  "enabled": true
}

System & discovery

Liveness and schema-discovery endpoints. Use /health for uptime and first-run detection, then walk the data plane top-down: list the namespaces you can see, drill into the types inside one, and pull per-type record counts and storage telemetry. These are the calls the InventDB Console uses to render its Browser tab.

GET/healthpublic SOAR + Serverless

Anonymous liveness check — no token required, and callable while the server is still in setup mode. Returns the running engine version plus setupRequired, which is derived from DB state (whether an active superadmin/admin exists in _system.users), not from config.json. Poll this from load balancers and uptime monitors, or read setupRequired to decide whether to route a new operator into the first-run wizard.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/health'

Response 200

{
  "status": "healthy",
  "version": "0.1.0",
  "setupRequired": false,
  "tenantProduct": "ai"
}
GET/api/namespacesBearer SOAR + Serverless

Lists the namespaces (databases) the caller can see, as a bare sorted JSON array of names. RBAC filters the result to only the namespaces the caller's grants admit; superadmin and admin see everything. Pass ?includeSystem=false to hide the engine's _-prefixed system namespaces (such as _system and _soar), leaving just your application namespaces like pms.

Path & query parameters

NameInTypeDescription
includeSystemquerybooleanInclude _-prefixed system namespaces such as _system/_soar. Default true. Set false to return only application namespaces like pms.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/api/namespaces?includeSystem=false' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

[
  "pms"
]

With the default includeSystem=true, the same caller (superadmin) also sees the engine's internal namespaces:

[
  "_soar",
  "_system",
  "pms"
]
GET/api/namespaces/{namespace}/typesBearer SOAR + Serverless

Lists the types (tables) inside a namespace. By default returns a bare array of type names such as ["owners", "properties", …]; pass ?metadata=true to instead get an array of {name, recordCount, …} objects. RBAC filters to the types the caller can read, and engine-internal _* types (attachments, embeddings, relationships) are hidden from non-admins.

Path & query parameters

NameInTypeDescription
namespacepathstringThe namespace to inspect, e.g. pms.
metadataquerybooleanIf true, the response is an array of TypeInfo objects ({name, recordCount, …}) instead of bare name strings. Default false.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/api/namespaces/pms/types' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

[
  "compliance",
  "daily_tasks",
  "inspections",
  "leases",
  "owners",
  "properties",
  "tenants",
  "transactions",
  "vendors",
  "work_orders"
]

With ?metadata=true each entry becomes an object carrying the live record count:

[
  { "name": "owners",       "recordCount": 10,   "indexStoreCacheSizeBytes": 0 },
  { "name": "properties",   "recordCount": 50,   "indexStoreCacheSizeBytes": 0 },
  { "name": "leases",       "recordCount": 46,   "indexStoreCacheSizeBytes": 0 },
  { "name": "tenants",      "recordCount": 46,   "indexStoreCacheSizeBytes": 0 },
  { "name": "transactions", "recordCount": 2261, "indexStoreCacheSizeBytes": 0 },
  { "name": "work_orders",  "recordCount": 95,   "indexStoreCacheSizeBytes": 0 }
]
GET/api/{namespace}Bearer SOAR + Serverless

Returns every type in a namespace with its per-type record count plus storage stats — the call that powers the InventDB Console "Browser" tab. For admin/superadmin the response includes dataSizeBytes, idLocatorSizeBytes, indexSizeBytes, and totalSizeBytes. Non-admin callers get only {namespace, typeName, recordCount} — the storage telemetry is stripped. Use it to build a namespace overview or to spot which types dominate on-disk footprint.

Path & query parameters

NameInTypeDescription
namespacepathstringThe namespace to summarize, e.g. pms.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/api/pms' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

[
  {
    "namespace": "pms",
    "typeName": "properties",
    "recordCount": 50,
    "dataSizeBytes": 30786,
    "idLocatorSizeBytes": 16384,
    "indexSizeBytes": 1118236,
    "totalSizeBytes": 1165406
  },
  {
    "namespace": "pms",
    "typeName": "owners",
    "recordCount": 10,
    "dataSizeBytes": 5239,
    "idLocatorSizeBytes": 8192,
    "indexSizeBytes": 352271,
    "totalSizeBytes": 365702
  },
  {
    "namespace": "pms",
    "typeName": "transactions",
    "recordCount": 2261,
    "dataSizeBytes": 1060769,
    "idLocatorSizeBytes": 307200,
    "indexSizeBytes": 8818703,
    "totalSizeBytes": 10186672
  },
  {
    "namespace": "pms",
    "typeName": "work_orders",
    "recordCount": 95,
    "dataSizeBytes": 44073,
    "idLocatorSizeBytes": 20480,
    "indexSizeBytes": 774160,
    "totalSizeBytes": 838713
  }
]

A non-admin caller sees the same rows without the size fields:

[
  { "namespace": "pms", "typeName": "properties", "recordCount": 50 },
  { "namespace": "pms", "typeName": "leases",     "recordCount": 46 }
]

SQL

Direct SQL execution via POST /sql. This is the primary read path into the InventDB engine — SELECT-only on the data plane today (writes go through db / bulk). The caller's JWT principal is enforced as a row-level-security (RLS) scope over every referenced table, and an optional ?metrics flag wraps the rows in a server-side timing envelope.

POST/sqlBearer SOAR + Serverless

Executes a SQL query against the InventDB engine and returns the matching rows. Use it for every read: filtered SELECTs, multi-table JOINs, GROUP BY aggregates, ORDER BY / LIMIT windowing, and semantic MEANING() search. RLS is applied automatically from the caller's principal (superadmin / admin / setup bypass it). Append ?metrics=1 to get a { rows, metrics } wrapper with server-side timing instead of the bare array. CRUD (INSERT / UPDATE / DELETE) is rejected here — those belong on /db/:namespace/:type/*.

Result keys mirror your SELECT list verbatim. A bare qualified column such as p.property_id is returned under the qualified key "p.property_id" (kept intact so JOINed tables that share a column name don't collide). Add an explicit alias — SELECT p.property_id AS property_id — whenever you want clean, unqualified keys in the response.

Path & query parameters

NameInTypeDescription
metricsquerystringOptional. 1 or true switches the response from a bare JSON array to a { "rows": [...], "metrics": {...} } wrapper carrying elapsedMs, count, truncated, and rowCap. Omit it (or any other value) for the legacy bare-array shape.

Request body

NameInTypeDescription
sqlbodystringRequired. The SQL text, executed verbatim under the caller's RLS scope. Identifiers are namespace.type (e.g. pms.properties). SELECT-only. MEANING(text_col) = 'phrase' / LIKE routes through the embedded MiniLM-L6-v2 semantic search (falls back to text-equality if no embedder is loaded).
parametersbodyobject | nullOptional / reserved. Placeholder for prepared-statement binds. The engine inlines literals today — put values directly in sql.

Request — filtered SELECT

curl -s -X POST 'https://acme.sandbox.inventdb.com/sql' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "sql": "SELECT property_id, city, state, type, beds, baths, market_rent, market_value, status FROM pms.properties WHERE state = '\''VA'\'' AND status = '\''Occupied'\'' ORDER BY market_rent DESC LIMIT 3" }'
{
  "sql": "SELECT property_id, city, state, type, beds, baths, market_rent, market_value, status FROM pms.properties WHERE state = 'VA' AND status = 'Occupied' ORDER BY market_rent DESC LIMIT 3"
}

Response 200 — bare array (no ?metrics)

[
  {
    "property_id": "P-5012",
    "city": "Herndon",
    "state": "VA",
    "type": "Single Family",
    "beds": 3,
    "baths": 4.5,
    "market_rent": 4675,
    "market_value": 749000,
    "status": "Occupied"
  },
  {
    "property_id": "P-5001",
    "city": "Charlottesville",
    "state": "VA",
    "type": "Single Family",
    "beds": 5,
    "baths": 4,
    "market_rent": 3775,
    "market_value": 668000,
    "status": "Occupied"
  },
  {
    "property_id": "P-5045",
    "city": "Vienna",
    "state": "VA",
    "type": "Townhouse",
    "beds": 4,
    "baths": 3,
    "market_rent": 3650,
    "market_value": 720000,
    "status": "Occupied"
  }
]

Request — JOIN with the metrics envelope

curl -s -X POST 'https://acme.sandbox.inventdb.com/sql?metrics=1' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "sql": "SELECT p.property_id, p.city, l.tenant_name, l.contract_rent, l.status FROM pms.properties p JOIN pms.leases l ON l.property_id = p.property_id WHERE l.status = '\''Active'\'' ORDER BY l.contract_rent DESC LIMIT 3" }'
{
  "sql": "SELECT p.property_id, p.city, l.tenant_name, l.contract_rent, l.status FROM pms.properties p JOIN pms.leases l ON l.property_id = p.property_id WHERE l.status = 'Active' ORDER BY l.contract_rent DESC LIMIT 3"
}

Response 200{ rows, metrics } wrapper

JOIN result columns are returned under their alias.column keys (e.g. p.property_id, l.tenant_name). The metrics block reports elapsedMs (parse + plan + execute + serialize; excludes network RTT), count (rows after RLS, equal to rows.length), and the unbounded-read guard (truncated / rowCap).

{
  "rows": [
    { "p.property_id": "P-5012", "p.city": "Herndon", "l.tenant_name": "Jordan Cooper", "l.contract_rent": 4625, "l.status": "Active" },
    { "p.property_id": "P-5001", "p.city": "Charlottesville", "l.tenant_name": "Eric Coleman", "l.contract_rent": 3775, "l.status": "Active" },
    { "p.property_id": "P-5045", "p.city": "Vienna", "l.tenant_name": "Imani Edwards", "l.contract_rent": 3650, "l.status": "Active" }
  ],
  "metrics": {
    "elapsedMs": 51,
    "count": 3,
    "truncated": false,
    "rowCap": 1000
  }
}

Request — GROUP BY aggregate

curl -s -X POST 'https://acme.sandbox.inventdb.com/sql?metrics=1' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "sql": "SELECT city, COUNT(*) AS units, AVG(market_rent) AS avg_rent, SUM(market_value) AS portfolio_value FROM pms.properties GROUP BY city ORDER BY portfolio_value DESC LIMIT 5" }'
{
  "sql": "SELECT city, COUNT(*) AS units, AVG(market_rent) AS avg_rent, SUM(market_value) AS portfolio_value FROM pms.properties GROUP BY city ORDER BY portfolio_value DESC LIMIT 5"
}

Response 200 — aggregated rows

Aggregates come back as JSON numbers (AVG / SUM as floats, COUNT as an integer) under the projected column alias. Note the grouped text key (city) is normalized to lowercase by the group operator.

{
  "rows": [
    { "city": "charlottesville", "units": 6, "avg_rent": 2329.1666666666665, "portfolio_value": 2566000.0 },
    { "city": "richmond",        "units": 5, "avg_rent": 1920.0,             "portfolio_value": 1878000.0 },
    { "city": "virginia beach",  "units": 6, "avg_rent": 1587.5,             "portfolio_value": 1801000.0 },
    { "city": "vienna",          "units": 3, "avg_rent": 2791.6666666666665, "portfolio_value": 1514000.0 },
    { "city": "williamsburg",    "units": 3, "avg_rent": 2491.6666666666665, "portfolio_value": 1357000.0 }
  ],
  "metrics": {
    "elapsedMs": 24,
    "count": 5,
    "truncated": false,
    "rowCap": 1000
  }
}

Error responses

StatusWhenBody
400Empty sql, malformed SQL, or a write statement (INSERT / UPDATE / DELETE) — the engine returns Unsupported statement type.{ "error": "SQL error: ..." }
403RBAC / RLS denied the principal access to one or more referenced tables.{ "error": "..." }
500Engine runtime error (planner, executor, serialization).{ "error": "..." }

Record CRUD (db)

Record CRUD over /db/:namespace/:type[/:id]. Insert, read, update, and delete individual documents in any type, plus a cheap id-listing walk. The engine is schemaless — new properties and new types are created on first write, no migration step. Writes stamp _id, _createdAt, _createdBy, _updatedAt, and _updatedBy from the JWT principal, validate _relationships referential integrity (409 on conflict), and enforce row-level security so you only touch rows your grants admit.

POST/db/{namespace}/{type}Bearer SOAR + Serverless

Insert a single document into :namespace.:type. The full JSON body is the document — there is no envelope. The engine assigns _id (UUID v4 unless you supply one) and stamps the created/updated audit fields from your token. New namespaces and types are created lazily on first write, and any new property is indexed on first sight. Use ?disableIndexing=true to defer property + vector index maintenance during bulk loads.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms. Created lazily if new.
typepathstringe.g. properties. Created lazily if new.
disableIndexingquerybooleanSkip property + vector index updates for this write.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/db/pms/properties' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{
    "property_id": "P-5099",
    "owner_id": "O-1002",
    "street": "88 Maple Ct",
    "city": "Charlottesville",
    "state": "VA",
    "zip": 22902,
    "region": "Central VA",
    "type": "Condo",
    "beds": 2,
    "baths": 2.5,
    "sqft": 963,
    "year_built": 1990,
    "market_rent": 1525,
    "market_value": 253000,
    "acq_date": "2008-02-27",
    "status": "Occupied"
  }'
{
  "property_id": "P-5099",
  "owner_id": "O-1002",
  "street": "88 Maple Ct",
  "city": "Charlottesville",
  "state": "VA",
  "zip": 22902,
  "region": "Central VA",
  "type": "Condo",
  "beds": 2,
  "baths": 2.5,
  "sqft": 963,
  "year_built": 1990,
  "market_rent": 1525,
  "market_value": 253000,
  "acq_date": "2008-02-27",
  "status": "Occupied"
}

Response 201

{
  "ok": true,
  "id": "d5f17a05-5806-48bf-88ab-e9a586bd06b9"
}
PUT/db/{namespace}/{type}Bearer SOAR + Serverless

Update an existing document by _id, which MUST be present in the request body. The body you send becomes the new latest version (append-only at the storage layer — older versions stay readable via point-in-time queries). Row-level filters must accept both the pre-image and the post-image: you can neither edit a row you don't own nor move one out of your ownership scope. Missing _id is 400; no matching row is 404; a referential-integrity conflict is 409.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms.
typepathstringe.g. properties.
disableIndexingquerybooleanSkip index updates for this write.

Request

curl -s -X PUT 'https://acme.sandbox.inventdb.com/db/pms/properties' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{
    "_id": "d5f17a05-5806-48bf-88ab-e9a586bd06b9",
    "property_id": "P-5099",
    "owner_id": "O-1002",
    "market_rent": 1595,
    "status": "Vacant"
  }'
{
  "_id": "d5f17a05-5806-48bf-88ab-e9a586bd06b9",
  "property_id": "P-5099",
  "owner_id": "O-1002",
  "market_rent": 1595,
  "status": "Vacant"
}

Response 200

{
  "ok": true,
  "id": "d5f17a05-5806-48bf-88ab-e9a586bd06b9"
}
GET/db/{namespace}/{type}/{id}Bearer SOAR + Serverless

Fetch a single document by _id. The response body is the document JSON itself — no envelope, including the system fields _id, _createdAt, and _updatedAt. RLS row filters apply: a row outside your scope returns 404 rather than 403, so the API never leaks whether an id you can't see exists.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms.
typepathstringe.g. properties.
idpathstringDocument _id, e.g. 2bba668e-ad1c-447b-9e70-7cf9d9221b2c.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/db/pms/properties/2bba668e-ad1c-447b-9e70-7cf9d9221b2c' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "property_id": "P-5009",
  "owner_id": "O-1001",
  "street": "5247 Heritage Park Dr",
  "city": "Virginia Beach",
  "state": "VA",
  "zip": 23451,
  "region": "Hampton Rds",
  "type": "Townhouse",
  "beds": 3,
  "baths": 2,
  "sqft": 1181,
  "year_built": 1968,
  "lot_ac": 0,
  "garage": "None",
  "market_rent": 1550,
  "market_value": 299000,
  "acq_date": "2009-07-05",
  "acq_price": 235000,
  "annual_tax": 3000,
  "annual_insurance": 1050,
  "monthly_hoa": 165,
  "status": "Occupied",
  "_id": "2bba668e-ad1c-447b-9e70-7cf9d9221b2c",
  "_createdAt": "2026-07-10T02:16:36.446031800+00:00",
  "_updatedAt": "2026-07-10T02:16:36.446031800+00:00"
}
DELETE/db/{namespace}/{type}/{id}Bearer SOAR + Serverless

Delete a document by _id. This is a soft delete: a tombstone supersedes the row for normal SELECTs, but earlier versions stay queryable for point-in-time reads. Any attachments owned by this _id are removed transactionally. A row protected by referential integrity — referenced by another type via _relationships — cannot be deleted until those references clear, and the call returns 409.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms.
typepathstringe.g. properties.
idpathstringDocument _id to delete.

Request

curl -s -X DELETE 'https://acme.sandbox.inventdb.com/db/pms/properties/d5f17a05-5806-48bf-88ab-e9a586bd06b9' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true
}
GET/db/{namespace}/{type}Bearer SOAR + Serverless

List all document _ids in a type. This is cheap — it walks the per-type id index only and reads no property values. RLS row filters still apply, so you only see the ids of rows you're allowed to read. For paged fetches of full rows, use POST /sql with LIMIT and ORDER BY instead of hydrating every id here.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms.
typepathstringe.g. properties.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/db/pms/properties' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

[
  "01c8183d-a16e-4612-91bd-13ce37774d59",
  "01e774f9-3c03-4a1b-9754-935e93d47df3",
  "0255a1f8-04be-4dc7-b33b-54c0ea590ab0",
  "07068d7b-c633-4312-b403-2c169c8b487a",
  "16498c30-9e60-4e8f-b79b-c443b48bc42b",
  "2bba668e-ad1c-447b-9e70-7cf9d9221b2c"
]

Pre-built queries

Convenience query wrappers that cover the most common single-type read patterns — substring search, numeric ranges, date ranges, and AND-composed multi-property filters — without hand-writing SQL. Each is a thin shim over the engine's SQL path (RLS row filters and per-table grants still apply), and every one returns a bare JSON array of full row objects. For OR semantics, joins, aggregation, or semantic MEANING() search, use POST /sql directly.

GET/query/{namespace}/{type}/containsBearer SOAR + Serverless

Substring search on a single string property. This is a convenience wrapper over SELECT * FROM ns.type WHERE prop LIKE '%q%'. It is fast on small types; for larger types prefer /sql with an explicit LIMIT. Use it for quick lookups like finding an owner by part of their name or a work order by a keyword in the issue text.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringType / table name, e.g. owners
propquerystringString property to scan, e.g. name
qquerystringSubstring to match (LIKE-style), e.g. Whitfield

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/query/pms/owners/contains?prop=name&q=Whitfield' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

[
  {
    "owner_id": "O-1004",
    "name": "Robert & Susan Whitfield",
    "type": "Joint",
    "contact": "Robert Whitfield",
    "phone": "(434) 555-5557",
    "email": "robert.whitfield97@example.com",
    "tax_id": "**-***3615",
    "w_9_on_file": true,
    "mailing_address": "7024 Patriot Way, Norfolk, VA 20191",
    "mgmt_fee": 0.09,
    "payout_method": "Check",
    "_id": "b9b11a69-608c-4434-82ed-363e80efa45b",
    "_createdAt": "2026-07-10T02:16:37.399111700+00:00",
    "_updatedAt": "2026-07-10T02:16:37.399111700+00:00"
  }
]
GET/query/{namespace}/{type}/rangeBearer SOAR + Serverless

Closed-interval numeric range query: lo <= prop <= hi. Both bounds are inclusive doubles. Use it to pull rows where a numeric column falls inside a window — for example properties whose market_rent is between two dollar amounts, or work orders in a cost band. Returns a bare array of full row objects.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringType / table name, e.g. properties
propquerystringNumeric property to range over, e.g. market_rent
loquerynumberInclusive lower bound, e.g. 1500
hiquerynumberInclusive upper bound, e.g. 1600

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/query/pms/properties/range?prop=market_rent&lo=1500&hi=1600' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

[
  {
    "property_id": "P-5033",
    "owner_id": "O-1002",
    "street": "8033 Willow Brook Pl",
    "city": "Charlottesville",
    "state": "VA",
    "zip": 22902,
    "region": "Central VA",
    "type": "Condo",
    "beds": 2,
    "baths": 2.5,
    "sqft": 963,
    "year_built": 1990,
    "market_rent": 1525,
    "market_value": 253000,
    "acq_date": "2008-02-27",
    "annual_tax": 2300,
    "annual_insurance": 825,
    "monthly_hoa": 285,
    "status": "Occupied",
    "_id": "260a5a18-8fd0-4c99-8808-3ae773cf0173",
    "_createdAt": "2026-07-10T02:16:36.446031800+00:00",
    "_updatedAt": "2026-07-10T02:16:36.446031800+00:00"
  },
  {
    "property_id": "P-5009",
    "owner_id": "O-1001",
    "street": "5247 Heritage Park Dr",
    "city": "Virginia Beach",
    "state": "VA",
    "zip": 23451,
    "region": "Hampton Rds",
    "type": "Townhouse",
    "beds": 3,
    "baths": 2,
    "sqft": 1181,
    "year_built": 1968,
    "market_rent": 1550,
    "market_value": 299000,
    "acq_date": "2009-07-05",
    "status": "Occupied",
    "_id": "2bba668e-ad1c-447b-9e70-7cf9d9221b2c",
    "_createdAt": "2026-07-10T02:16:36.446031800+00:00",
    "_updatedAt": "2026-07-10T02:16:36.446031800+00:00"
  }
]
GET/query/{namespace}/{type}/daterangeBearer SOAR + Serverless

Date / DateTime range query with inclusive bounds. Bounds are ISO 8601 strings; date-only literals are treated as midnight UTC, so to capture the entire end day pass a full timestamp such as end=2026-10-31T23:59:59Z. Use it for date-window reports like leases starting in a given month or transactions posted in a quarter. Returns a bare array of full row objects.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringType / table name, e.g. leases
propquerystringDate / datetime property to filter, e.g. lease_start
startquerystringInclusive start (ISO 8601), e.g. 2025-10-01
endquerystringInclusive end (ISO 8601), e.g. 2025-10-31

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/query/pms/leases/daterange?prop=lease_start&start=2025-10-01&end=2025-10-31T23:59:59Z' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

[
  {
    "lease_id": "L-7018",
    "property_id": "P-5020",
    "tenant_id": "T-9018",
    "tenant_name": "Emily Brooks",
    "lease_start": "2025-10-20",
    "lease_end": "2026-10-15",
    "term_mo": 12,
    "market_rent": 2675,
    "contract_rent": 2725,
    "deposit_held": 2725,
    "late_fee": 136,
    "grace_days": 5,
    "renewal_type": "Auto-renew",
    "status": "Active",
    "_id": "72a1ca6a-6964-445a-bf43-1921f011515d",
    "_createdAt": "2026-07-10T02:16:37.753951200+00:00",
    "_updatedAt": "2026-07-10T02:16:37.753951200+00:00"
  },
  {
    "lease_id": "L-7043",
    "property_id": "P-5046",
    "tenant_id": "T-9043",
    "tenant_name": "Ryan Clark",
    "lease_start": "2025-10-24",
    "lease_end": "2026-10-19",
    "term_mo": 12,
    "market_rent": 1850,
    "contract_rent": 1850,
    "deposit_held": 1850,
    "late_fee": 92,
    "grace_days": 5,
    "renewal_type": "Auto-renew",
    "status": "Active",
    "_id": "c98d5f7f-16e2-42f2-87d8-2bb8b3cae986",
    "_createdAt": "2026-07-10T02:16:37.753951200+00:00",
    "_updatedAt": "2026-07-10T02:16:37.753951200+00:00"
  }
]
POST/query/{namespace}/{type}/filterBearer SOAR + Serverless

AND-composed multi-property filter with pagination and ordering — a convenience over /sql for the very common "filter on N properties, sort, paginate" case. Each entry in conditions becomes one WHERE prop OP value clause and all clauses are AND'ed together; for OR semantics use /sql directly. Sort with order_by + order_dir and page with limit / offset. Returns a bare array of full row objects.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringType / table name, e.g. properties
conditionsbodyarrayRequired. AND'ed list of { prop, op, value }. Empty list returns all readable rows (subject to RLS).
conditions[].opbodystringOne of eq, ne, gt, gte, lt, lte, like (SQL LIKE), contains (case-sensitive substring).
order_bybodystring?Property to sort by, e.g. market_rent.
order_dirbodystring?asc (default) or desc, case-insensitive.
limitbodyinteger?Max rows. No cap enforced if absent — use with care.
offsetbodyinteger?Skip this many rows after order_by is applied.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/query/pms/leases/filter' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "conditions": [ { "prop": "status", "op": "eq", "value": "Active" }, { "prop": "contract_rent", "op": "gte", "value": 2500 } ], "order_by": "contract_rent", "order_dir": "desc", "limit": 2, "offset": 0 }'
{
  "conditions": [
    { "prop": "status", "op": "eq", "value": "Active" },
    { "prop": "contract_rent", "op": "gte", "value": 2500 }
  ],
  "order_by": "contract_rent",
  "order_dir": "desc",
  "limit": 2,
  "offset": 0
}

Response 200

[
  {
    "lease_id": "L-7012",
    "property_id": "P-5012",
    "tenant_id": "T-9012",
    "tenant_name": "Jordan Cooper",
    "lease_start": "2025-10-26",
    "lease_end": "2026-10-21",
    "term_mo": 12,
    "market_rent": 4675,
    "contract_rent": 4625,
    "deposit_held": 4625,
    "late_fee": 231,
    "grace_days": 5,
    "renewal_type": "Auto-renew",
    "status": "Active",
    "_id": "c129df1c-73b9-4339-8fc8-ad271fd505e8",
    "_createdAt": "2026-07-10T02:16:37.753951200+00:00",
    "_updatedAt": "2026-07-10T02:16:37.753951200+00:00"
  },
  {
    "lease_id": "L-7001",
    "property_id": "P-5001",
    "tenant_id": "T-9001",
    "tenant_name": "Eric Coleman",
    "lease_start": "2026-04-04",
    "lease_end": "2027-03-30",
    "term_mo": 12,
    "market_rent": 3775,
    "contract_rent": 3775,
    "deposit_held": 3775,
    "late_fee": 189,
    "grace_days": 5,
    "renewal_type": "Auto-renew",
    "status": "Active",
    "_id": "01ba54c2-728a-4357-b184-0bff550400cd",
    "_createdAt": "2026-07-10T02:16:37.753951200+00:00",
    "_updatedAt": "2026-07-10T02:16:37.753951200+00:00"
  }
]

Bulk operations

High-throughput, whole-batch operations against a type: insert many documents in one round-trip, and remove rows en masse by id, by filter, or all at once. Destructive endpoints layer on guard rails — referential-integrity checks, an explicit CONFIRM phrase, and admin-only type/namespace drops — so a single call can never quietly cascade.

POST/api/{namespace}/{type}/bulkBearer SOAR + Serverless

Insert N documents in a single request instead of one POST per row. Accepts either a bare JSON array or an { "documents": [...] } envelope. RBAC and the writer's row-filter are validated once up front, then the batch streams into the engine; per-row failures are aggregated into error while the ids that landed are still returned. A successful insert responds 201 Created; an empty request (zero documents) is a 200 no-op. Pass ?disableIndexing=true on large loads and rebuild indexes afterward.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms. Created lazily on first write.
typepathstringe.g. properties. Created lazily on first write.
disableIndexingquerybooleanSkip property + vector index maintenance for write-throughput-sensitive loads. Rebuild via /api/indexes/:ns/:type/rebuild after.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/pms/properties/bulk?disableIndexing=true' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '[
    { "property_id": "P-5033", "owner_id": "O-1002", "street": "418 Rugby Rd", "city": "Charlottesville", "state": "VA", "zip": 22903, "type": "Condo", "beds": 2, "baths": 2.5, "sqft": 963, "market_rent": 1525, "market_value": 253000, "status": "occupied" },
    { "property_id": "P-5034", "owner_id": "O-1004", "street": "77 Elliott Ave", "city": "Charlottesville", "state": "VA", "zip": 22902, "type": "Townhome", "beds": 3, "baths": 2.0, "sqft": 1480, "market_rent": 1975, "market_value": 312000, "status": "vacant" }
  ]'
{
  "documents": [
    { "property_id": "P-5033", "owner_id": "O-1002", "street": "418 Rugby Rd", "city": "Charlottesville", "state": "VA", "zip": 22903, "type": "Condo", "beds": 2, "baths": 2.5, "sqft": 963, "market_rent": 1525, "market_value": 253000, "status": "occupied" },
    { "property_id": "P-5034", "owner_id": "O-1004", "street": "77 Elliott Ave", "city": "Charlottesville", "state": "VA", "zip": 22902, "type": "Townhome", "beds": 3, "baths": 2.0, "sqft": 1480, "market_rent": 1975, "market_value": 312000, "status": "vacant" }
  ]
}

Response 201

{
  "ok": true,
  "insertedCount": 2,
  "ids": [
    "4e7d7e56-a14c-47d9-acf1-d6b9187f94f6",
    "12b2083a-e03e-4c35-8a79-89068fcd5676"
  ],
  "error": null
}
POST/api/{namespace}/{type}/deleteManyBearer SOAR + Serverless

Delete a set of documents by _id in one call. Referential integrity is validated for the entire batch before any row is removed — if any id is referenced by another type via _relationships, the whole call aborts with 409 and nothing is deleted. Ids that don't exist are skipped silently, so deletedCount may be less than ids.length. Returns 206 if some individual deletes errored after the RI gate passed.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. work_orders

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/pms/work_orders/deleteMany' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "ids": ["93906841-8a89-4310-9d61-e056bf72c330", "cc32867d-d243-444f-89ba-57f6c546172d"] }'
{
  "ids": [
    "93906841-8a89-4310-9d61-e056bf72c330",
    "cc32867d-d243-444f-89ba-57f6c546172d"
  ]
}

Response 200

{
  "ok": true,
  "deletedCount": 1,
  "error": null
}
POST/api/{namespace}/{type}/deleteBearer SOAR + Serverless

Delete every row that matches a filter condition. The engine runs a SELECT _id with the filter applied, then deletes each matched row. The filter is a property→value map whose terms are AND'ed into a SQL WHERE clause — e.g. {"status": "void", "year": 2024} becomes WHERE status = 'void' AND year = 2024. Safer than deleteAll because the filter shape forces an explicit decision about what to drop.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. transactions

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/pms/transactions/delete' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "filter": { "type": "Expense", "account_category": "Property Tax", "property_id": "P-5033" } }'
{
  "filter": {
    "type": "Expense",
    "account_category": "Property Tax",
    "property_id": "P-5033"
  }
}

Response 200

{
  "ok": true,
  "deletedCount": 3,
  "error": null
}
POST/api/{namespace}/{type}/deleteAllBearer SOAR + Serverless

Delete every row in a type — a truncate. The request body must carry the literal confirmation phrase "CONFIRM"; anything else refuses with 400 to guard against accidental cascades. The type folder, its properties, and history rows remain (use DELETE /api/:ns/:type to drop the type entirely). Requires delete access on the type.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. leases

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/pms/leases/deleteAll' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "confirm": "CONFIRM" }'
{
  "confirm": "CONFIRM"
}

Response 200

{
  "ok": true,
  "deletedCount": 118,
  "error": null
}
DELETE/api/{namespace}/{type}Bearer SOAR + Serverless

Drop an entire type — its data directory, segments, indexes, and attachments — from disk. This is destructive and irreversible: unlike a soft-deleted row you cannot point-in-time-query it back. Admin / superadmin only. Use deleteAll instead if you only want to empty the rows but keep the type and its schema.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. inspections

Request

curl -s -X DELETE 'https://acme.sandbox.inventdb.com/api/pms/inspections' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "ok": true,
    "message": "Type pms.inspections deleted successfully"
  }
}
DELETE/api/namespaces/{namespace}Bearer SOAR + Serverless

Drop an entire namespace and every type inside it — the whole namespace folder is removed. This is the most destructive operation in the API, equivalent to dropping every type sequentially. Admin / superadmin only. There is no undo and no point-in-time recovery once the folder is gone.

Path & query parameters

NameInTypeDescription
namespacepathstringThe namespace to drop, e.g. pms_archive_2024.

Request

curl -s -X DELETE 'https://acme.sandbox.inventdb.com/api/namespaces/pms_archive_2024' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "ok": true,
    "message": "Namespace pms_archive_2024 deleted successfully"
  }
}

Vector embeddings

Universal vector embedding pipeline — MEANING() SQL is wired in transparently against per-segment HNSW indexes. Every user type with string properties is embedded automatically (MiniLM all-MiniLM-L6-v2, 384 dims); new writes are embedded inline and sealed segments are backfilled by a background worker. These admin-only endpoints expose install status, an aggregated per-segment overview for the Settings → Vector Indexes card, and a one-shot backfill trigger. You almost never call these to search — semantic search is done in SQL, e.g. WHERE MEANING('quiet condo near downtown') > 0.5.

GET/api/vectors/universal/statusBearer SOAR + Serverless

Returns whether the universal vector embedding pipeline is installed and loadable on this instance. Use it as a lightweight health probe before relying on MEANING() semantic search in SQL, or from the admin console to confirm the MiniLM model + tokenizer files were found at boot. Admin/superadmin only; a non-admin caller gets 403.

Path & query parameters

None. The status is instance-wide, not scoped to a namespace or type.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/api/vectors/universal/status' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "embeddingGeneratorInstalled": true,
  "modelFilesExist": true,
  "note": "Universal vector embedding pipeline is active. New writes are auto-embedded; sealed segments are auto-backfilled in background."
}
GET/api/vectors/universal/overviewBearer SOAR + Serverless

Aggregates per-segment vector index counts across every user namespace.type (for example every table in the pms namespace) into a single round-trip for the Settings → Vector Indexes card. Cheap and lock-free — it walks each type's segments.json manifest from disk and tallies; it runs no DB queries. Use it to see how many sealed segments are still vectors_pending versus fully indexed. Admin/superadmin only.

Path & query parameters

None. Counts are aggregated across all user (non-_) namespaces and types.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/api/vectors/universal/overview' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "embeddingGeneratorInstalled": true,
  "modelName": "all-MiniLM-L6-v2",
  "modelDimensions": 384,
  "typesTotal": 14,
  "activeSegments": 14,
  "sealedSegmentsTotal": 6,
  "sealedSegmentsIndexed": 5,
  "sealedSegmentsPending": 1,
  "note": "Pipeline active. 1 sealed segment marked vectors_pending across pms.transactions; background worker will index it shortly."
}

Here typesTotal: 14 reflects the 14 pms tables (properties, owners, leases, tenants, transactions, work_orders, vendors, inspections, compliance, daily_tasks, …). Each type has exactly one active write segment, so activeSegments equals the number of types with data on disk. sealedSegmentsPending counts sealed segments the background backfill worker still needs to embed.

POST/api/vectors/universal/backfillBearer SOAR + Serverless

Triggers a one-shot, synchronous backfill across ALL types/segments tagged vectors_pending. A background thread already does this continuously, but this endpoint forces an immediate run from the admin console — useful right after a bulk import into pms.transactions or pms.leases when you want semantic search over the new rows without waiting for the worker. Returns the number of segments processed and wall-clock time. Admin/superadmin only; internal failure returns 500.

Path & query parameters

None. There is no request body — the backfill scope is every pending segment across all user types.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/vectors/universal/backfill' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "processedSegments": 1,
  "elapsedMs": 3
}

When nothing is pending (all sealed segments already indexed), the response is { "processedSegments": 0, "elapsedMs": 3 } and the call is effectively a no-op.

Indexes (admin)

Property B-tree index management — inspect per-property index statistics, run full rebuilds (Server-Sent Events stream), poll rebuild progress, consolidate (flush + fsync) indexes to disk, and pause/resume property indexing globally during bulk loads. Every endpoint is admin-only; the index files live on disk under <data_root>/<ns>/<type>/prop_*.tree.

GET/api/indexesBearer SOAR + Serverless

Returns a status grid for every user-namespace type, with per-property B-tree statistics (tree height, leaf count, distinct values, estimated cardinality). Use it to power an admin index-health dashboard or to decide which types need a rebuild. Admin-only; internal _* namespaces are skipped.

Response 200

curl -s 'https://acme.sandbox.inventdb.com/api/indexes' \
  -H 'Authorization: Bearer $TOKEN'
{
  "ok": true,
  "indexes": [
    {
      "namespace": "pms",
      "type": "properties",
      "recordCount": 500,
      "indexStatus": "active",
      "lastRebuilt": "2026-07-13T11:08:20.504Z",
      "propertyIndexes": [
        {
          "propertyName": "property_id",
          "totalEntries": 500,
          "treeHeight": 3,
          "leafCount": 12,
          "distinctValues": 500,
          "estimatedCardinality": 500
        },
        {
          "propertyName": "status",
          "totalEntries": 500,
          "treeHeight": 2,
          "leafCount": 2,
          "distinctValues": 4,
          "estimatedCardinality": 200
        },
        {
          "propertyName": "market_rent",
          "totalEntries": 500,
          "treeHeight": 3,
          "leafCount": 10,
          "distinctValues": 318,
          "estimatedCardinality": 200
        }
      ]
    },
    {
      "namespace": "pms",
      "type": "leases",
      "recordCount": 46,
      "indexStatus": "active",
      "lastRebuilt": null,
      "propertyIndexes": [
        {
          "propertyName": "status",
          "totalEntries": 46,
          "treeHeight": 2,
          "leafCount": 2,
          "distinctValues": 3,
          "estimatedCardinality": 18
        }
      ]
    }
  ]
}

indexStatus is active under normal operation; it reads rebuilding while a rebuild is in flight, or stale when property indexes lag the latest writes (usually self-heals on the next checkpoint).

GET/api/indexes/{namespace}/{type}/progressBearer SOAR + Serverless

Polling alternative to the SSE rebuild stream — returns a single snapshot of rebuild progress for one type. status is one of idle, rebuilding, completed, or failed. Poll this from tooling that can't (or shouldn't) hold an EventSource connection open while a rebuild runs.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. properties

Response 200

curl -s 'https://acme.sandbox.inventdb.com/api/indexes/pms/properties/progress' \
  -H 'Authorization: Bearer $TOKEN'
{
  "ok": true,
  "namespace": "pms",
  "type": "properties",
  "status": "idle",
  "progressPercent": null,
  "recordsProcessed": null,
  "totalRecords": null
}

While a rebuild is active the same shape carries live numbers, e.g. "status": "rebuilding", "progressPercent": 42.0, "recordsProcessed": 210, "totalRecords": 500.

GET/api/indexes/{namespace}/{type}/rebuildBearer SOAR + Serverless

Runs a full index rebuild for one type: it walks every row, drops the existing prop_*.tree files, and re-indexes from scratch. The response is a Server-Sent Events stream of progress frames; the terminal data: frame has status: completed or failed, followed by an event: done frame. Because EventSource can't send an Authorization header, pass the JWT via the ?token= query parameter instead.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. leases
tokenquerystringJWT bearer token (EventSource has no header support).

Request

curl -sN 'https://acme.sandbox.inventdb.com/api/indexes/pms/leases/rebuild?token=$TOKEN'

Response 200 text/event-stream

data: {"namespace":"pms","type":"leases","status":"rebuilding","totalRecords":0,"processedRecords":0,"progressPercentage":0.0,"startTime":"2026-07-13T11:08:16.337Z","elapsedMs":0}

data: {"namespace":"pms","type":"leases","status":"rebuilding","totalRecords":46,"processedRecords":0,"progressPercentage":0.0,"startTime":"2026-07-13T11:08:16.337Z","elapsedMs":0}

data: {"namespace":"pms","type":"leases","status":"completed","totalRecords":46,"processedRecords":46,"progressPercentage":100.0,"startTime":"2026-07-13T11:08:16.337Z","endTime":"2026-07-13T11:08:20.504Z","elapsedMs":4167}

event: done
data: {}
POST/api/{namespace}/{type}/reindexBearer SOAR + Serverless

POST variant of the SSE rebuild — same outcome as GET /api/indexes/:ns/:type/rebuild, but addressable via POST so tooling that can't open an EventSource connection can still kick off a rebuild. It runs as the system principal and streams the same progress frames back. Prefer this from scripts and background jobs; prefer the GET/SSE form for browser-driven admin consoles.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. leases

Request

curl -sN -X POST 'https://acme.sandbox.inventdb.com/api/pms/leases/reindex' \
  -H 'Authorization: Bearer $TOKEN'

Response 200 text/event-stream

data: {"namespace":"pms","type":"leases","status":"rebuilding","totalRecords":46,"processedRecords":0,"progressPercentage":0.0,"startTime":"2026-07-13T11:08:20.677Z","elapsedMs":0}

data: {"namespace":"pms","type":"leases","status":"completed","totalRecords":46,"processedRecords":46,"progressPercentage":100.0,"startTime":"2026-07-13T11:08:20.677Z","endTime":"2026-07-13T11:08:23.689Z","elapsedMs":3011}

event: done
data: {}
POST/api/indexes/{namespace}/{type}/consolidateBearer SOAR + Serverless

Flushes and fsyncs the indexes for one type to disk. Synchronous — the call returns only once every dirty page is durable. Use it before snapshotting the data directory or before a graceful shutdown so no index writes are lost.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. properties

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/indexes/pms/properties/consolidate' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "message": "Index consolidation completed for pms.properties - all data synced to disk"
}
POST/api/indexes/consolidate-allBearer SOAR + Serverless

Flushes and fsyncs every user-namespace type to disk in one call — it walks list_namespaces() → list_types(), skipping internal _* stores, and syncs each. Returns 200 when all types sync cleanly, or 206 if some fail, with a message that summarizes the count.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/indexes/consolidate-all' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "message": "Consolidated 20 type stores - all data synced to disk"
}

Response 206

{
  "ok": false,
  "message": "Consolidated 18 of 20 type stores - 2 failed to sync",
  "error": "pms.transactions: disk I/O error"
}
DELETE/api/indexes/{namespace}/{type}Bearer SOAR + Serverless

Disabled by design — this endpoint is a no-op. It returns 200 with an explanatory message instead of deleting anything. The Rust engine rebuilds indexes incrementally from the source data, so an API-driven delete would leave a window of empty indexes with no safety net. To refresh indexes, use GET /api/indexes/:ns/:type/rebuild (or the POST /reindex variant) instead.

Path & query parameters

NameInTypeDescription
namespacepathstringe.g. pms
typepathstringe.g. properties

Request

curl -s -X DELETE 'https://acme.sandbox.inventdb.com/api/indexes/pms/properties' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "message": "Index deletion not recommended. Type pms.properties has 29 property indexes. Use /rebuild endpoint to refresh indexes instead."
}
POST/api/indexingBearer SOAR + Serverless

Globally enables or disables property indexing. When disabled, every write skips index maintenance — the standard trick for speeding up large bulk loads. Afterwards, re-enable with this endpoint and then call POST /api/indexes/:ns/:type/consolidate (or run a full rebuild) so the indexes catch up with the loaded rows.

Path & query parameters

NameInTypeDescription
enabledquerybooleantrue to re-enable indexing, false to pause it. Required.

Request

# pause indexing before a bulk load
curl -s -X POST 'https://acme.sandbox.inventdb.com/api/indexing?enabled=false' \
  -H 'Authorization: Bearer $TOKEN'

# ...bulk-load pms.properties, then re-enable...
curl -s -X POST 'https://acme.sandbox.inventdb.com/api/indexing?enabled=true' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "indexingEnabled": true
}
POST/api/indexing/waitBearer SOAR + Serverless

Blocks until all pending index work has drained, then returns 200. Handy in scripted workflows that bulk-load a type (e.g. pms.transactions) and then immediately query it by an indexed column — call this first to guarantee the indexes are caught up before the read.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/indexing/wait' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "message": "All indexing operations completed"
}
POST/api/indexes/clear-historyBearer SOAR + Serverless

Clears the rebuild-progress history table, resetting the "Last rebuild" timeline shown in the admin console after maintenance. This is bookkeeping only — it has no effect on live data or on the indexes themselves.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/indexes/clear-history' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "message": "Rebuild history cleared"
}

Relationships

The foreign-key graph for a namespace. Each edge is a property-to-property link between two types (e.g. pms.leases.property_id → pms.properties.property_id) carrying a cardinality hint and an enforce flag for referential integrity. Read the graph, replace it (admin only), drop it, auto-detect candidate edges from column-name heuristics plus data sampling, or fetch the per-type property schema that drives the Console's relationship designer.

GET/api/relationships/{namespace}Bearer SOAR + Serverless

Returns the full relationship graph for a namespace as a single graph record. Each entry in edges links a source property to a target property, with a cardinality hint (many-to-one, one-to-many, …), an enforce flag for referential integrity, and an optional human label. Use it to render join diagrams or to seed an edit before a PUT. An empty graph (edges: []) is returned when nothing is configured yet.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace whose graph to read, e.g. pms.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/api/relationships/pms' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "_id": "graph",
  "edges": [
    {
      "sourceType": "leases",
      "sourceProperty": "property_id",
      "targetType": "properties",
      "targetProperty": "property_id",
      "cardinality": "many-to-one",
      "enforce": false,
      "label": "belongs to"
    },
    {
      "sourceType": "leases",
      "sourceProperty": "tenant_id",
      "targetType": "tenants",
      "targetProperty": "tenant_id",
      "cardinality": "many-to-one",
      "enforce": false,
      "label": "belongs to"
    },
    {
      "sourceType": "properties",
      "sourceProperty": "owner_id",
      "targetType": "owners",
      "targetProperty": "owner_id",
      "cardinality": "many-to-one",
      "enforce": false,
      "label": "belongs to"
    }
  ],
  "layout": { "ai_detected_at": "2026-07-11T12:46:55.161Z" },
  "_createdAt": "2026-07-11T12:46:55.185234300+00:00",
  "_updatedAt": "2026-07-11T12:46:55.185234300+00:00",
  "_createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed"
}
PUT/api/relationships/{namespace}Bearer SOAR + Serverless

Replaces the relationship graph for a namespace. Admin / superadmin only — the graph defines the data model itself (which types reference which, and on what columns), so a plain write grant is not enough. PUT semantics are wholesale: the supplied edges array fully replaces what was there, so GET first, edit the array, then send it back. The optional layout blob (node positions, zoom) is round-tripped verbatim.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace whose graph to overwrite, e.g. pms.

Request

curl -s -X PUT 'https://acme.sandbox.inventdb.com/api/relationships/pms' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{
    "edges": [
      {
        "sourceType": "leases",
        "sourceProperty": "property_id",
        "targetType": "properties",
        "targetProperty": "property_id",
        "cardinality": "many-to-one",
        "enforce": false,
        "label": "belongs to"
      },
      {
        "sourceType": "leases",
        "sourceProperty": "tenant_id",
        "targetType": "tenants",
        "targetProperty": "tenant_id",
        "cardinality": "many-to-one",
        "enforce": false,
        "label": "belongs to"
      }
    ],
    "layout": { "ai_detected_at": "2026-07-11T12:46:55.161Z" }
  }'
{
  "edges": [
    {
      "sourceType": "leases",
      "sourceProperty": "property_id",
      "targetType": "properties",
      "targetProperty": "property_id",
      "cardinality": "many-to-one",
      "enforce": false,
      "label": "belongs to"
    },
    {
      "sourceType": "leases",
      "sourceProperty": "tenant_id",
      "targetType": "tenants",
      "targetProperty": "tenant_id",
      "cardinality": "many-to-one",
      "enforce": false,
      "label": "belongs to"
    }
  ],
  "layout": { "ai_detected_at": "2026-07-11T12:46:55.161Z" }
}

Response 200

{ "ok": true }
DELETE/api/relationships/{namespace}Bearer SOAR + Serverless

Drops the relationship graph for a namespace. Requires admin permission on the namespace. The underlying edge records and the saved layout are removed, but the namespace's actual data (properties, leases, tenants, …) is left completely untouched — only the FK metadata goes away.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace whose graph to delete, e.g. pms.

Request

curl -s -X DELETE 'https://acme.sandbox.inventdb.com/api/relationships/pms' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{ "ok": true }
POST/api/relationships/{namespace}/detectBearer SOAR + Serverless

Auto-detects candidate relationships by combining column-name heuristics (*_id columns map to a matching type), data sampling (checks that the foreign-key values actually resolve in the target type), and an optional LLM assist for naming-mismatch links. Pass an optional types array to restrict the scan; omit it to scan every type in the namespace. The response is suggestions only — nothing is persisted. The user reviews them in the designer and PUTs the final set back via save_relationships.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace to scan, e.g. pms.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/relationships/pms/detect' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "types": ["leases", "tenants", "properties", "owners"] }'
{ "types": ["leases", "tenants", "properties", "owners"] }

Response 200

{
  "suggestions": [
    {
      "sourceType": "leases",
      "sourceProperty": "property_id",
      "targetType": "properties",
      "targetProperty": "_id",
      "cardinality": "many-to-one",
      "enforce": false,
      "confidence": 0.4,
      "method": "heuristic",
      "reason": "Property 'property_id' matches type 'properties'",
      "dataMatchRatio": 0.0,
      "sampleSize": 5,
      "matchCount": 0
    },
    {
      "sourceType": "leases",
      "sourceProperty": "tenant_id",
      "targetType": "tenants",
      "targetProperty": "_id",
      "cardinality": "many-to-one",
      "enforce": false,
      "confidence": 0.4,
      "method": "heuristic",
      "reason": "Property 'tenant_id' matches type 'tenants'",
      "dataMatchRatio": 0.0,
      "sampleSize": 5,
      "matchCount": 0
    },
    {
      "sourceType": "properties",
      "sourceProperty": "owner_id",
      "targetType": "owners",
      "targetProperty": "_id",
      "cardinality": "many-to-one",
      "enforce": false,
      "confidence": 0.4,
      "method": "heuristic",
      "reason": "Property 'owner_id' matches type 'owners'",
      "dataMatchRatio": 0.0,
      "sampleSize": 5,
      "matchCount": 0
    }
  ],
  "typesAnalyzed": 14
}
GET/api/relationships/{namespace}/types-schemaBearer SOAR + Serverless

Returns every non-system type in a namespace together with its recordCount and known properties[]. This drives the InventDB Console's relationship designer so the user can pick valid source and target columns (e.g. leases.property_idproperties.property_id). Property lists reflect what the schemaless engine has seen so far, so newer columns appear automatically as rows are written.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace whose type schema to list, e.g. pms.

Request

curl -s -X GET 'https://acme.sandbox.inventdb.com/api/relationships/pms/types-schema' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "types": [
    {
      "name": "leases",
      "recordCount": 46,
      "properties": [
        { "name": "_id" },
        { "name": "lease_id" },
        { "name": "property_id" },
        { "name": "tenant_id" },
        { "name": "tenant_name" },
        { "name": "market_rent" },
        { "name": "contract_rent" },
        { "name": "deposit_held" },
        { "name": "lease_start" },
        { "name": "lease_end" },
        { "name": "status" }
      ]
    },
    {
      "name": "properties",
      "recordCount": 50,
      "properties": [
        { "name": "_id" },
        { "name": "property_id" },
        { "name": "owner_id" },
        { "name": "street" },
        { "name": "city" },
        { "name": "state" },
        { "name": "market_rent" },
        { "name": "market_value" },
        { "name": "status" }
      ]
    },
    {
      "name": "owners",
      "recordCount": 10,
      "properties": [
        { "name": "_id" },
        { "name": "owner_id" },
        { "name": "name" },
        { "name": "type" },
        { "name": "phone" },
        { "name": "email" },
        { "name": "mgmt_fee" },
        { "name": "payout_method" }
      ]
    },
    {
      "name": "transactions",
      "recordCount": 2261,
      "properties": [
        { "name": "_id" },
        { "name": "date" },
        { "name": "type" },
        { "name": "property_id" },
        { "name": "owner_id" },
        { "name": "account_category" },
        { "name": "amount" },
        { "name": "method" }
      ]
    }
  ]
}

Workflows

Event-driven workflows engine — workflow CRUD, the runs timeline, manual fire, run export, and lifecycle toggles (pause / resume / activate). A workflow pairs a trigger (cron, gmail, webhook, manual, …) with an ordered plan of steps (sql_query, render_report, llm_extract, send_email, notify_user, create_calendar_event, finish, …). Every workflow is owned by the caller and scoped by RLS. All endpoints require a Bearer token.

POST/api/workflowsBearer SOAR + Serverless

Create a new event-driven workflow owned by the caller. The body carries the display name, the trigger_kind + kind-specific trigger_spec, a short natural-language trigger_intent, and the ordered plan array of steps. New workflows land in pending_approval=true / active=false — call /activate (or the pause/resume toggles) to let the dispatcher fire them. sandbox defaults to true, so a fresh workflow never produces externally-visible side effects until you deliberately turn sandbox off.

Request body fields

NameInTypeDescription
namebodystringDisplay name (non-empty).
trigger_kindbodystringcron | gmail | webhook | manual | …
trigger_specbodyobjectKind-specific config, e.g. { "expr": "0 9 * * *", "tz": "Asia/Calcutta" } for cron.
trigger_intentbodystringShort natural-language description of what should happen.
planbodyarrayOrdered WorkflowStep objects. Each needs idx, label, narration, kind and kind-specific fields (a notify_user step needs title + body; a sql_query step needs sql).
tools_allowedbodyarrayAllowlist for LLM tool calls inside the run (empty = none).
budgetbodyobjectOptional ceiling, e.g. { "max_tool_calls": 25, "max_input_tokens": 50000 }.
sandboxbodybooleanDefaults true; when true the run makes no externally-visible side effects (emails go to inbox staging only).

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/workflows' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "name": "Overdue Rent Alert",
  "trigger_kind": "cron",
  "trigger_spec": { "expr": "0 8 * * *", "tz": "Asia/Calcutta" },
  "trigger_intent": "Every morning, find rent transactions and notify the operator.",
  "plan": [
    { "idx": 1, "label": "Find rent activity",
      "narration": "Pulls recent rent income rows for the portfolio.",
      "kind": "sql_query", "save_as": "rent",
      "sql": "SELECT property_id, amount FROM pms.transactions WHERE type = '\''Income'\'' AND account_category = '\''Rent'\'' LIMIT 50" },
    { "idx": 2, "label": "Notify operator",
      "narration": "Posts an in-app notification with the rent summary.",
      "kind": "notify_user", "title": "Rent summary ready",
      "body": "Rent activity summary is ready to review." }
  ],
  "tools_allowed": [],
  "sandbox": true
}'
{
  "name": "Overdue Rent Alert",
  "trigger_kind": "cron",
  "trigger_spec": { "expr": "0 8 * * *", "tz": "Asia/Calcutta" },
  "trigger_intent": "Every morning, find rent transactions and notify the operator.",
  "plan": [
    {
      "idx": 1,
      "label": "Find rent activity",
      "narration": "Pulls recent rent income rows for the portfolio.",
      "kind": "sql_query",
      "save_as": "rent",
      "sql": "SELECT property_id, amount FROM pms.transactions WHERE type = 'Income' AND account_category = 'Rent' LIMIT 50"
    },
    {
      "idx": 2,
      "label": "Notify operator",
      "narration": "Posts an in-app notification with the rent summary.",
      "kind": "notify_user",
      "title": "Rent summary ready",
      "body": "Rent activity summary is ready to review."
    }
  ],
  "tools_allowed": [],
  "sandbox": true
}

Response 200

{
  "ok": true,
  "data": {
    "_id": "1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2",
    "name": "Overdue Rent Alert",
    "owner_user_id": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
    "trigger_kind": "cron",
    "trigger_spec": { "expr": "0 8 * * *", "tz": "Asia/Calcutta" },
    "trigger_intent": "Every morning, find rent transactions and notify the operator.",
    "plan": [
      { "idx": 1, "label": "Find rent activity", "narration": "Pulls recent rent income rows for the portfolio.", "kind": "sql_query", "save_as": "rent", "sql": "SELECT property_id, amount FROM pms.transactions WHERE type = 'Income' AND account_category = 'Rent' LIMIT 50" },
      { "idx": 2, "label": "Notify operator", "narration": "Posts an in-app notification with the rent summary.", "kind": "notify_user", "title": "Rent summary ready", "body": "Rent activity summary is ready to review." }
    ],
    "pending_approval": true,
    "tools_allowed": [],
    "budget": { "max_tool_calls": 25, "max_input_tokens": 50000, "max_runtime_secs": 1800, "max_parked_secs": 1209600 },
    "sandbox": true,
    "active": false,
    "version": 1,
    "created_at": "2026-07-13T11:12:24.314637900+00:00",
    "updated_at": "2026-07-13T11:12:24.314637900+00:00",
    "next_run_at": "2026-07-14T02:30:00+00:00",
    "_createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed"
  }
}
GET/api/workflowsBearer SOAR + Serverless

List every workflow owned by the caller, newest activity first. Each row includes the trigger kind, natural-language intent, lifecycle flags (active, pending_approval), and dispatch timestamps. Pass ?include_deleted=true to also surface soft-deleted rows for audit or undo flows.

Path & query parameters

NameInTypeDescription
include_deletedquerybooleanInclude soft-deleted workflows (default false).

Request

curl -s 'https://acme.sandbox.inventdb.com/api/workflows?include_deleted=false' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "workflows": [
      {
        "_id": "3d754a89-3b42-462f-969e-80b7ece7d880",
        "name": "Daily Owner Statements — 9 AM email to each owner",
        "trigger_kind": "cron",
        "trigger_intent": "Every morning at 9:00 AM (Asia/Calcutta), render the \"Owner Statement\" report for each owner and email each owner their own statement.",
        "active": true,
        "pending_approval": false,
        "deleted_at": null,
        "created_at": "2026-07-11T12:40:47.156715100+00:00",
        "updated_at": "2026-07-13T03:30:18.793315+00:00",
        "last_dispatched_at": "2026-07-13T03:30:18.793281500+00:00"
      },
      {
        "_id": "1d315f38-db91-4daf-8055-02b56b92de14",
        "name": "Lease Renewal Reminder (60-day, with approval)",
        "trigger_kind": "cron",
        "trigger_intent": "Every morning, find active leases expiring within 60 days, draft a renewal-reminder email per tenant, and park for approval.",
        "active": false,
        "pending_approval": true,
        "deleted_at": null,
        "created_at": "2026-07-09T15:17:35.036894400+00:00",
        "updated_at": "2026-07-09T15:17:35.036894400+00:00",
        "last_dispatched_at": null
      }
    ]
  }
}
GET/api/workflows/{id}Bearer SOAR + Serverless

Read a single workflow's full definition — the complete plan, trigger_spec, budget, lifecycle flags, and the natural-language message_context that built it. Use this to inspect or clone a workflow before editing it with PUT.

Path & query parameters

NameInTypeDescription
idpathstringWorkflow _id (uuid).

Request

curl -s 'https://acme.sandbox.inventdb.com/api/workflows/138a46dd-6ba9-4505-8ae5-ba9b4c4d09d1' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "_id": "138a46dd-6ba9-4505-8ae5-ba9b4c4d09d1",
    "name": "Daily Owner Portfolio Summary email",
    "owner_user_id": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
    "trigger_kind": "cron",
    "trigger_spec": { "expr": "0 9 * * *", "tz": "Asia/Calcutta" },
    "trigger_intent": "Every morning at 9:00 AM (Asia/Calcutta), render the \"Owner Portfolio Summary\" report for owner O-1002 and email it.",
    "plan": [
      { "idx": 1, "label": "Render Owner Portfolio Summary", "narration": "Renders the saved report template live against pms data for owner O-1002.", "save_as": "report", "kind": "render_report", "template_id": "tpl_095c0b7a07a2", "params": { "owner_id": "O-1002" } },
      { "idx": 2, "label": "Email the report to me", "narration": "Emails the rendered portfolio summary HTML as the morning digest.", "kind": "send_email", "to": "developer.3@inventdb.com", "subject": "Owner Portfolio Summary — ${today}", "body": "${report.html}" },
      { "idx": 3, "label": "Done", "narration": "Marks the run complete once the email has been dispatched.", "kind": "finish", "summary": "Rendered the report and emailed it." }
    ],
    "pending_approval": false,
    "tools_allowed": [],
    "budget": { "max_tool_calls": 25, "max_input_tokens": 50000, "max_runtime_secs": 1800, "max_parked_secs": 1209600 },
    "sandbox": false,
    "active": true,
    "version": 1,
    "created_at": "2026-07-11T06:35:02.843768600+00:00",
    "updated_at": "2026-07-13T03:30:18.681641700+00:00",
    "last_dispatched_at": "2026-07-13T03:30:18.664288600+00:00",
    "next_run_at": "2026-07-14T03:30:00+00:00",
    "_createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed"
  }
}
PUT/api/workflows/{id}Bearer SOAR + Serverless

Update a workflow's metadata or plan. All fields are optional — send only what changed. When plan is included it is re-validated and any errors come back as 400 with an issues array describing each problem; non-plan edits (like a rename) skip validation to avoid unnecessary SQL probes. Each successful update bumps version.

Path & query parameters

NameInTypeDescription
idpathstringWorkflow _id (uuid).
namebodystringOptional new display name.
trigger_specbodyobjectOptional new trigger config.
planbodyarrayOptional new step plan (re-validated).

Request

curl -s -X PUT 'https://acme.sandbox.inventdb.com/api/workflows/1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "name": "Overdue Rent Alert (renamed)" }'
{ "name": "Overdue Rent Alert (renamed)" }

Response 200

{
  "ok": true,
  "data": {
    "_id": "1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2",
    "name": "Overdue Rent Alert (renamed)",
    "owner_user_id": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
    "trigger_kind": "cron",
    "trigger_spec": { "expr": "0 8 * * *", "tz": "Asia/Calcutta" },
    "trigger_intent": "Every morning, find rent transactions and notify the operator.",
    "pending_approval": false,
    "sandbox": true,
    "active": true,
    "version": 2,
    "updated_at": "2026-07-13T11:13:02.128849400+00:00"
  }
}
DELETE/api/workflows/{id}Bearer SOAR + Serverless

Soft-delete a workflow. Past runs and their event history are preserved (they still show up under /runs and export), and the row can be surfaced again with ?include_deleted=true — but the workflow stops firing on new events. Use this instead of a hard delete so audit trails survive.

Path & query parameters

NameInTypeDescription
idpathstringWorkflow _id (uuid).

Request

curl -s -X DELETE 'https://acme.sandbox.inventdb.com/api/workflows/1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": { "deleted": "1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2" }
}
POST/api/workflows/{id}/activateBearer SOAR + Serverless

Activate a workflow — clears pending_approval and sets active=true. Idempotent for already-active rows. The optional body lets the AI canvas persist a sandbox value in the same click that approves the workflow; omit the body entirely to leave the persisted sandbox flag untouched.

Path & query parameters

NameInTypeDescription
idpathstringWorkflow _id (uuid).
sandboxbodyboolean | nullOptional. When set, overrides the persisted sandbox flag before flipping active on. Omit to preserve the current value.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/workflows/1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2/activate' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "sandbox": true }'
{ "sandbox": true }

Response 200

{
  "ok": true,
  "data": {
    "_id": "1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2",
    "name": "Overdue Rent Alert",
    "trigger_kind": "cron",
    "trigger_spec": { "expr": "0 8 * * *", "tz": "Asia/Calcutta" },
    "pending_approval": false,
    "active": true,
    "sandbox": true,
    "version": 1,
    "updated_at": "2026-07-13T11:12:41.902117300+00:00"
  }
}
POST/api/workflows/{id}/pauseBearer SOAR + Serverless

Pause a workflow — sets active=false. New trigger events queue but are not dispatched until you resume; any in-flight runs keep going to completion. No request body.

Path & query parameters

NameInTypeDescription
idpathstringWorkflow _id (uuid).

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/workflows/1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2/pause' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "_id": "1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2",
    "name": "Overdue Rent Alert",
    "active": false,
    "pending_approval": false,
    "version": 1,
    "updated_at": "2026-07-13T11:12:49.551030700+00:00"
  }
}
POST/api/workflows/{id}/resumeBearer SOAR + Serverless

Resume a paused workflow — sets active=true again. Backlog events that arrived while it was paused are not replayed; they are marked with an ignored_reason. Only events that fire after resume will dispatch. No request body.

Path & query parameters

NameInTypeDescription
idpathstringWorkflow _id (uuid).

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/workflows/1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2/resume' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "_id": "1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2",
    "name": "Overdue Rent Alert",
    "active": true,
    "pending_approval": false,
    "version": 1,
    "updated_at": "2026-07-13T11:12:56.207744100+00:00"
  }
}
POST/api/workflows/{id}/runBearer SOAR + Serverless

Manually fire a workflow once. This appends a synthetic manual event carrying the supplied sample_payload; the router picks it up on its next tick and creates a run. Set sandbox_override: true to force this single run to be non-side-effecting even if the workflow itself has sandbox off — this is exactly how the canvas "Test" button works.

Path & query parameters

NameInTypeDescription
idpathstringWorkflow _id (uuid).
sample_payloadbodyobjectOptional payload injected as the synthetic manual event's data.
sandbox_overridebodybooleanOptional. Force this run to be non-side-effecting regardless of the workflow's own sandbox flag.

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/api/workflows/1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2/run' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "sample_payload": { "owner_id": "O-1002" }, "sandbox_override": true }'
{
  "sample_payload": { "owner_id": "O-1002" },
  "sandbox_override": true
}

Response 200

{
  "ok": true,
  "data": {
    "event_id": "cc5097c6-269c-4b59-8f9d-3766f8e3d2d8",
    "workflow_id": "1f34ff78-55a0-40a8-9a55-3fb62d7e4bd2",
    "status": "pending",
    "note": "Event queued. Dispatch happens once the router picks it up on its next tick."
  }
}
GET/api/workflows/{id}/runsBearer SOAR + Serverless

List all runs of one workflow, newest first. Each entry is a run summary — status, start/end timestamps, sandbox flag, and any top-level error — without the per-step timeline. Fetch a single run's /runs/{id} to drill into its steps.

Path & query parameters

NameInTypeDescription
idpathstringWorkflow _id (uuid).

Request

curl -s 'https://acme.sandbox.inventdb.com/api/workflows/3d754a89-3b42-462f-969e-80b7ece7d880/runs' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "runs": [
      {
        "_id": "3330bdf7-8891-466b-85b4-de5fa588140e",
        "workflow_id": "3d754a89-3b42-462f-969e-80b7ece7d880",
        "status": "succeeded",
        "started_at": "2026-07-13T03:30:19.370381+00:00",
        "ended_at": "2026-07-13T03:30:26.709449900+00:00",
        "last_active_at": "2026-07-13T03:30:26.709449900+00:00",
        "error": null,
        "sandbox": false,
        "trigger_event_id": "d3c83317-8bda-4ec8-bc6d-1856325941ad"
      }
    ]
  }
}
GET/api/workflows/runsBearer SOAR + Serverless

List every run across all workflows the caller owns, newest first. This is the global runs feed that powers the "recent activity" dashboard — one flat array of run summaries spanning all of your workflows.

Request

curl -s 'https://acme.sandbox.inventdb.com/api/workflows/runs' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "runs": [
      {
        "_id": "3330bdf7-8891-466b-85b4-de5fa588140e",
        "workflow_id": "3d754a89-3b42-462f-969e-80b7ece7d880",
        "status": "succeeded",
        "started_at": "2026-07-13T03:30:19.370381+00:00",
        "ended_at": "2026-07-13T03:30:26.709449900+00:00",
        "last_active_at": "2026-07-13T03:30:26.709449900+00:00",
        "error": null,
        "sandbox": false
      },
      {
        "_id": "a1f0c2d4-7e6b-4c33-9a12-55b0e9f4d201",
        "workflow_id": "138a46dd-6ba9-4505-8ae5-ba9b4c4d09d1",
        "status": "succeeded",
        "started_at": "2026-07-13T03:30:18.700000+00:00",
        "ended_at": "2026-07-13T03:30:23.410000+00:00",
        "last_active_at": "2026-07-13T03:30:23.410000+00:00",
        "error": null,
        "sandbox": false
      }
    ]
  }
}
GET/api/workflows/runs/{id}Bearer SOAR + Serverless

Read a single run together with its full step timeline. Returns { run, steps } where steps is ordered by idx and includes each step's role (model_in, tool_call, tool_result), the tool_name, its tool_args, the tool_result, and timing. Use it to inspect exactly what a workflow did on a given run.

Path & query parameters

NameInTypeDescription
idpathstringRun _id (uuid).

Request

curl -s 'https://acme.sandbox.inventdb.com/api/workflows/runs/3330bdf7-8891-466b-85b4-de5fa588140e' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "run": {
      "_id": "3330bdf7-8891-466b-85b4-de5fa588140e",
      "workflow_id": "3d754a89-3b42-462f-969e-80b7ece7d880",
      "trigger_event_id": "d3c83317-8bda-4ec8-bc6d-1856325941ad",
      "principal": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
      "status": "succeeded",
      "started_at": "2026-07-13T03:30:19.370381+00:00",
      "ended_at": "2026-07-13T03:30:26.709449900+00:00",
      "tool_calls_used": 0,
      "input_tokens_used": 0,
      "output_tokens_used": 0,
      "cost_used_usd": 0.0,
      "sandbox": false,
      "depth": 0
    },
    "steps": [
      {
        "_id": "4de6ddae-3850-40dd-bc34-25a0beddd118",
        "run_id": "3330bdf7-8891-466b-85b4-de5fa588140e",
        "idx": 0,
        "role": "model_in",
        "content": "Workflow 'Daily Owner Statements — 9 AM email to each owner' triggered.",
        "tool_name": null,
        "tool_args": null,
        "tool_result": null,
        "created_at": "2026-07-13T03:30:19.379397900+00:00"
      },
      {
        "_id": "82308f0c-b63d-44cf-9d62-3d94ee5220b4",
        "run_id": "3330bdf7-8891-466b-85b4-de5fa588140e",
        "idx": 1,
        "role": "tool_call",
        "content": "[Fetch all owners] Looks up every owner so a statement can be emailed to each one.",
        "tool_name": "sql_query",
        "tool_args": { "kind": "sql_query", "sql": "SELECT owner_id, email, name FROM pms.owners" },
        "tool_result": null,
        "created_at": "2026-07-13T03:30:19.444932100+00:00"
      },
      {
        "_id": "2805b7d7-3546-427f-bf7c-ba6bf75bcdcd",
        "run_id": "3330bdf7-8891-466b-85b4-de5fa588140e",
        "idx": 1,
        "role": "tool_result",
        "content": "",
        "tool_name": "sql_query",
        "tool_result": [
          { "owner_id": "O-1002", "email": "vikram.sundara75@team758135.testinator.email", "name": "Sundara Holdings LLC" },
          { "owner_id": "O-1004", "email": "robert.whitfield97@team758135.testinator.email", "name": "Robert & Susan Whitfield" }
        ],
        "created_at": "2026-07-13T03:30:19.494639400+00:00"
      }
    ]
  }
}
GET/api/workflows/runs/{id}/exportBearer SOAR + Serverless

Dump a run, its steps, the original trigger event, and any notifications it produced — a single self-contained bundle for audit, debugging, or support hand-off. The run embeds a workflow_snapshot (the exact plan as it ran), and the versioned schema field (e.g. inventdb.workflow.run.v1) is the stable contract for consumers of the export.

Path & query parameters

NameInTypeDescription
idpathstringRun _id (uuid).

Request

curl -s 'https://acme.sandbox.inventdb.com/api/workflows/runs/3330bdf7-8891-466b-85b4-de5fa588140e/export' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "schema": "inventdb.workflow.run.v1",
    "exported_at": "2026-07-13T11:10:59.610687100+00:00",
    "run": {
      "_id": "3330bdf7-8891-466b-85b4-de5fa588140e",
      "workflow_id": "3d754a89-3b42-462f-969e-80b7ece7d880",
      "trigger_event_id": "d3c83317-8bda-4ec8-bc6d-1856325941ad",
      "principal": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
      "workflow_snapshot": {
        "_id": "3d754a89-3b42-462f-969e-80b7ece7d880",
        "name": "Daily Owner Statements — 9 AM email to each owner",
        "trigger_kind": "cron",
        "trigger_spec": { "expr": "0 9 * * *", "tz": "Asia/Calcutta" },
        "plan": [
          { "idx": 1, "label": "Fetch all owners", "save_as": "owners", "kind": "sql_query", "sql": "SELECT owner_id, email, name FROM pms.owners" }
        ],
        "sandbox": false,
        "active": true,
        "version": 2
      },
      "status": "succeeded",
      "started_at": "2026-07-13T03:30:19.370381+00:00",
      "ended_at": "2026-07-13T03:30:26.709449900+00:00"
    },
    "steps": [
      {
        "_id": "4de6ddae-3850-40dd-bc34-25a0beddd118",
        "idx": 0,
        "role": "model_in",
        "content": "Workflow 'Daily Owner Statements — 9 AM email to each owner' triggered.",
        "created_at": "2026-07-13T03:30:19.379397900+00:00"
      }
    ],
    "trigger_event": {
      "_id": "d3c83317-8bda-4ec8-bc6d-1856325941ad",
      "source": "cron",
      "occurred_at": "2026-07-13T03:30:18.793281500+00:00",
      "payload": {
        "workflow_id": "3d754a89-3b42-462f-969e-80b7ece7d880",
        "fired_at": "2026-07-13T03:30:18.793281500+00:00",
        "trigger_intent": "Every morning at 9:00 AM (Asia/Calcutta), render the \"Owner Statement\" report for each owner and email it."
      },
      "correlation_keys": { "workflow_id": "3d754a89-3b42-462f-969e-80b7ece7d880" },
      "status": "dispatched",
      "dispatched_to": [
        { "workflow_id": "3d754a89-3b42-462f-969e-80b7ece7d880", "run_id": "3330bdf7-8891-466b-85b4-de5fa588140e" }
      ],
      "dispatched_at": "2026-07-13T03:30:19.375385200+00:00",
      "user_id": "a9b4e239-27ff-4f38-949b-38b332b3f8ed"
    },
    "notifications": []
  }
}

Attachments

Per-record file attachments: upload (multipart), list, fetch metadata, raw download, delete, and full-text / semantic / combined search over extracted text and embeddings. Storage is native — chunks, extracted text, and embeddings live in the _attachment* system types (_attachments, _attachment_text, _attachment_embeddings). Every attachment is owned by an existing record, addressed by that record's system _id.

POST/attach/{namespace}/{type_name}/{record_id}Bearer SOAR + Serverless

Upload a file attachment against an existing record. The request is a multipart form; the file part carries the raw bytes. On upload the engine stores chunks, extracts text (PDF / DOCX / TXT / images via OCR), and — if the embedding model is loaded — generates semantic embeddings so the file becomes content-searchable. Small text files finish inline; larger files return Pending and the background worker advances processing_state to Ready.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace, e.g. pms
type_namepathstringType name, e.g. properties
record_idpathstringOwning record's system _id (uuid), e.g. fe289741-574c-4311-9733-ad6be7e3ceb0

Request

Multipart form. Required part: file. Optional parts: description, tags (JSON array), custom_properties (JSON object), folder_path.

curl -s -X POST 'https://acme.sandbox.inventdb.com/attach/pms/properties/fe289741-574c-4311-9733-ad6be7e3ceb0' \
  -H 'Authorization: Bearer $TOKEN' \
  -F 'file=@/reports/P-5038-inspection-2026.pdf' \
  -F 'description=2026 annual inspection report' \
  -F 'tags=["inspection","2026"]' \
  -F 'folder_path=/inspections/2026'

Response 201

{
  "ok": true,
  "data": {
    "ok": true,
    "attachment_id": "att_3a422f35-4abd-41af-86e3-55b29ca8c474",
    "version": 1,
    "filename": "P-5038-inspection-2026.pdf",
    "content_type": "application/pdf",
    "size": 48213,
    "extraction_status": "Pending",
    "processing_state": "Pending",
    "file_metadata": {
      "page_count": 4,
      "author": "Blue Ridge Property Mgmt"
    }
  }
}
GET/attach/{namespace}/{type_name}/{record_id}Bearer SOAR + Serverless

List every attachment owned by a single record. Returns metadata, tags, folder_path, and per-file processing_state / extraction_status — but not the bytes. Use this to render an attachment explorer for one property, lease, or work order, then fetch bytes via the /download sub-route.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace, e.g. pms
type_namepathstringType name, e.g. properties
record_idpathstringOwning record's system _id

Request

curl -s 'https://acme.sandbox.inventdb.com/attach/pms/properties/fe289741-574c-4311-9733-ad6be7e3ceb0' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "attachments": [
    {
      "attachment_id": "att_3a422f35-4abd-41af-86e3-55b29ca8c474",
      "filename": "P-5038-inspection-2026.pdf",
      "content_type": "application/pdf",
      "size": 48213,
      "version": 1,
      "created_at": "2026-07-13T11:11:45.187886700+00:00",
      "updated_at": "2026-07-13T11:11:45.187886700+00:00",
      "description": "2026 annual inspection report",
      "folder_path": "/inspections/2026",
      "tags": ["inspection", "2026"],
      "record_id": "fe289741-574c-4311-9733-ad6be7e3ceb0",
      "processing_state": "Ready",
      "extraction_status": "Completed",
      "ai_cost_usd": 0.0
    }
  ],
  "total": 1
}
GET/attach/{namespace}/{type_name}/{record_id}/{attachment_id}Bearer SOAR + Serverless

Fetch a single attachment's metadata, tags, and extraction status. Bytes are not included — use the /download sub-route for the raw file. Handy for polling processing_state after a deferred upload until it reaches Ready.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace, e.g. pms
type_namepathstringType name, e.g. properties
record_idpathstringOwning record's system _id
attachment_idpathstringAttachment id, e.g. att_3a422f35-4abd-41af-86e3-55b29ca8c474

Request

curl -s 'https://acme.sandbox.inventdb.com/attach/pms/properties/fe289741-574c-4311-9733-ad6be7e3ceb0/att_3a422f35-4abd-41af-86e3-55b29ca8c474' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": {
    "attachment_id": "att_3a422f35-4abd-41af-86e3-55b29ca8c474",
    "filename": "P-5038-inspection-2026.pdf",
    "content_type": "application/pdf",
    "size": 48213,
    "version": 1,
    "created_at": "2026-07-13T11:11:45.187886700+00:00",
    "updated_at": "2026-07-13T11:11:45.187886700+00:00",
    "description": "2026 annual inspection report",
    "tags": ["inspection", "2026"],
    "record_id": "fe289741-574c-4311-9733-ad6be7e3ceb0",
    "processing_state": "Ready",
    "extraction_status": "Completed",
    "ai_cost_usd": 0.0
  }
}
GET/attach/{namespace}/{type_name}/{record_id}/{attachment_id}/downloadBearer SOAR + Serverless

Download the current version of an attachment as raw bytes. The response sets Content-Type to the stored MIME type and Content-Disposition: attachment so browsers trigger a save dialog. Returns 404 if the attachment does not exist.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace, e.g. pms
type_namepathstringType name, e.g. properties
record_idpathstringOwning record's system _id
attachment_idpathstringAttachment id

Request

curl -s -OJ 'https://acme.sandbox.inventdb.com/attach/pms/properties/fe289741-574c-4311-9733-ad6be7e3ceb0/att_3a422f35-4abd-41af-86e3-55b29ca8c474/download' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

HTTP/1.1 200 OK
content-type: application/pdf
content-disposition: attachment; filename="P-5038-inspection-2026.pdf"
content-length: 48213

<raw file bytes — application/octet-stream>
DELETE/attach/{namespace}/{type_name}/{record_id}/{attachment_id}Bearer SOAR + Serverless

Delete an attachment — all versions plus stored bytes, extracted text, and embeddings. This is a hard delete with no soft-delete or undo. After this call the file no longer appears in list or search results.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace, e.g. pms
type_namepathstringType name, e.g. properties
record_idpathstringOwning record's system _id
attachment_idpathstringAttachment id

Request

curl -s -X DELETE 'https://acme.sandbox.inventdb.com/attach/pms/properties/fe289741-574c-4311-9733-ad6be7e3ceb0/att_3a422f35-4abd-41af-86e3-55b29ca8c474' \
  -H 'Authorization: Bearer $TOKEN'

Response 200

{
  "ok": true,
  "data": null
}
POST/attach/{namespace}/{type_name}/searchBearer SOAR + Serverless

Search attachments across a type. search_type selects the mode: keyword (filename / description / tags via LIKE), fulltext (extracted text via LIKE), semantic (vector similarity via the MEANING() function — requires embeddings), or combined (default; weighted blend of all three). Optional filters, mime_types, tags, threshold, and per-mode weights narrow and rank results.

Path & query parameters

NameInTypeDescription
namespacepathstringNamespace, e.g. pms
type_namepathstringType name, e.g. properties

Request

curl -s -X POST 'https://acme.sandbox.inventdb.com/attach/pms/properties/search' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{
    "query": "faucet repair",
    "search_type": "combined",
    "limit": 5,
    "tags": ["inspection"],
    "mime_types": ["application/pdf", "text/plain"],
    "threshold": 0.25,
    "weights": { "keyword": 0.2, "fulltext": 0.3, "semantic": 0.5 },
    "filters": { "min_size": 100, "max_pages": 20 }
  }'
{
  "query": "faucet repair",
  "search_type": "combined",
  "limit": 5,
  "tags": ["inspection"],
  "mime_types": ["application/pdf", "text/plain"],
  "threshold": 0.25,
  "weights": { "keyword": 0.2, "fulltext": 0.3, "semantic": 0.5 },
  "filters": { "min_size": 100, "max_pages": 20 }
}

Response 200

{
  "ok": true,
  "results": [
    {
      "attachment_id": "att_3a422f35-4abd-41af-86e3-55b29ca8c474",
      "record_ns": "pms",
      "record_type": "properties",
      "record_id": "fe289741-574c-4311-9733-ad6be7e3ceb0",
      "filename": "P-5038-inspection-2026.pdf",
      "content_type": "application/pdf",
      "size": 48213,
      "version": 1,
      "created_at": "2026-07-13T11:11:45.187886700Z",
      "updated_at": "2026-07-13T11:11:45.187886700Z",
      "processing_state": "Ready",
      "extraction_status": "Ready",
      "description": "2026 annual inspection report",
      "tags": ["inspection", "2026"],
      "fulltext_score": 0.3,
      "semantic_score": 0.32360438,
      "combined_score": 0.6236044,
      "text_snippet": "Kitchen faucet leaking - work order WO-30055 opened for repair.",
      "chunk_matches": [],
      "file_metadata": { "page_count": 4 }
    }
  ],
  "total_matches": 1
}

Users (self-service)

Self-service user endpoints. Distinct from /api/users/* (admin) and /api/auth/* (auth state). The caller acts only on their own user row and only on the fields these endpoints expose — no role, activation, or password changes here.

PUT/api/me/emailBearer SOAR + Serverless

Updates the email address on the calling user's own row — nothing else. This exists separately from the admin PUT /api/users/{userId} because scheduled-report delivery fails when a regular user's _system.users.email is blank, yet a non-admin cannot use the admin endpoint. The settings page posts here from its one-click "Use my Gmail address" button (shown when Gmail OAuth is connected and the stored email is empty). Scope is strictly the caller's own email field — no role or password mutation.

Path & query parameters

None. The target user is always the authenticated caller (derived from the Bearer token); there is no namespace, id, or query parameter.

Request

curl -s -X PUT 'https://acme.sandbox.inventdb.com/api/me/email' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'content-type: application/json' \
  -d '{ "email": "june@inventdb.com" }'
{
  "email": "june@inventdb.com"
}

Response 200

{
  "ok": true,
  "user": {
    "_id": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
    "username": "june",
    "email": "june@inventdb.com",
    "role": "superadmin",
    "isActive": true,
    "createdAt": "2026-06-11T16:37:15.528274200+00:00",
    "updatedAt": "2026-07-13T11:13:56.297191200+00:00"
  }
}

Response 400 — empty or malformed email

{
  "ok": false,
  "error": "email is required"
}

Response 404 — user not found

{
  "ok": false,
  "error": "user not found"
}

Reports Mixed tier

Report-template CRUD, SQL-driven HTML rendering, and LLM-assisted template authoring under /api/report-templates.

POST/api/report-templatesBearer SOAR + Serverless

Persists a new SQL-driven HTML report template into _System.ReportTemplates. Body carries the complete html document (with <script type="server"> blocks), mode ("scheduled"|"interactive"), parameters, tags, and an optional per-template max_rows cap. version starts at 1 and createdBy is taken from the caller's JWT. Returns the stored document. No LLM involved.

Request

{
  "name": "Monthly Rent Roll",
  "description": "Active leases and market rent by property",
  "category": "Operational",
  "html": "<!DOCTYPE html><html><head><style>@page{size:A4;margin:14mm 12mm}</style></head><body><h1>Rent Roll</h1><table><thead><tr><th>Property</th><th>City</th><th>Market Rent</th></tr></thead><tbody><script type=\"server\">const r=query(\"SELECT property_id, city, market_rent FROM pms.properties WHERE status = :status\",{status:params.status});for(const row of r.rows){out.write('<tr><td>'+esc(row.property_id)+'</td><td>'+esc(row.city)+'</td><td>'+fmt.currency(Number(row.market_rent))+'</td></tr>');}</script></tbody></table></body></html>",
  "mode": "scheduled",
  "parameters": [
    {
      "name": "status",
      "type": "string",
      "label": "Status",
      "required": true,
      "default": "occupied"
    }
  ],
  "tags": [
    "rent-roll",
    "operational"
  ],
  "max_rows": 5000
}

Response 201

{
  "ok": true,
  "data": {
    "_id": "tpl_8f21ac",
    "name": "Monthly Rent Roll",
    "description": "Active leases and market rent by property",
    "category": "Operational",
    "html": "<!DOCTYPE html>...</html>",
    "mode": "scheduled",
    "parameters": [
      {
        "name": "status",
        "type": "string",
        "label": "Status",
        "required": true,
        "default": "occupied"
      }
    ],
    "version": 1,
    "createdBy": "user_101",
    "tags": [
      "rent-roll",
      "operational"
    ],
    "max_rows": 5000
  }
}
GET/api/report-templatesBearer SOAR + Serverless

Returns a lightweight list of saved templates (SELECT _id, name, description, category, mode, version, createdBy … LIMIT 1000) from _System.ReportTemplates. The heavy html body is NOT included — use GET /{id} to fetch a full template. No LLM involved.

Response 200

{
  "ok": true,
  "data": {
    "templates": [
      {
        "_id": "tpl_8f21ac",
        "name": "Monthly Rent Roll",
        "description": "Active leases and market rent by property",
        "category": "Operational",
        "mode": "scheduled",
        "version": 3,
        "createdBy": "user_101"
      },
      {
        "_id": "tpl_44d0e2",
        "name": "Owner Statement",
        "description": "Per-owner distribution statement",
        "category": "Finance",
        "mode": "scheduled",
        "version": 1,
        "createdBy": "user_101"
      }
    ]
  }
}
GET/api/report-templates/{id}Bearer SOAR + Serverless

Fetches the full stored template document by id, including the complete html body, parameters, tags, and version. Returns 404 when the id does not exist. No LLM involved.

Parameters

NameInTypeDescription
idpathstringTemplate id (e.g. tpl_8f21ac).

Response 200

{
  "ok": true,
  "data": {
    "_id": "tpl_8f21ac",
    "name": "Monthly Rent Roll",
    "description": "Active leases and market rent by property",
    "category": "Operational",
    "html": "<!DOCTYPE html>...</html>",
    "mode": "scheduled",
    "parameters": [
      {
        "name": "status",
        "type": "string",
        "label": "Status",
        "required": true
      }
    ],
    "version": 3,
    "createdBy": "user_101",
    "tags": [
      "rent-roll",
      "operational"
    ]
  }
}
PUT/api/report-templates/{id}Bearer SOAR + Serverless

Shallow-merges the JSON body into the stored template (any field except _id, _createdAt, createdBy which are protected) and auto-increments version. Typically used to save html/parameters/name/tags edits. Returns 404 when the id is unknown. No LLM involved.

Parameters

NameInTypeDescription
idpathstringTemplate id to update.

Request

{
  "name": "Monthly Rent Roll (v2)",
  "html": "<!DOCTYPE html>...updated...</html>",
  "tags": [
    "rent-roll",
    "finance"
  ]
}

Response 200

{
  "ok": true,
  "data": {
    "ok": true
  }
}
DELETE/api/report-templates/{id}Bearer SOAR + Serverless

Permanently removes the template from _System.ReportTemplates. Returns the deleted id on success. No LLM involved.

Parameters

NameInTypeDescription
idpathstringTemplate id to delete.

Response 200

{
  "ok": true,
  "data": {
    "deleted": "tpl_8f21ac"
  }
}
GET/api/report-templates/{id}/renderBearer SOAR + Serverless

Loads the template and renders it server-side through the boa-backed report engine under the caller's RLS principal. Parameters are supplied via query string and coerced to the declared parameter types; the template's max_rows override is honoured. Charts render as inline SVG (email_safe_charts=false) for in-browser viewing. Rendering executes the template's SQL only — it does NOT invoke the LLM, so it works on the Serverless tier.

Parameters

NameInTypeDescription
idpathstringTemplate id to render.
statusquerystringAny declared template parameter, e.g. ?status=occupied (parameter names vary per template).

Response 200

{
  "ok": true,
  "data": {
    "html": "<!DOCTYPE html>...rendered document with data...</html>",
    "meta": {
      "elapsed_ms": 42,
      "mode": "scheduled",
      "bytes": 18734
    }
  }
}
POST/api/report-templates/{id}/renderBearer SOAR + Serverless

POST variant of the render endpoint: accepts a typed/nested JSON params object instead of query-string params, avoiding URL-encoding. email_safe_charts defaults to true (email/bulk-email use); the in-app viewer sends false to get SVG charts. Executes the template SQL under the caller's RLS principal — no LLM, works on Serverless.

Parameters

NameInTypeDescription
idpathstringTemplate id to render.

Request

{
  "params": {
    "status": "occupied",
    "as_of_date": "2026-06-30"
  },
  "email_safe_charts": false
}

Response 200

{
  "ok": true,
  "data": {
    "html": "<!DOCTYPE html>...rendered document...</html>",
    "meta": {
      "elapsed_ms": 51,
      "mode": "scheduled",
      "bytes": 20418
    }
  }
}
POST/api/report-templates/render-ad-hocBearer SOAR + Serverless

Renders an arbitrary report-template html document supplied inline (not saved) through the same engine pipeline under the caller's RLS principal. Body is a RenderRequest: { html, params, mode?, limits?, email_safe_charts? }. Used by the AI Canvas / editor preview to render before saving. Returns 400 when html is empty, 422 when a server script crashes. Pure rendering — no LLM, works on Serverless.

Request

{
  "html": "<!DOCTYPE html><html><body><script type=\"server\">const r=query(\"SELECT COUNT(*) AS value FROM pms.properties\");out.write('Total properties: '+fmt.number(r.rows[0].value));</script></body></html>",
  "params": {},
  "mode": "scheduled",
  "email_safe_charts": false
}

Response 200

{
  "html": "<!DOCTYPE html><html><body>Total properties: 5,001</body></html>",
  "meta": {
    "elapsed_ms": 12,
    "mode": "scheduled",
    "bytes": 96
  }
}
POST/api/report-templates/test-queryBearer SOAR + Serverless

Editor's inline data inspector: substitutes :name params into the given SQL (string values quoted/escaped), expands CURRENT_DATE, appends a LIMIT (default 500) for non-aggregate queries, and executes it. Returns the rows, row count, and execution time. Returns 400 when sql is missing or the query errors. Straight SQL execution — no LLM, works on Serverless.

Request

{
  "sql": "SELECT property_id, city, state, market_rent, status FROM pms.properties WHERE status = :status",
  "params": {
    "status": "occupied"
  },
  "max_rows": 100
}

Response 200

{
  "ok": true,
  "data": {
    "rows": [
      {
        "property_id": "P-5001",
        "city": "Austin",
        "state": "TX",
        "market_rent": 2450,
        "status": "occupied"
      },
      {
        "property_id": "P-5002",
        "city": "Dallas",
        "state": "TX",
        "market_rent": 1980,
        "status": "occupied"
      }
    ],
    "count": 2,
    "executionMs": 7
  }
}
POST/api/report-templates/generateBearer SOAR only

Renders a report from either an inline html body or a saved template_id, merging params/parameters into the render. Listed among the AI report routes and gated on the AI tier: on a Serverless-tier instance the whole report-templates nest requires AI to be enabled, so this returns 403 with the "AI features are not available on the Serverless Database tier." error. Requires html OR template_id (400 otherwise). 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "template_id": "tpl_8f21ac",
  "parameters": {
    "status": "occupied"
  }
}

Response 200

{
  "html": "<!DOCTYPE html>...rendered document...</html>",
  "meta": {
    "elapsed_ms": 45,
    "mode": "scheduled",
    "bytes": 18902
  }
}
POST/api/report-templates/from-documentBearer SOAR only

Multipart upload (file + optional name/description). The built-in LLM (vision) reads the uploaded design (PDF/DOCX/XLSX/image) and authors a complete SQL-driven HTML report-template document bound to the customer's schema, returning { html, parameters, mode }. Invokes the built-in LLM — returns 403 on the Serverless Database tier. Also returns 400 when no file is sent, 422 on text-extract failure or non-JSON model output, 502 on LLM error. 403 on the Serverless tier (this route calls the built-in LLM).

Parameters

NameInTypeDescription
filequeryfile (multipart)The design document to recreate (PDF, DOCX, XLSX, or image). Multipart form field, not a query param.
namequerystringSuggested template name (multipart form field).
descriptionquerystringOptional description (multipart form field).

Response 200

{
  "ok": true,
  "data": {
    "html": "<!DOCTYPE html>...generated template...</html>",
    "parameters": [
      {
        "name": "as_of_date",
        "type": "date",
        "label": "As Of Date",
        "required": true
      }
    ],
    "mode": "scheduled"
  }
}
POST/api/report-templates/{id}/layout/streamBearer SOAR only

SSE endpoint. The user gives a natural-language layout instruction (optionally with pasted images/attachments); the built-in LLM returns a COMPLETE replacement html document, which is validate-rendered under RLS and, if it renders, persisted (version bumped, parameters auto-derived). Streams start/reasoning/html/saved/done (or error) events. Invokes the built-in LLM — returns 403 on the Serverless Database tier. 403 on the Serverless tier (this route calls the built-in LLM).

Parameters

NameInTypeDescription
idpathstringTemplate id being edited.

Request

{
  "instruction": "Add a bar chart of total market rent grouped by city, and use a teal accent.",
  "messages": [],
  "conversationMode": false,
  "modelFamily": "claude"
}

Response 200

event: start
data: {"template_id":"tpl_8f21ac","current_version":3,"instruction":"Add a bar chart..."}

event: reasoning
data: {"content":"Composing the report… (8s)"}

event: html
data: {"html":"<!DOCTYPE html>...</html>","bytes":21044}

event: saved
data: {"template_id":"tpl_8f21ac","version":4}

event: done
data: {}
POST/api/report-templates/promote-from-reportBearer SOAR + Serverless

Promotes a rendered ad-hoc report attachment (under _System.AIReports) into a saved template. Prefers the source-tagged sibling attachment (which keeps the live <script type="server"> bindings), auto-detects interactive vs scheduled mode from the html, auto-derives parameters from params.<name> accesses, inserts the new template, and returns { id, editorUrl }. Reads attachments + inserts a record — no LLM call, works on Serverless. Returns 404/400 when the source attachment is missing/empty/non-UTF-8.

Request

{
  "recordId": "rec_ai_9021",
  "attachmentId": "att_render_5567",
  "name": "Owner Statement (from canvas)"
}

Response 201

{
  "ok": true,
  "data": {
    "id": "tpl_new_77aa10",
    "editorUrl": "/report-templates?open=tpl_new_77aa10"
  }
}

AI Agent & Canvas Mixed tier

Natural-language AI agent: streaming analyst chat, NL-to-SQL, data/type/recipe generation, plus non-LLM helpers (model/config introspection, chat file staging, transactional change-set apply, and Analyze-thread persistence).

POST/ai/chat/streamBearer SOAR only

Runs the v3 tool-calling agent loop and streams progress as Server-Sent Events (each event data is a JSON AgentStep: {type, content, ...} plus tool results, SQL steps, widgets, forms, charts, and a final done). This is an alias for /ai/chat/v3/stream; the Console app calls it directly and the Tenantry proxy forwards to /ai/chat/v3/stream, so both converge on the same backend. AI config is read live from the encrypted DbSettings record; the per-message model_family (or the customer's configured default) resolves the provider/gateway path. Invokes the built-in LLM. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "messages": [
    {
      "role": "user",
      "content": "Which owners have the highest total market_value across their properties?"
    }
  ],
  "conversationMode": true,
  "modelFamily": "claude",
  "theme": "light",
  "timeZone": "America/New_York"
}

Response 200

data: {"type":"info","content":"Analyzing your question..."}

data: {"type":"sql","content":"Querying database...","sql":"SELECT o.owner_id, o.name, SUM(p.market_value) AS total_value FROM pms.properties p JOIN pms.owners o ON p.owner_id = o.owner_id GROUP BY o.owner_id, o.name ORDER BY total_value DESC LIMIT 5"}

data: {"type":"result","content":"Query returned 5 rows","rows":[{"owner_id":"O-1001","name":"Ridgeline Holdings","total_value":4820000}]}

data: {"type":"done","content":"","answer":"Ridgeline Holdings (O-1001) holds the highest total market value at $4.82M."}

POST/ai/chat/v2/streamBearer SOAR only

Legacy v2 agent loop that streams SSE AgentStep events. Uses the ToolRegistry two-tier loop with task management. AI config is read live from DbSettings; 400 if no provider/key is configured. Superseded by v3 (/ai/chat/stream) but kept for compatibility. Invokes the built-in LLM. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "messages": [
    {
      "role": "user",
      "content": "Show me all open work orders for property P-5001"
    }
  ],
  "theme": "light"
}

Response 200

data: {"type":"info","content":"Analyzing your question..."}

data: {"type":"result","content":"Query returned 3 rows","rows":[{"property_id":"P-5001","status":"open","description":"HVAC repair"}]}

data: {"type":"done","content":""}

POST/ai/chat/v3/streamBearer SOAR only

The canonical v3 tool-calling agent loop. Identical backend to /ai/chat/stream. Streams SSE AgentStep events; resolves the model via apply_model_family so each family routes to its own gateway path (Claude -> Anthropic /messages, GLM/Sparkle -> OpenAI /chat/completions). 400 if no provider is configured. Invokes the built-in LLM. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "messages": [
    {
      "role": "user",
      "content": "Vacate the tenant on lease L-7039 and start a new lease for the incoming tenant"
    }
  ],
  "modelFamily": "open"
}

Response 200

data: {"type":"info","content":"Analyzing your question..."}

data: {"type":"change_set","content":"Review the proposed changes","steps":[{"op":"update","typeName":"leases","recordId":"L-7039"}]}

data: {"type":"done","content":""}

POST/ai/chat/{tab_id}/respondBearer SOAR only

User's decision on a task that is in the awaiting_input state, keyed by the chat tab id. responseType is one of approve, deny, edit, cancel (also approve_all/answer). For edit/answer, value carries the edited SQL or answer text. Resumes (or cancels) the LLM-driven task and persists it. 403 if the caller is neither the task owner nor superadmin; 404 if no awaiting task exists for the tab. Sits behind the AI gate and resumes an LLM agent loop. 403 on the Serverless tier (this route calls the built-in LLM).

Parameters

NameInTypeDescription
tab_idpathstringChat tab id whose awaiting_input task should be resumed

Request

{
  "responseType": "edit",
  "value": "SELECT * FROM pms.leases WHERE status = 'active' LIMIT 50"
}

Response 200

{
  "status": "running",
  "taskId": "task_9f2a"
}
POST/ai/generate-sqlBearer SOAR only

Turns a natural-language question into a SQL SELECT without running it. Tries fast tiers first (exact + template cache, then the NQC neural query compiler, then the optional PSI engine) and only calls the LLM (Tier 2, tool-calling) on a miss. inferenceSource in the response reports which tier answered (Cache / InventDB NQC / InventDB Inference / External Inference). Refinements pass base_sql (bypasses all fast tiers). context narrows the schema hint; model_family overrides the model. Because the LLM is the fallback path, this route is AI-gated (403 on serverless). 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "question": "list properties in Austin, TX with market_rent over 2000, most expensive first",
  "context": {
    "namespace": "pms",
    "type": "properties"
  },
  "modelFamily": "claude"
}

Response 200

{
  "sql": "SELECT property_id, city, state, market_rent, beds, baths FROM pms.properties WHERE city = 'Austin' AND state = 'TX' AND market_rent > 2000 ORDER BY market_rent DESC",
  "inferenceSource": "External Inference"
}
POST/ai/sql-titleBearer SOAR only

Given a final SQL SELECT, returns a concise 3-7 word Title-Case label for a data-viewer grid, using the same permission-filtered schema context as the SQL generator. Results are cached per sql-hash (cached=true on a repeat hit). Best-effort: on LLM failure returns an empty title with 200 (the client falls back to its own heuristic). 400 if sql is empty. Invokes the built-in LLM on a cache miss. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "sql": "SELECT o.name, COUNT(*) AS property_count FROM pms.properties p JOIN pms.owners o ON p.owner_id = o.owner_id GROUP BY o.name ORDER BY property_count DESC"
}

Response 200

{
  "title": "Properties by Owner",
  "cached": false
}
POST/ai/generate-typesBearer SOAR only

Uses the LLM to draft a JSON array of type definitions (namespace, typeName, fields with name/type/description and optional FK references, recordCount) from a natural-language domain description. Used by the simulation/data-seeding wizard. 400 if description is empty; 500 if the model fails to return a valid JSON array (raw text echoed back). Invokes the built-in LLM. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "description": "A property management system with owners, properties, leases and tenants"
}

Response 200

{
  "types": [
    {
      "namespace": "PMS",
      "typeName": "Owner",
      "fields": [
        {
          "name": "name",
          "type": "string",
          "description": "Owner full name"
        },
        {
          "name": "email",
          "type": "string",
          "description": "Contact email"
        }
      ],
      "recordCount": 200
    },
    {
      "namespace": "PMS",
      "typeName": "Property",
      "fields": [
        {
          "name": "ownerId",
          "type": "string",
          "description": "Owning owner",
          "references": "Owner"
        },
        {
          "name": "city",
          "type": "string",
          "description": "City"
        },
        {
          "name": "marketRent",
          "type": "number",
          "description": "Monthly market rent"
        }
      ],
      "recordCount": 2500
    }
  ]
}
POST/ai/generate-dataBearer SOAR only

Streams LLM-generated realistic mock records for each supplied type definition as SSE AgentStep events. Processes parent (FK-target) types first so child records can reference real _id values; caps at 100K records/type and generates up to 50 template records per type via the model. humanReadableIds toggles CUST001-style ids vs UUIDs. Invokes the built-in LLM. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "types": [
    {
      "namespace": "pms",
      "typeName": "owners",
      "fields": [
        {
          "name": "name",
          "type": "string",
          "description": "Owner name"
        },
        {
          "name": "email",
          "type": "string",
          "description": "Email"
        }
      ],
      "recordCount": 50
    }
  ],
  "description": "property management owners",
  "humanReadableIds": true
}

Response 200

data: {"type":"info","content":"Generating simulation data for: property management owners"}

data: {"type":"info","content":"Generating 50 template records for pms.owners..."}

data: {"type":"done","content":""}

POST/ai/generate-recipeBearer SOAR only

Phase 1 of the simulation flow: derives a deterministic data-generation recipe from the supplied type definitions using Rust-side heuristics (field-name + field-type patterns and built-in word pools) and returns it plus a 10-record-per-type preview for user approval. Despite no longer calling the model, this route sits behind the AI gate and returns 403 on the Serverless tier. 400 if types is empty. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "types": [
    {
      "namespace": "pms",
      "typeName": "properties",
      "fields": [
        {
          "name": "city",
          "type": "string",
          "description": "City"
        },
        {
          "name": "marketRent",
          "type": "number",
          "description": "Rent"
        },
        {
          "name": "status",
          "type": "string",
          "description": "Status"
        }
      ],
      "recordCount": 2500
    }
  ],
  "description": "property management",
  "humanReadableIds": true
}

Response 200

{
  "recipe": {
    "types": [
      {
        "namespace": "pms",
        "typeName": "properties",
        "recordCount": 2500
      }
    ],
    "humanReadableIds": true
  },
  "preview": [
    {
      "namespace": "pms",
      "typeName": "properties",
      "records": [
        {
          "_id": "PROP001",
          "city": "Austin",
          "marketRent": 2150,
          "status": "occupied"
        }
      ]
    }
  ]
}
POST/ai/execute-simulationBearer SOAR only

Phase 2 of the simulation flow: generates records from an approved recipe and bulk-inserts them into the database, streaming SSE progress (per-type preview, then generating events at every 1% boundary, then a final done_with_answer). FK linking is preserved by processing parent types first and reusing generated _id pools. Generation itself is deterministic (recipe-driven, no model call) but the route validates AI config and sits behind the AI gate (403 on serverless). 400 if the recipe is empty. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "recipe": {
    "types": [
      {
        "namespace": "pms",
        "typeName": "properties",
        "recordCount": 2500
      }
    ],
    "humanReadableIds": true
  }
}

Response 200

data: {"type":"info","content":"Starting simulation: 1 types, 2500 total records"}

data: {"type":"generating","typeName":"properties","inserted":2500,"total":2500}

data: {"type":"done","content":"Simulation complete. Generated and inserted records across 1 types."}

GET/ai/modelsBearer SOAR + Serverless

Returns the configured model first (marked active), followed by provider-appropriate alternatives. For the InventDB gateway the alternatives are catalog-driven from the enabled model registry ('Family - LLM (Gateway)'); for direct anthropic/openai providers a small static list is returned. No LLM invocation, so this stays open on every tier.

Response 200

[
  {
    "id": "claude-opus-4-8",
    "name": "claude-opus-4-8 (active — from config.json)",
    "provider": "anthropic"
  },
  {
    "id": "glm-5.2",
    "name": "Open — GLM 5.2 (Gateway)",
    "provider": "inventdb"
  }
]
GET/ai/configBearer SOAR + Serverless

Returns whether AI is configured, a masked API key, the base URL, the model, and the resolved default model family (claude / glm / sparkle / sparkle-thinking) read live from DbSettings. Never exposes the raw key. No LLM invocation; open on every tier.

Response 200

{
  "configured": true,
  "maskedKey": "sk-a...9f2c",
  "baseUrl": "https://gateway.inventdb.com/anthropic/v1",
  "model": "claude-opus-4-8",
  "modelFamily": "claude"
}
POST/ai/uploads/stageBearer SOAR + Serverless

Multipart upload of one file into the per-session, disk-backed staging store. Returns a pending_id the client embeds in '[attached: ... — pending: <id>]' markers; the agent reads bytes for vision/text peeks and the canvas form re-attaches on Create. No DB writes, no OCR, no embeddings; files expire after 30 min and are capped at 25 MB (413 if larger). 400 if no file field is present. No LLM invocation; open on every tier.

Response 201

{
  "ok": true,
  "pending_id": "pf_1718900000000_3",
  "filename": "lease-L-7039.pdf",
  "content_type": "application/pdf",
  "size": 48213
}
GET/ai/uploads/{id}Bearer SOAR + Serverless

Returns the raw bytes of a staged file (cache-first, disk-fallback so it survives a restart). Used by the canvas form to re-attach the file to a newly created record. 404 if the pending file is missing or expired; 403 if the caller is not the uploader (superadmin may always fetch). No LLM invocation; open on every tier.

Parameters

NameInTypeDescription
idpathstringThe pending_id (pf_...) returned by /ai/uploads/stage

Response 200

<raw file bytes; Content-Type + Content-Disposition set from the staged file>
DELETE/ai/uploads/{id}Bearer SOAR + Serverless

Removes a staged file from the store and disk (called by the canvas form after a successful attach or when the user dismisses the form). Always returns 200 with a removed flag. No LLM invocation; open on every tier.

Parameters

NameInTypeDescription
idpathstringThe pending_id (pf_...) of the staged file to remove

Response 200

{
  "ok": true,
  "removed": true
}
POST/ai/change-set/applyBearer SOAR + Serverless

Runs an array of insert/update/delete steps inside a single InventDB transaction (all-or-nothing), then runs any attach steps post-commit (best-effort, outside the tx). This is the confirm-and-commit chokepoint for the AI canvas change-set card, so writes only land after explicit user consent. update merges partial fields over the current doc; record targets resolve by _id or by matchField business key. Returns per-step results and a transactionId. 400 if steps is empty or a data step fails (transaction rolled back); 409 on commit failure; 200 with ok:false if data committed but some attaches failed. No LLM invocation; open on every tier — an external agent authors the steps, this route just applies them.

Request

{
  "title": "Lease turnover for P-5001",
  "steps": [
    {
      "op": "update",
      "namespace": "pms",
      "typeName": "leases",
      "recordId": "L-7039",
      "matchField": "Lease_ID",
      "fields": {
        "status": "ended"
      }
    },
    {
      "op": "insert",
      "namespace": "pms",
      "typeName": "tenants",
      "fields": {
        "name": "Dana Cole",
        "email": "dana@example.com"
      }
    }
  ]
}

Response 200

{
  "ok": true,
  "results": [
    {
      "index": 0,
      "op": "update",
      "namespace": "pms",
      "typeName": "leases",
      "recordId": "L-7039",
      "ok": true
    },
    {
      "index": 1,
      "op": "insert",
      "namespace": "pms",
      "typeName": "tenants",
      "recordId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "ok": true
    }
  ],
  "transactionId": "tx_7c1e"
}
GET/ai/threadsBearer SOAR + Serverless

Returns the caller's persisted Analyze/Workbench chat threads (newest first) from _system._SoarThreads, scoped by user_id. The server reads via a system principal (bypassing RLS) but enforces per-user scoping on user_id. No LLM invocation; open on every tier.

Response 200

{
  "ok": true,
  "threads": [
    {
      "_id": "th_01",
      "user_id": "u_123",
      "label": "Owner value analysis",
      "created": "2026-06-20T14:03:00Z"
    }
  ]
}
PUT/ai/threadsBearer SOAR + Serverless

Creates or updates a single Analyze thread for the caller (the server stamps user_id from the bearer). The full thread payload (exchanges with artifacts + steps) is stored under the underscore-prefixed _exchanges blob field so a reload is a single round-trip and large payloads stay out of the index. 400 if id is missing; 500 on write failure. No LLM invocation; open on every tier.

Request

{
  "id": "th_01",
  "label": "Owner value analysis",
  "created": "2026-06-20T14:03:00Z",
  "exchanges": [
    {
      "question": "Top owners by market_value?",
      "answer": "Ridgeline Holdings leads at $4.82M."
    }
  ]
}

Response 200

{
  "ok": true,
  "id": "th_01"
}
DELETE/ai/threads/{id}Bearer SOAR + Serverless

Deletes a single Analyze thread owned by the caller (ownership checked against user_id via the system principal). Always returns 200 with ok:true; a non-owned or missing id is a silent no-op. No LLM invocation; open on every tier.

Parameters

NameInTypeDescription
idpathstringThread _id to delete (must be owned by the caller)

Response 200

{
  "ok": true
}

LLM API (OpenAI-compatible) Mixed tier

OpenAI-compatible LLM API surface (/v1): chat completions proxied to the built-in model, model listing, and local MiniLM embeddings.

POST/v1/chat/completionsBearer SOAR only

Proxies an OpenAI-style chat request through the shared LlmClient (the same client the AI agent uses), which dispatches to the configured provider (Anthropic reshapes to /v1/messages; OpenAI / InventDB Gateway / llama-server keep the /v1/chat/completions shape). Requires the messages array; temperature defaults to 0.7. Because it invokes the built-in LLM it returns HTTP 403 on the Serverless Database tier. Returns an OpenAI chat.completion object (usage counters are zeroed). 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "messages": [
    {
      "role": "system",
      "content": "You are a property-management assistant."
    },
    {
      "role": "user",
      "content": "Summarize the lease status for owner O-1001's properties."
    }
  ],
  "temperature": 0.7
}

Response 200

{
  "id": "chatcmpl-inventdb-3f2a9c1e-4b7d-4e21-9f0a-1c2d3e4f5a6b",
  "object": "chat.completion",
  "created": 1752403200,
  "model": "claude-opus-4-8",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Owner O-1001 holds 3 properties. P-5001 (Austin, TX) is Occupied at $2,400 market_rent; two others are Vacant."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}
GET/v1/modelsBearer SOAR + Serverless

Returns the configured external model in OpenAI list format. Reads model metadata from LlmRouter.external_config() and does NOT invoke the LLM, so it works on all tiers including Serverless.

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "claude-opus-4-8",
      "object": "model",
      "created": 1752403200,
      "owned_by": "external"
    }
  ]
}
POST/v1/embeddingsBearer SOAR + Serverless

Generates embeddings using the local all-MiniLM-L6-v2 runtime via LazyEmbeddingGenerator. Accepts a single string or an array of strings in the input field. This is the local embedding model (part of the serverless OCR+MiniLM carve-out), NOT the built-in chat/completion LLM, so it works on the Serverless tier. Returns 503 if the MiniLM model is not installed. Usage token counts are approximated by whitespace-split word count.

Request

{
  "input": [
    "Property P-5001 in Austin, TX with market_rent 2400",
    "Owner O-1001 contact info"
  ]
}

Response 200

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [
        0.0123,
        -0.0456,
        0.0789
      ]
    },
    {
      "object": "embedding",
      "index": 1,
      "embedding": [
        -0.0011,
        0.0342,
        -0.0567
      ]
    }
  ],
  "model": "text-embedding-minilm",
  "usage": {
    "prompt_tokens": 12,
    "total_tokens": 12
  }
}

Agent Tasks Any tier

Autonomous agent task management: list, inspect, cancel, and delete persisted long-running agent tasks (no LLM invoked; works on every tier).

GET/agent/tasksBearer SOAR + Serverless

Returns all persisted agent tasks owned by the authenticated user (scoped by claims.sub). Each task is a resumable, auditable record of a promoted 'full task mode' agent run — its state, steps, files created, iteration count, and token/time totals. Reads persisted records only; invokes no LLM, so it works on the Serverless Database tier.

Response 200

{
  "tasks": [
    {
      "id": "3f2a9c14-7b0e-4d2a-9c1f-8e6b5a1d0042",
      "chatTabId": "direct",
      "chatMessageSeq": 0,
      "owner": "developer.3@inventdb.com",
      "ownerRole": "admin",
      "title": "Analyze vacant units and market rent by city",
      "goal": "Give me a full analysis of vacant properties and their market rent across cities",
      "state": "Done",
      "steps": [],
      "filesCreated": [],
      "variables": {},
      "maxIterations": 50,
      "iterationCount": 8,
      "timeoutSeconds": 300,
      "confirmationPolicy": "Always",
      "createdAt": "2026-07-13T09:12:04Z",
      "updatedAt": "2026-07-13T09:13:41Z",
      "completedAt": "2026-07-13T09:13:41Z",
      "totalLlmTokens": 18422,
      "totalExecutionTimeMs": 96540
    }
  ]
}
GET/agent/tasks/{id}Bearer SOAR + Serverless

Returns the full AgentTask record for the given task id, including its complete step audit trail, task-scoped variables, and any files created. Access is restricted to the task owner or an admin/superadmin (403 otherwise); an unknown id returns 404. Reads a persisted record only; no LLM is invoked, so it works on the Serverless Database tier.

Parameters

NameInTypeDescription
idpathstringThe task UUID (from the list endpoint's `id` field).

Response 200

{
  "id": "3f2a9c14-7b0e-4d2a-9c1f-8e6b5a1d0042",
  "chatTabId": "direct",
  "chatMessageSeq": 0,
  "owner": "developer.3@inventdb.com",
  "ownerRole": "admin",
  "title": "Analyze vacant units and market rent by city",
  "goal": "Give me a full analysis of vacant properties and their market rent across cities",
  "state": "Done",
  "steps": [
    {
      "stepNumber": 1,
      "toolName": "sql_query",
      "input": {
        "sql": "SELECT city, AVG(market_rent) FROM pms.properties WHERE status = 'vacant' GROUP BY city"
      }
    }
  ],
  "filesCreated": [],
  "variables": {},
  "maxIterations": 50,
  "iterationCount": 8,
  "timeoutSeconds": 300,
  "confirmationPolicy": "Always",
  "createdAt": "2026-07-13T09:12:04Z",
  "updatedAt": "2026-07-13T09:13:41Z",
  "completedAt": "2026-07-13T09:13:41Z",
  "totalLlmTokens": 18422,
  "totalExecutionTimeMs": 96540
}
POST/agent/tasks/{id}/cancelBearer SOAR + Serverless

Marks the task as Cancelled and persists the change, stopping any further agent iterations for it. Restricted to the task owner or a superadmin (403 otherwise); an unknown id returns 404. This only flips persisted task state — it invokes no LLM and works on the Serverless Database tier.

Parameters

NameInTypeDescription
idpathstringThe task UUID to cancel.

Response 200

{
  "status": "cancelled",
  "taskId": "3f2a9c14-7b0e-4d2a-9c1f-8e6b5a1d0042"
}
DELETE/agent/tasks/{id}Bearer SOAR + Serverless

Permanently deletes the persisted agent task record. Restricted to the task owner or a superadmin (403 otherwise); an unknown id returns 404. Deletes a stored record only; no LLM is invoked, so it works on the Serverless Database tier.

Parameters

NameInTypeDescription
idpathstringThe task UUID to delete.

Response 200

{
  "status": "deleted",
  "taskId": "3f2a9c14-7b0e-4d2a-9c1f-8e6b5a1d0042"
}

Gmail Integration Any tier

Per-user Gmail OAuth connect plus send, bulk-send, list, and read endpoints backed by stored refresh tokens.

GET/api/gmail/statusBearer SOAR + Serverless

Returns whether the authenticated user has connected Gmail, and if so the connected email address and the Unix-seconds timestamp of when the connection was made. Reads the stored token record from _system.user_integrations keyed by gmail:{username}; never touches the built-in LLM.

Response 200

{
  "connected": true,
  "email": "manager@acme-pm.com",
  "connected_at": 1749600000
}
GET/api/gmail/oauth-configBearer SOAR + Serverless

Returns the Google OAuth client_id and the requested Gmail scopes (gmail.send + gmail.readonly) plus a configured flag indicating whether the server has both client_id and client_secret resolved (from DbSettings google_oauth, env fallback). The browser uses this to initialize Google Identity Services initCodeClient. No LLM involved.

Response 200

{
  "client_id": "1234567890-abc.apps.googleusercontent.com",
  "scopes": "https://www.googleapis.com/auth/gmail.send https://www.googleapis.com/auth/gmail.readonly",
  "configured": true
}
POST/api/gmail/exchange-codeBearer SOAR + Serverless

Accepts the one-shot authorization code obtained by the browser via Google Identity Services (ux_mode popup, redirect_uri=postmessage), exchanges it at Google's token endpoint for an access + refresh token pair, fetches the connected Gmail address, and persists the tokens (encrypted at rest) in _system.user_integrations. Returns 503 if OAuth is not configured, 400 if the exchange fails or Google returns no refresh token. Pure OAuth connect flow; no built-in LLM call.

Request

{
  "code": "4/0AeanS0b-oneShotAuthCodeFromGoogle"
}

Response 200

{
  "connected": true,
  "email": "manager@acme-pm.com",
  "connected_at": 1749600000
}
POST/api/gmail/revokeBearer SOAR + Serverless

Best-effort revokes the stored refresh token at Google's revoke endpoint, then always deletes the local token record from _system.user_integrations for the current user. Idempotent: returns ok:true even if no tokens were stored. No LLM involved.

Response 200

{
  "ok": true
}
POST/api/gmail/sendBearer SOAR + Serverless

Sends one email through the user's connected Gmail. The RFC-2822 message is built server-side with an RFC 2047 encoded-word Subject and a deterministic Message-ID (<wf-<nonce>@inventdb.local>) for reply-thread correlation. Optional attachments reference existing InventDB attachments by namespace + attachment_id and are fetched from NativeAttachmentStore and MIME-embedded. When INVENTDB_DEMO_SAFE_EMAIL is set, recipients are overridden. Auto-refreshes the access token first (401 on failure), 503 if OAuth unconfigured, 502 on Gmail send failure. This is a direct Gmail API send, not an LLM call.

Request

{
  "to": "owner@acme-pm.com",
  "cc": [],
  "bcc": [],
  "subject": "Past Due — Lease L-7011 at 42 Elm St",
  "body_html": "<p>Hi Jane, rent for P-5001 (market_rent $2,150) is past due.</p>",
  "attachments": [
    {
      "namespace": "pms",
      "attachment_id": "att-9f2c",
      "filename": "statement-P-5001.pdf",
      "content_type": "application/pdf"
    }
  ]
}

Response 200

{
  "message_id": "18f3a2b9c1d4e5f6",
  "thread_id": "18f3a2b9c1d4e5f6",
  "rfc_message_id": "<wf-1749600000123456789@inventdb.local>"
}
POST/api/gmail/bulk-sendBearer SOAR + Serverless

Accepts an emails array (each a fully substituted SendEmailInput) and dispatches them serially through the connected Gmail account to stay under Gmail's per-user send quota. Refreshes the access token once for the whole batch and returns a per-row success/failure result so the UI can surface cell-by-cell status. 400 if the array is empty, 503 if OAuth unconfigured, 401 on token refresh failure. Each send is a direct Gmail API call; no LLM involved (the frontend already rendered the bodies).

Request

{
  "emails": [
    {
      "to": "owner1@acme-pm.com",
      "subject": "Rent reminder — P-5001",
      "body_html": "<p>P-5001 market_rent $2,150 due for owner O-1001.</p>"
    },
    {
      "to": "owner2@acme-pm.com",
      "subject": "Rent reminder — P-5002",
      "body_html": "<p>P-5002 market_rent $1,875 due for owner O-1002.</p>"
    }
  ]
}

Response 200

{
  "sent": 2,
  "failed": 0,
  "results": [
    {
      "to": "owner1@acme-pm.com",
      "ok": true,
      "message_id": "18f3a2b9c1d4e5f6"
    },
    {
      "to": "owner2@acme-pm.com",
      "ok": true,
      "message_id": "18f3a2b9c1d4e5aa"
    }
  ]
}
GET/api/gmail/messagesBearer SOAR + Serverless

Lists recent messages from the connected Gmail account within the last `days` window, optionally filtered by a Gmail search query (from:, to:, subject:, etc.). Each result carries decoded headers (RFC 2047 + mojibake-repaired), snippet, full decoded body (text/plain preferred, HTML stripped to plain), and correlation headers (rfc_message_id, in_reply_to, references). days is clamped 1-365, max clamped 1-50. Auto-refreshes token; 503 if OAuth unconfigured, 401 on refresh failure, 502 on Gmail list failure. Read-only Gmail API access; no LLM.

Parameters

NameInTypeDescription
daysqueryintegerLook-back window in days (default 7, clamped 1-365)
qquerystringOptional Gmail search query appended to newer_than filter (e.g. from:owner@acme-pm.com)
maxqueryintegerMax messages to fetch (default 20, clamped 1-50)

Response 200

[
  {
    "id": "18f3a2b9c1d4e5f6",
    "thread_id": "18f3a2b9c1d4e5f6",
    "from": "owner@acme-pm.com",
    "to": "manager@acme-pm.com",
    "subject": "Re: Past Due — Lease L-7011",
    "date": "2026-06-20T14:03:00-07:00",
    "snippet": "I can pay by Friday for P-5001...",
    "body": "I can pay by Friday for P-5001. Please confirm the amount.",
    "rfc_message_id": "<CAF123@mail.gmail.com>",
    "in_reply_to": "<wf-1749600000123456789@inventdb.local>"
  }
]
GET/api/gmail/messages/{id}Bearer SOAR + Serverless

Fetches a single message by Gmail id (format=full), returning meta (headers, snippet, decoded body) plus separate body_text and body_html fields. Used by the expand action on the EmailSummaryCard. Auto-refreshes token; 503 if OAuth unconfigured, 401 on refresh failure, 502 on Gmail get failure. Read-only Gmail API access; no LLM.

Parameters

NameInTypeDescription
idpathstringGmail internal message id

Response 200

{
  "meta": {
    "id": "18f3a2b9c1d4e5f6",
    "thread_id": "18f3a2b9c1d4e5f6",
    "from": "owner@acme-pm.com",
    "to": "manager@acme-pm.com",
    "subject": "Re: Past Due — Lease L-7011",
    "date": "2026-06-20T14:03:00-07:00",
    "snippet": "I can pay by Friday for P-5001...",
    "body": "I can pay by Friday for P-5001. Please confirm the amount.",
    "in_reply_to": "<wf-1749600000123456789@inventdb.local>"
  },
  "body_text": "I can pay by Friday for P-5001. Please confirm the amount.",
  "body_html": "<div>I can pay by Friday for P-5001. Please confirm the amount.</div>"
}

Calendar Integration Any tier

Google Calendar integration — per-user OAuth connect plus list/create/update/delete events and free/busy; no built-in LLM involved.

GET/api/calendar/statusBearer SOAR + Serverless

Returns whether the authenticated user has a stored Google Calendar OAuth grant. When connected, includes the user's primary calendar email and the connect timestamp (unix seconds). Reads token record `_system.user_integrations` keyed by `gcal:{username}`. No LLM.

Response 200

{
  "connected": true,
  "email": "june@pmco.example",
  "connected_at": 1750636800
}
GET/api/calendar/oauth-configBearer SOAR + Serverless

Returns the server-side Google OAuth client_id and requested Calendar scopes so the frontend can launch the consent popup. `configured` is false when no Google OAuth client is set (via DbSettings google_oauth or GOOGLE_OAUTH_*/GMAIL_OAUTH_* env). The OAuth client is shared with Gmail. No LLM.

Response 200

{
  "client_id": "1234567890-abcdef.apps.googleusercontent.com",
  "scopes": "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly",
  "configured": true
}
POST/api/calendar/exchange-codeBearer SOAR + Serverless

Exchanges the OAuth `code` (from the postmessage popup flow) for tokens at Google, captures the primary calendar email, and persists a per-user CalendarTokens record keyed by `gcal:{username}`. Returns 503 when Google OAuth is not configured, and 400 when the exchange fails or Google returns no refresh_token (needs access_type=offline + prompt=consent). No LLM.

Request

{
  "code": "4/0Aea...authorization_code"
}

Response 200

{
  "connected": true,
  "email": "june@pmco.example",
  "connected_at": 1750636800
}
POST/api/calendar/revokeBearer SOAR + Serverless

Best-effort revokes the stored refresh token at Google's revoke endpoint and deletes the local CalendarTokens record for the user. Always returns ok:true. No LLM.

Response 200

{
  "ok": true
}
GET/api/calendar/calendarsBearer SOAR + Serverless

Refreshes the access token if needed, then lists the user's calendars (minAccessRole=writer, up to 50) via Google's calendarList API. Returns 503 if OAuth not configured, 401 if Calendar not connected / token refresh fails, 502 on upstream Google errors. No LLM.

Response 200

[
  {
    "id": "june@pmco.example",
    "summary": "June (Property Manager)",
    "primary": true,
    "access_role": "owner",
    "time_zone": "America/New_York",
    "background_color": "#9fe1e7"
  }
]
GET/api/calendar/eventsBearer SOAR + Serverless

Lists events for a calendar (defaults to `primary`) between timeMin and timeMax (RFC 3339), expanding recurrences (singleEvents=true) and ordering by start time. Optional free-text `q` filter and `max` cap (1..250, default 50). Returns 503 if OAuth not configured, 401 if not connected, 502 on Google errors. No LLM.

Parameters

NameInTypeDescription
calendarIdquerystringTarget calendar id; defaults to 'primary'
timeMinquerystringWindow start, RFC 3339 (required)
timeMaxquerystringWindow end, RFC 3339 (required)
qquerystringOptional free-text search filter
maxqueryintegerMax events to return, 1..250 (default 50)

Response 200

[
  {
    "id": "abc123evt",
    "status": "confirmed",
    "html_link": "https://www.google.com/calendar/event?eid=abc123evt",
    "summary": "Unit inspection — P-5001",
    "location": "P-5001, Austin, TX",
    "start": "2026-07-15T10:00:00-05:00",
    "end": "2026-07-15T10:30:00-05:00",
    "time_zone": "America/Chicago",
    "attendees": [
      {
        "email": "owner.o-1001@pmco.example",
        "display_name": "O-1001",
        "response_status": "needsAction"
      }
    ],
    "creator_email": "june@pmco.example",
    "organizer_email": "june@pmco.example",
    "all_day": false
  }
]
POST/api/calendar/eventsBearer SOAR + Serverless

Creates an event on the target calendar (defaults to `primary`), supporting timed or all-day events, IANA time_zone, attendees (sendUpdates=all so Google emails invites), and optional Google Meet link (add_meet). Under demo-safe mode all attendee addresses are rewritten to the configured safe address. Returns 503 if OAuth not configured, 401 if not connected, 502 on Google errors. No LLM.

Request

{
  "calendar_id": "primary",
  "summary": "Rent-follow-up call — O-1001",
  "description": "Discuss market_rent adjustment for P-5001",
  "location": "P-5001, Austin, TX",
  "start": "2026-07-16T14:00:00",
  "end": "2026-07-16T14:30:00",
  "time_zone": "America/Chicago",
  "all_day": false,
  "attendees": [
    "owner.o-1001@pmco.example"
  ],
  "add_meet": true
}

Response 200

{
  "id": "def456evt",
  "status": "confirmed",
  "html_link": "https://www.google.com/calendar/event?eid=def456evt",
  "summary": "Rent-follow-up call — O-1001",
  "start": "2026-07-16T14:00:00-05:00",
  "end": "2026-07-16T14:30:00-05:00",
  "time_zone": "America/Chicago",
  "attendees": [
    {
      "email": "owner.o-1001@pmco.example",
      "response_status": "needsAction"
    }
  ],
  "conference_link": "https://meet.google.com/abc-defg-hij",
  "creator_email": "june@pmco.example",
  "organizer_email": "june@pmco.example",
  "all_day": false
}
PATCH/api/calendar/events/{id}Bearer SOAR + Serverless

Partially updates an event by id on the target calendar (defaults to `primary`). Any of summary/description/location/attendees may be set; start and end must be supplied together (Google rejects only-one) and all_day toggles date vs dateTime shapes. sendUpdates=all notifies attendees. Returns 503 if OAuth not configured, 401 if not connected, 502 on Google errors. No LLM.

Parameters

NameInTypeDescription
idpathstringGoogle Calendar event id

Request

{
  "calendar_id": "primary",
  "summary": "Rent-follow-up call — O-1001 (rescheduled)",
  "start": "2026-07-17T15:00:00",
  "end": "2026-07-17T15:30:00",
  "time_zone": "America/Chicago",
  "all_day": false
}

Response 200

{
  "id": "def456evt",
  "status": "confirmed",
  "html_link": "https://www.google.com/calendar/event?eid=def456evt",
  "summary": "Rent-follow-up call — O-1001 (rescheduled)",
  "start": "2026-07-17T15:00:00-05:00",
  "end": "2026-07-17T15:30:00-05:00",
  "time_zone": "America/Chicago",
  "attendees": [
    {
      "email": "owner.o-1001@pmco.example",
      "response_status": "needsAction"
    }
  ],
  "creator_email": "june@pmco.example",
  "organizer_email": "june@pmco.example",
  "all_day": false
}
DELETE/api/calendar/events/{id}Bearer SOAR + Serverless

Deletes an event by id from the target calendar (defaults to `primary`); sendUpdates=all cancels the invite for attendees. A Google 410 Gone (already deleted) is treated as success. Returns 503 if OAuth not configured, 401 if not connected, 502 on other Google errors. No LLM.

Parameters

NameInTypeDescription
idpathstringGoogle Calendar event id
calendarIdquerystringTarget calendar id; defaults to 'primary'

Response 200

{
  "ok": true
}
POST/api/calendar/bulk-eventsBearer SOAR + Serverless

Creates a list of pre-rendered events sequentially against the user's calendar (sequential to respect Google's per-user write throttling). Returns per-item results with saved/failed counts. Rejects an empty events array with 400. Returns 503 if OAuth not configured, 401 if not connected. Each event uses the same CreateEventInput shape as POST /events. No LLM.

Request

{
  "events": [
    {
      "summary": "Inspection — P-5001",
      "start": "2026-07-20T09:00:00",
      "end": "2026-07-20T09:30:00",
      "time_zone": "America/Chicago",
      "attendees": [
        "owner.o-1001@pmco.example"
      ]
    },
    {
      "summary": "Inspection — P-5002",
      "start": "2026-07-20T11:00:00",
      "end": "2026-07-20T11:30:00",
      "time_zone": "America/Chicago"
    }
  ]
}

Response 200

{
  "saved": 2,
  "failed": 0,
  "results": [
    {
      "summary": "Inspection — P-5001",
      "ok": true,
      "id": "evt-p5001",
      "html_link": "https://www.google.com/calendar/event?eid=evt-p5001"
    },
    {
      "summary": "Inspection — P-5002",
      "ok": true,
      "id": "evt-p5002",
      "html_link": "https://www.google.com/calendar/event?eid=evt-p5002"
    }
  ]
}

Twilio / SMS Any tier

Bring-your-own-Twilio SMS integration: per-user credential storage, number listing, single/bulk SMS send, message history, and a public signature-verified inbound webhook.

GET/api/twilio/statusBearer SOAR + Serverless

Returns whether the authenticated user has stored Twilio credentials, along with the connected Account SID, Twilio friendly name, default 'From' number, whether an auth token is present (needed for webhook signature verification), and the connection timestamp. When not connected, all fields are null/false. No LLM involved.

Response 200

{
  "connected": true,
  "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "friendly_name": "InventDB PMS Outbound",
  "default_from": "+18885550100",
  "has_auth_token": true,
  "connected_at": 1751328000
}
POST/api/twilio/store-credentialsBearer SOAR + Serverless

Persists the user's Twilio credentials (encrypt-stored in _system.user_integrations keyed by twilio:{username}). Requires account_sid (must start with 'AC' and be 34 chars) plus EITHER an auth_token OR an api_key_sid+api_key_secret pair. Credentials are validated live against GET /Accounts/{SID}.json before persisting; a 401 is returned if Twilio rejects them. A previously-configured default_from is preserved across re-saves. Returns the same shape as /status. Configures an external provider (Twilio), not the built-in LLM.

Request

{
  "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "api_key_sid": "SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "api_key_secret": "your-api-key-secret",
  "auth_token": "your-account-auth-token"
}

Response 200

{
  "connected": true,
  "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "friendly_name": "InventDB PMS Outbound",
  "default_from": null,
  "has_auth_token": true,
  "connected_at": 1751328000
}
POST/api/twilio/revokeBearer SOAR + Serverless

Deletes the current user's stored Twilio credentials from _system.user_integrations. Idempotent — always returns ok:true. No LLM involved.

Response 200

{
  "ok": true
}
GET/api/twilio/numbersBearer SOAR + Serverless

Fetches up to 50 IncomingPhoneNumbers from the user's Twilio account (GET /Accounts/{SID}/IncomingPhoneNumbers.json), returning each number's SID, E.164 phone number, friendly name, and whether it is SMS-capable. Returns 401 if Twilio is not connected, 502 if the Twilio API call fails. No LLM involved.

Response 200

[
  {
    "sid": "PNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "phone_number": "+18885550100",
    "friendly_name": "PMS Notifications",
    "capabilities_sms": true
  }
]
POST/api/twilio/default-fromBearer SOAR + Serverless

Sets the default outbound 'From' number used by /send and /bulk-send when a message omits an explicit 'from'. Persists to the user's stored credentials. Returns 401 if Twilio is not connected. No LLM involved.

Request

{
  "phone_number": "+18885550100"
}

Response 200

{
  "ok": true,
  "default_from": "+18885550100"
}
POST/api/twilio/sendBearer SOAR + Serverless

Sends one SMS via the user's Twilio account (POST /Accounts/{SID}/Messages.json). Body requires 'to' and 'body'; 'from' is optional and falls back to the stored default_from (error if neither is set). Returns the Twilio message SID, delivery status, resolved to/from numbers, and segment count. Returns 401 if not connected, 502 on Twilio API failure. Uses the external Twilio API, not the built-in LLM — works on serverless.

Request

{
  "to": "+14155550188",
  "from": "+18885550100",
  "body": "Reminder: rent for P-5001 (Austin, TX) is due. Balance $2450."
}

Response 200

{
  "sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "status": "queued",
  "to": "+14155550188",
  "from": "+18885550100",
  "num_segments": 1
}
POST/api/twilio/bulk-sendBearer SOAR + Serverless

Sends an array of SMS messages sequentially, each with its own to/body/optional from. Returns per-message results (ok flag, SID or error, segment count) plus aggregate sent/failed counts. Returns 400 if the messages array is empty, 401 if not connected. Individual message failures are captured in the results array rather than failing the whole request. External Twilio API, no built-in LLM — works on serverless.

Request

{
  "messages": [
    {
      "to": "+14155550188",
      "body": "Rent reminder for owner O-1001."
    },
    {
      "to": "+14155550190",
      "from": "+18885550100",
      "body": "Work order update for P-5001."
    }
  ]
}

Response 200

{
  "sent": 2,
  "failed": 0,
  "results": [
    {
      "to": "+14155550188",
      "ok": true,
      "sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "num_segments": 1
    },
    {
      "to": "+14155550190",
      "ok": true,
      "sid": "SMyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
      "num_segments": 1
    }
  ]
}
GET/api/twilio/messagesBearer SOAR + Serverless

Lists recent messages from the user's Twilio account (GET /Accounts/{SID}/Messages.json), filtered by a rolling day window and optional from/to numbers. Returns SID, direction, from/to, body, status, date_sent, segment count, and price/price_unit per message. Returns 401 if not connected, 502 on Twilio failure. No built-in LLM — works on serverless.

Parameters

NameInTypeDescription
daysqueryintegerRolling window of days to look back (DateSent>=). Defaults to 7; minimum enforced as 1.
fromquerystringOptional filter — only messages sent from this E.164 number.
toquerystringOptional filter — only messages sent to this E.164 number.
maxqueryintegerMax rows to fetch (PageSize). Defaults to 50; clamped to 1..=100.

Response 200

[
  {
    "sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "direction": "outbound-api",
    "from": "+18885550100",
    "to": "+14155550188",
    "body": "Rent reminder for P-5001.",
    "status": "delivered",
    "date_sent": "Mon, 13 Jul 2026 14:02:11 +0000",
    "num_segments": 1,
    "price": "-0.00790",
    "price_unit": "USD"
  }
]
POST/twilio-webhook/incomingpublic SOAR + Serverless

Public route (no JWT) that Twilio POSTs as application/x-www-form-urlencoded when one of the user's numbers receives an SMS. The user is identified via the AccountSid form field; the X-Twilio-Signature header is verified with HMAC-SHA1 over the full request URL plus sorted params keyed by the user's auth_token (honouring X-Forwarded-Proto/Host for reverse-proxy setups). On success a stripped record (account_sid, message_sid, from, to, body, num_segments, num_media, received_at, username) is persisted to _system.twilio_incoming as an audit/compliance log and an empty TwiML-equivalent 200 is returned. Missing AccountSid => 400; unknown account => 404; signature mismatch => 401. No built-in LLM involved.

Request

{
  "AccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "MessageSid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "From": "+14155550188",
  "To": "+18885550100",
  "Body": "Tenant reply: I'll pay by Friday.",
  "NumSegments": "1",
  "NumMedia": "0"
}

Response 200

{
  "ok": true
}

Web Search Any tier

Per-user opt-in toggle for the AI agent's hosted web-search tool (Anthropic web_search_20250305).

GET/api/websearch/statusBearer SOAR + Serverless

Returns whether the AI agent's hosted web-search tool is enabled for the authenticated user, plus whether they have acknowledged the data-leaves-tenant consent dialog and when. Reads a single per-user document from _system.user_integrations keyed by websearch:{username}; if none exists, returns enabled=false/consented=false. Pure settings read — does not invoke the built-in LLM, so it works on the Serverless tier.

Response 200

{
  "enabled": true,
  "consented": true,
  "consented_at": 1718000000
}
POST/api/websearch/enableBearer SOAR + Serverless

Turns on the opt-in web-search integration for the authenticated user. Sets enabled=true on the per-user _system.user_integrations document (creating it if absent). Records the consent timestamp the first time it is enabled and preserves that timestamp across later disable/re-enable cycles so the consent dialog is not re-shown. This only flips a settings flag that the agent loop later reads to decide whether to append the hosted web_search_20250305 tool; the route itself does not call the built-in LLM, so it works on the Serverless tier.

Request

{}

Response 200

{
  "ok": true,
  "enabled": true,
  "consented_at": 1718000000
}
POST/api/websearch/disableBearer SOAR + Serverless

Turns off the web-search integration for the authenticated user by setting enabled=false on the per-user _system.user_integrations document. The original consent timestamp is preserved so the user is not re-prompted if they re-enable later. Settings-write only — does not invoke the built-in LLM, so it works on the Serverless tier.

Request

{}

Response 200

{
  "ok": true,
  "enabled": false
}

AI Usage Metrics Any tier

Read-only AI usage/cost analytics, per-user/type/model breakdowns, quota management, and Chorus plan-cache savings — no LLM invoked, so it works on the Serverless tier (reports zero AI usage).

GET/api/ai-metrics/usageBearer SOAR + Serverless

Rolls up _system.AIUsage into a single period summary: request count, input/output/cached token totals, cache hit rate, average latency, and USD cost rollups (total/input/output). Reports mode 'local' or 'external' depending on whether a local LLM router is registered. Aggregation is done in Rust (GROUP BY model, no WHERE) to sidestep a known WHERE+SUM engine bug, so the period param is accepted but not yet filtered. Read-only reporting; invokes no LLM, works on serverless (reports zeros).

Parameters

NameInTypeDescription
periodquerystringLookback window token, e.g. 30d, 7d, 24h. Defaults to 30d. Currently echoed in the response but not applied to filtering.

Response 200

{
  "mode": "external",
  "period": "30d",
  "totalRequests": 412,
  "totalInputTokens": 1840293,
  "totalOutputTokens": 96114,
  "totalCachedTokens": 1620004,
  "cacheHitRate": 46.81,
  "avgLatencyMs": 2143,
  "totalCostUsd": 12.45,
  "totalInputCostUsd": 9.124,
  "totalOutputCostUsd": 3.326
}
GET/api/ai-metrics/dailyBearer SOAR + Serverless

Returns a per-day series (bucketed by the YYYY-MM-DD prefix of _createdAt) of request count, input/output/cached tokens, average latency, and total USD cost. Raw rows are fetched and aggregated in Rust because GROUP BY with scalar functions plus SUM hits a known engine bug. Read-only reporting; no LLM, works on serverless.

Parameters

NameInTypeDescription
periodquerystringLookback window token (30d/7d/24h). Defaults to 30d; echoed back but not applied to filtering.

Response 200

{
  "period": "30d",
  "days": [
    {
      "date": "2026-07-11",
      "requests": 22,
      "inputTokens": 84021,
      "outputTokens": 5140,
      "cachedTokens": 71002,
      "avgLatencyMs": 1980,
      "totalCostUsd": 0.6412
    },
    {
      "date": "2026-07-12",
      "requests": 31,
      "inputTokens": 120440,
      "outputTokens": 7210,
      "cachedTokens": 99880,
      "avgLatencyMs": 2210,
      "totalCostUsd": 0.9033
    }
  ]
}
GET/api/ai-metrics/modelsBearer SOAR + Serverless

Returns one row per model from _system.AIUsage (GROUP BY model): request count, input/output/cached token sums, total USD cost (rounded to 4 decimals) and average latency. Read-only reporting; no LLM, works on serverless.

Parameters

NameInTypeDescription
periodquerystringLookback window token; defaults to 30d, echoed but not applied to filtering.

Response 200

{
  "period": "30d",
  "models": [
    {
      "model": "claude-opus-4-8",
      "requests": 210,
      "inputTokens": 980221,
      "outputTokens": 52110,
      "cachedTokens": 860003,
      "totalCostUsd": 8.421,
      "avgLatencyMs": 2560
    }
  ]
}
GET/api/ai-metrics/healthBearer SOAR + Serverless

Lightweight health probe reporting whether a local LLM is available and the resulting mode ('local' vs 'external'). Does not query any table and does not invoke the LLM; works on serverless.

Response 200

{
  "status": "ok",
  "mode": "external",
  "localLlmAvailable": false
}
GET/api/ai-metrics/usage-consolidatedBearer SOAR + Serverless

Same summary shape as /usage but sourced from the gateway's central AIGateway.Usage ledger, scoped to the calling customer via WHERE clientId = <jwt.sub> (RLS-enforced). Rows are post-filtered by the period threshold in memory and cost is computed on-read from the pricing rate cache. Missing table / RLS-deny degrades to zeroed metrics rather than a 500. Read-only reporting; no LLM, works on serverless.

Parameters

NameInTypeDescription
periodquerystringWindow token: 24h/1d, 7d, 30d, 90d. Unrecognized tokens fall back to all-time. Defaults to 30d.

Response 200

{
  "mode": "gateway",
  "period": "30d",
  "totalRequests": 88,
  "totalInputTokens": 420110,
  "totalOutputTokens": 21044,
  "totalCachedTokens": 360020,
  "cacheHitRate": 46.15,
  "avgLatencyMs": 2050,
  "totalCostUsd": 3.21,
  "totalInputCostUsd": 2.401,
  "totalOutputCostUsd": 0.809
}
GET/api/ai-metrics/daily-consolidatedBearer SOAR + Serverless

Daily series (bucketed by the YYYY-MM-DD prefix of the gateway's rfc3339 timestamp) from AIGateway.Usage, scoped to the calling customer via clientId and post-filtered by the period threshold. Cost computed on-read from the pricing cache. Missing table degrades to an empty series. Read-only reporting; no LLM, works on serverless.

Parameters

NameInTypeDescription
periodquerystringWindow token: 24h/1d, 7d, 30d, 90d. Defaults to 30d; unrecognized falls back to all-time.

Response 200

{
  "period": "30d",
  "days": [
    {
      "date": "2026-07-12",
      "requests": 12,
      "inputTokens": 60110,
      "outputTokens": 3021,
      "cachedTokens": 50004,
      "avgLatencyMs": 1990,
      "totalCostUsd": 0.4412
    }
  ]
}
GET/api/ai-metrics/models-consolidatedBearer SOAR + Serverless

Per-model breakdown that, despite the name, reads THIS instance's own _system.AIUsage ledger (AIGateway.Usage is not written on a SOAR/customer instance). Raw (provider,model) variants are folded into one friendly model name; each row carries request count, token sums, average latency, total USD cost, and a 4-lever breakdown (Input / Cache read / Cache write / Output with tokens, per-1M rate, and subtotal). Sorted highest-spend first. Read-only reporting; no LLM, works on serverless.

Parameters

NameInTypeDescription
periodquerystringWindow token: 24h/1d, 7d, 30d, 90d. When recognized it applies a WHERE timestamp >= threshold filter; otherwise all-time. Defaults to 30d.

Response 200

{
  "period": "30d",
  "models": [
    {
      "model": "Claude Opus 4.8",
      "requests": 210,
      "inputTokens": 980221,
      "outputTokens": 52110,
      "cachedTokens": 860003,
      "cacheWriteTokens": 42000,
      "avgLatencyMs": 2560,
      "totalCostUsd": 8.421,
      "breakdown": [
        {
          "lever": "Input",
          "tokens": 980221,
          "ratePerM": 15,
          "costUsd": 14.703315
        },
        {
          "lever": "Cache read",
          "tokens": 860003,
          "ratePerM": 1.5,
          "costUsd": 1.290005
        },
        {
          "lever": "Cache write",
          "tokens": 42000,
          "ratePerM": 18.75,
          "costUsd": 0.7875
        },
        {
          "lever": "Output",
          "tokens": 52110,
          "ratePerM": 75,
          "costUsd": 3.90825
        }
      ]
    }
  ]
}
GET/api/ai-metrics/quotasBearer SOAR + Serverless

Returns AI quota status from the quota subsystem. Admin/superadmin callers see full config (global + per-user limits), today's usage totals, and by-user / by-type breakdowns. Non-admins get a redacted view: global limit knobs and global totals are stripped, top-level quota-pct fields removed, byUser filtered to the caller's own row, and byType dropped. Read-only; no LLM, works on serverless.

Response 200

{
  "enabled": true,
  "date": "2026-07-13",
  "config": {
    "dailyRequestLimit": 6000,
    "dailyTokenLimit": 25000000,
    "perUserDailyRequestLimit": 600,
    "perUserDailyTokenLimit": 2500000
  },
  "usage": {
    "totalRequests": 2,
    "totalInputTokens": 112,
    "totalOutputTokens": 1582,
    "totalCachedTokens": 7354,
    "totalTokens": 1694
  },
  "requestQuotaPct": 0.0,
  "tokenQuotaPct": 0.0,
  "byUser": [
    {
      "user": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
      "requests": 2,
      "inputTokens": 112,
      "outputTokens": 1582,
      "cachedTokens": 7354,
      "totalTokens": 1694
    }
  ],
  "byType": [
    {
      "type": "ai_agent",
      "requests": 1,
      "tokens": 120
    },
    {
      "type": "ai_sql",
      "requests": 1,
      "tokens": 1574
    }
  ]
}
PUT/api/ai-metrics/quotasBearer SOAR + Serverless

Updates the global and per-user daily request/token limits and the enabled flag. Admin/superadmin only — returns 403 for any other role so a user with a domain write grant cannot raise their own AI limits. Returns the applied config. No LLM invoked; works on serverless.

Request

{
  "dailyRequestLimit": 6000,
  "dailyTokenLimit": 25000000,
  "perUserDailyRequestLimit": 600,
  "perUserDailyTokenLimit": 2500000,
  "enabled": true
}

Response 200

{
  "ok": true,
  "config": {
    "dailyRequestLimit": 6000,
    "dailyTokenLimit": 25000000,
    "perUserDailyRequestLimit": 600,
    "perUserDailyTokenLimit": 2500000,
    "enabled": true
  }
}
POST/api/ai-metrics/quotas/resetBearer SOAR + Serverless

Resets the current day's AI quota counter. Admin/superadmin only — returns 403 otherwise so a user cannot zero out their own usage to bypass the per-user cap. No LLM invoked; works on serverless.

Response 200

{
  "ok": true,
  "message": "Daily counter reset"
}
GET/api/ai-metrics/usage-by-userBearer SOAR + Serverless

Aggregates _system.AIUsage per database user in Rust: request count, input/output/cached tokens, total tokens (input+output) and average latency. Read-only reporting; no LLM, works on serverless.

Parameters

NameInTypeDescription
periodquerystringLookback window token; defaults to 30d, echoed but not applied to filtering.

Response 200

{
  "period": "30d",
  "users": [
    {
      "user": "developer.3@inventdb.com",
      "requests": 40,
      "inputTokens": 210000,
      "outputTokens": 10200,
      "cachedTokens": 180000,
      "totalTokens": 220200,
      "avgLatencyMs": 2100
    }
  ]
}
GET/api/ai-metrics/usage-by-typeBearer SOAR + Serverless

Aggregates _system.AIUsage per usageType (e.g. chat, report, extract) in Rust: request count, input/output/cached tokens, total tokens and average latency. Read-only reporting; no LLM, works on serverless.

Parameters

NameInTypeDescription
periodquerystringLookback window token; defaults to 30d, echoed but not applied to filtering.

Response 200

{
  "period": "30d",
  "types": [
    {
      "type": "chat",
      "requests": 60,
      "inputTokens": 300110,
      "outputTokens": 15020,
      "cachedTokens": 260000,
      "totalTokens": 315130,
      "avgLatencyMs": 2200
    }
  ]
}
GET/api/ai-metrics/detailBearer SOAR + Serverless

Detailed analytics for THIS instance from _system.AIUsage (SELECT * LIMIT 200000, filtered by timestamp prefix in Rust). Default mode returns period totals (requests, tokens, cache hit rate, avg latency, success rate, dollars), a daily series, and byModel / byOperation / byUser / byProvider buckets, plus the current-month AI $ allowance from the cmgmt account cache (null on standalone instances). With ?day=YYYY-MM-DD it returns that day's per-request rows (newest first). With ?view=events it returns a filterable, paged event feed. Read-only reporting; no LLM, works on serverless.

Parameters

NameInTypeDescription
monthquerystringMonth window YYYY-MM. Defaults to the current UTC month.
dayquerystringDay drill-down YYYY-MM-DD; returns that day's per-request rows newest-first instead of the aggregate.
viewquerystringSet to 'events' for the month-scoped filterable/paged per-request event feed.
modelquerystringevents view: exact-match filter on friendly model name.
providerquerystringevents view: exact-match filter on friendly provider name.
userquerystringevents view: exact-match filter on database user.
opquerystringevents view: exact-match filter on usageType/operation.
pagequeryintegerevents view: zero-based page index. Defaults to 0.
sizequeryintegerevents view: page size, clamped 1..500. Defaults to 50.

Response 200

{
  "ok": true,
  "month": "2026-07",
  "totals": {
    "requests": 88,
    "inputTokens": 420110,
    "outputTokens": 21044,
    "cachedTokens": 360020,
    "cacheCreationTokens": 42000,
    "cacheHitRate": 0.4615,
    "avgLatencyMs": 2050,
    "successRate": 1,
    "dollars": 3.21
  },
  "daily": [
    {
      "day": "2026-07-12",
      "requests": 12,
      "inputTokens": 60110,
      "outputTokens": 3021,
      "cachedTokens": 50004
    }
  ],
  "byModel": [
    {
      "key": "Claude Opus 4.8",
      "requests": 60,
      "inputTokens": 300110,
      "outputTokens": 15020,
      "cachedTokens": 260000
    }
  ],
  "byOperation": [
    {
      "key": "chat",
      "requests": 60,
      "inputTokens": 300110,
      "outputTokens": 15020,
      "cachedTokens": 260000
    }
  ],
  "byUser": [
    {
      "key": "developer.3@inventdb.com",
      "requests": 40,
      "inputTokens": 210000,
      "outputTokens": 10200,
      "cachedTokens": 180000
    }
  ],
  "byProvider": [
    {
      "key": "Anthropic",
      "requests": 60,
      "inputTokens": 300110,
      "outputTokens": 15020,
      "cachedTokens": 260000
    }
  ],
  "allowance": {
    "dollarsAllocated": 50,
    "dollarsUsed": 3.21,
    "periodMonth": "2026-07",
    "tierBase": "team"
  }
}
GET/api/ai-metrics/chorusBearer SOAR + Serverless

Computes the Chorus (semantic plan cache) funnel and measured savings entirely from _system.ChorusMetrics + _system.AIUsage with no hardcoded rates. Returns hit/agent-turn counts and hit-rate, the outcome funnel, chorus wall-time and priced extraction actuals (endpoint=chorus-extract), and for each observed default-model family a measured 'what those hits would have cost' comparison (avg agent-turn ms/tokens/cost plus time/tokens/cost saved). ChorusMetrics is absent until the first Chorus decision, degrading to zeros. Read-only reporting; no LLM, works on serverless.

Response 200

{
  "hits": 120,
  "agentTurns": 80,
  "hitRatePct": 60,
  "funnel": {
    "hit_sql": 90,
    "hit_tool": 30,
    "miss": 55,
    "skip": 25
  },
  "chorus": {
    "totalHitMs": 4200,
    "avgHitMs": 35,
    "extractCalls": 80,
    "extractTokens": 96000,
    "extractCostUsd": 0.144
  },
  "assumptions": {
    "callsPerAgentTurn": 3
  },
  "savingsByModel": [
    {
      "model": "Claude Opus 4.8",
      "avgAgentTurnMs": 7680,
      "avgAgentTurnTokens": 14400,
      "avgAgentTurnCostUsd": 0.216,
      "timeSavedMs": 917400,
      "tokensSaved": 1631000,
      "costSavedUsd": 25.776
    }
  ]
}

Saved Views Mixed tier

Persist a data-viewer card (its SQL + display config) as a reusable, listable Saved View, with deterministic custom-layout rendering and optional AI layout generation.

POST/api/saved-viewsBearer SOAR + Serverless

Persists a data-viewer card as a `_System.SavedViews` document: its authoritative `baseSql` plus display config (defaultMode, custom layout template link, attachment/search flags, column order, live browse filter, and the Ask-AI conversation that produced the SQL). `createdBy` is taken from the caller's JWT and `version` is forced to 1. Returns the stored document. Deterministic CRUD — no LLM — so it works on the Serverless tier.

Request

{
  "name": "Active Vacant Properties",
  "description": "Vacant units ordered by market rent",
  "namespace": "pms",
  "baseSql": "SELECT property_id, city, state, market_rent, status, beds, baths FROM pms.properties WHERE status = 'vacant' ORDER BY market_rent DESC",
  "defaultMode": "table",
  "attachmentsEnabled": false,
  "searchAttachments": false,
  "columns": [
    "property_id",
    "city",
    "state",
    "market_rent",
    "status"
  ],
  "search": "vacant",
  "sort": {
    "col": "market_rent",
    "dir": "desc"
  },
  "tags": [
    "properties",
    "vacancy"
  ]
}

Response 201

{
  "ok": true,
  "data": {
    "_id": "view_9f2a1c",
    "name": "Active Vacant Properties",
    "description": "Vacant units ordered by market rent",
    "namespace": "pms",
    "baseSql": "SELECT property_id, city, state, market_rent, status, beds, baths FROM pms.properties WHERE status = 'vacant' ORDER BY market_rent DESC",
    "defaultMode": "table",
    "customScope": "row",
    "attachmentsEnabled": false,
    "searchAttachments": false,
    "columns": [
      "property_id",
      "city",
      "state",
      "market_rent",
      "status"
    ],
    "search": "vacant",
    "sort": {
      "col": "market_rent",
      "dir": "desc"
    },
    "version": 1,
    "createdBy": "u_1001",
    "tags": [
      "properties",
      "vacancy"
    ]
  }
}
GET/api/saved-viewsBearer SOAR + Serverless

Lists all saved views (summary columns only: _id, name, description, namespace, defaultMode, attachmentsEnabled, version, createdBy) ordered by name, capped at 1000. Deterministic read — no LLM — works on the Serverless tier.

Response 200

{
  "ok": true,
  "data": {
    "views": [
      {
        "_id": "view_9f2a1c",
        "name": "Active Vacant Properties",
        "description": "Vacant units ordered by market rent",
        "namespace": "pms",
        "defaultMode": "table",
        "attachmentsEnabled": false,
        "version": 1,
        "createdBy": "u_1001"
      }
    ]
  }
}
GET/api/saved-views/{id}Bearer SOAR + Serverless

Fetches one saved view's full document by id. Returns 404 when the id does not exist. Deterministic read — no LLM — works on the Serverless tier.

Parameters

NameInTypeDescription
idpathstringThe saved view's _id (e.g. view_9f2a1c).

Response 200

{
  "ok": true,
  "data": {
    "name": "Active Vacant Properties",
    "description": "Vacant units ordered by market rent",
    "namespace": "pms",
    "baseSql": "SELECT property_id, city, state, market_rent, status, beds, baths FROM pms.properties WHERE status = 'vacant' ORDER BY market_rent DESC",
    "defaultMode": "table",
    "customScope": "row",
    "attachmentsEnabled": false,
    "searchAttachments": false,
    "columns": [
      "property_id",
      "city"
    ],
    "search": "vacant",
    "sort": {
      "col": "market_rent",
      "dir": "desc"
    },
    "version": 1,
    "createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
    "tags": [
      "properties",
      "vacancy"
    ],
    "_id": "5a1fcac6-7089-416a-aef3-57b7c73147ed",
    "_createdAt": "2026-07-13T15:57:13.185793800+00:00",
    "_updatedAt": "2026-07-13T15:57:13.185793800+00:00",
    "_createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed"
  }
}
PUT/api/saved-views/{id}Bearer SOAR + Serverless

Shallow-merges the JSON body into the existing saved-view document (any subset of fields), ignoring the immutable `_id`, `_createdAt`, and `createdBy` keys, and auto-increments `version`. Returns 404 if the view is missing. Deterministic mutation — no LLM — works on the Serverless tier.

Parameters

NameInTypeDescription
idpathstringThe saved view's _id to update.

Request

{
  "defaultMode": "custom",
  "customTemplateId": "tpl_7c33",
  "customScope": "row",
  "columns": [
    "property_id",
    "city",
    "market_rent",
    "beds",
    "baths"
  ]
}

Response 200

{
  "ok": true,
  "data": {
    "ok": true
  }
}
DELETE/api/saved-views/{id}Bearer SOAR + Serverless

Deletes the saved-view document by id. Returns the deleted id. Deterministic mutation — no LLM — works on the Serverless tier.

Parameters

NameInTypeDescription
idpathstringThe saved view's _id to delete.

Response 200

{
  "ok": true,
  "data": {
    "deleted": "view_9f2a1c"
  }
}
POST/api/saved-views/render-layoutBearer SOAR + Serverless

Renders a view's custom (non-table) card layout for a single page. Windows `baseSql` server-side via the engine parser (no client SQL surgery), loads the linked `_System.ReportTemplates` template, runs it over that page with `params.viewSql` set to the windowed SQL, and returns `{ html, total, page, pageSize }` where `total` is the real full row count for accurate paging. Pure report-engine rendering with no model in the path — works on the Serverless tier (an AI-authored template renders with no LLM).

Request

{
  "templateId": "tpl_7c33",
  "baseSql": "SELECT property_id, city, state, market_rent, status, beds, baths FROM pms.properties WHERE status = 'vacant' ORDER BY market_rent DESC",
  "page": 0,
  "pageSize": 25
}

Response 200

{
  "ok": true,
  "data": {
    "html": "<section class=\"cards\">…<div class=\"card\" data-record-id=\"P-5001\">…</div>…</section>",
    "total": 128,
    "page": 0,
    "pageSize": 25
  }
}
POST/api/saved-views/generate-layoutBearer SOAR only

Invokes the built-in LLM to produce report-engine HTML for a view's custom (non-table) card layout. Fresh generation designs a card layout from the schema + sample records; when `currentHtml` is supplied it EDITS that design applying only `instruction` (with `history` as context). Attached `pendingIds` (from /ai/uploads/stage) are passed as vision inputs (images, e.g. a mockup) or inlined text. The HTML reads its rows from `query(params.viewSql)` and may embed a leading `<!--VIEWSQL: SELECT … -->` directive; the response returns `{ html, sql }` (sql falls back to baseSql). This route calls the built-in LLM, so it returns HTTP 403 on the Serverless Database tier. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "baseSql": "SELECT property_id, city, state, market_rent, status, beds, baths, sqft FROM pms.properties",
  "namespace": "pms",
  "instruction": "Property cards showing photo, city, beds/baths and rent, highest sqft first",
  "modelFamily": "claude"
}

Response 200

{
  "html": "<style>…</style><script type=\"server\">const r = query(params.viewSql); for (const row of r.rows) { out.write(`<div class=\"card\" data-surface=\"flat\" data-record-id=\"${esc(String(row._id))}\">…${esc(row.city)}…${fmt.currency(row.market_rent)}…</div>`); }</script>",
  "sql": "SELECT * FROM pms.properties ORDER BY sqft DESC"
}

Forms Any tier

Schema-driven data-entry form engine: analyze a type's schema, build/store HTML form templates, render forms with FK typeahead lookups, run SQL validation rules, and submit records (create/update).

GET/forms/schema/{namespace}/{type_name}Bearer SOAR + Serverless

Samples records from the given namespace/type (default 100 rows) to infer each property's data type, FK targets (fields ending in _id whose target type exists), FK display fields, cascade relationships (e.g. unit_id cascades from property_id), and enum-like fields (2-20 distinct values). Returns the inferred property list, the type's display field, and total record count. Used by an external AI agent to design a form template; the route itself runs no LLM.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'
type_namepathstringType (table) name, e.g. 'properties'
sample_sizequeryintegerNumber of records to sample for schema inference (default 100)

Response 200

{
  "ok": true,
  "namespace": "pms",
  "typeName": "properties",
  "properties": [
    {
      "name": "owner_id",
      "type": "string",
      "is_fk": true,
      "fk_target": "owners",
      "fk_display": "name"
    },
    {
      "name": "beds",
      "type": "integer"
    },
    {
      "name": "city",
      "type": "string"
    },
    {
      "name": "status",
      "type": "string",
      "enum_values": [
        "occupied",
        "vacant"
      ]
    },
    {
      "name": "market_rent",
      "type": "number"
    }
  ],
  "displayField": "_id",
  "recordCount": 5000
}
GET/forms/lookup/{namespace}/{type_name}Bearer SOAR + Serverless

Searches a target type for a searchable FK picker. Matches the query against the detected display field(s) and _id via LIKE, optionally constrained by a cascade filter (filter=field:value). Returns up to 'limit' items (default 20, capped 200), each with _id, a composed display string, and the full record. No LLM involved.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'
type_namepathstringTarget type to search, e.g. 'owners'
qquerystringSearch text matched against display field(s) and _id
filterquerystringCascade filter in the form field:value, e.g. 'property_id:P-5001'
limitqueryintegerMax results (default 20, capped at 200)

Response 200

{
  "ok": true,
  "items": [
    {
      "_id": "P-5019",
      "display": "P-5019",
      "record": {
        "property_id": "P-5001",
        "city": "Richmond"
      }
    },
    {
      "_id": "P-5037",
      "display": "P-5037",
      "record": {
        "property_id": "P-5037",
        "owner_id": "O-1005",
        "street": "3924 Pinecrest Cir",
        "unit": "Apt 301",
        "city": "Falls Church",
        "state": "VA",
        "zip": "22042",
        "region": "Northern VA",
        "type": "Apartment Unit",
        "beds": 1,
        "baths": 1,
        "sqft": 968,
        "year_built": 2015,
        "lot_ac": 0,
        "garage": "None",
        "market_rent": 2550,
        "market_value": 468000,
        "acq_date": "2017-10-22T12:00:00",
        "acq_price": 227000,
        "annual_tax": 4000,
        "annual_insurance": 1025,
        "monthly_hoa": 165,
        "status": "Occupied",
        "_id": "P-5037",
        "_createdAt": "2026-07-13T15:51:56.948069100+00:00",
        "_updatedAt": "2026-07-13T15:51:56.948069100+00:00"
      }
    }
  ],
  "total": 20
}
GET/forms/templatesBearer SOAR + Serverless

Returns up to 200 form template documents stored in _System.FormTemplates. Pure read; no LLM.

Response 200

{
  "ok": true,
  "data": [
    {
      "_id": "pm.property",
      "namespace": "pm",
      "typeName": "property",
      "fields": [
        {
          "name": "property_id",
          "order": 0
        },
        {
          "name": "owner_id",
          "order": 1
        }
      ],
      "_createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
      "_createdAt": "2026-06-12T07:05:29.806022100+00:00",
      "_updatedAt": "2026-06-12T07:05:29.806022100+00:00"
    },
    {
      "_id": "crm.departments",
      "namespace": "crm",
      "typeName": "departments",
      "fields": [
        {
          "name": "department_code",
          "order": 0,
          "label": "Department Code",
          "type": "text"
        },
        {
          "name": "name",
          "order": 1,
          "label": "Department Name",
          "type": "text"
        }
      ],
      "_createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
      "_createdAt": "2026-06-12T11:23:15.626362500+00:00",
      "_updatedAt": "2026-06-12T11:23:15.626362500+00:00"
    }
  ]
}
POST/forms/templatesBearer SOAR + Serverless

Inserts a new form template into _System.FormTemplates with the supplied name, target namespace/type, HTML template body (with {{field:*}}, {{lookup:*}}, {{validate:*}} placeholders), bindings, and style. Stamps status='active', version=1, and created_by from the caller's claims. The template body is typically authored by an external AI agent; this route runs no LLM. Returns 201 with the new record id.

Request

{
  "name": "New Property",
  "namespace": "pms",
  "typeName": "properties",
  "htmlTemplate": "{{field:city|label=City|required}} {{lookup:owner_id|target=owners|display=name}}",
  "bindings": [],
  "style": {
    "primaryColor": "#0d9488"
  }
}

Response 201

{
  "ok": true,
  "id": "FT-02"
}
GET/forms/templates/{id}Bearer SOAR + Serverless

Returns a single form template document from _System.FormTemplates, or ok:false if not found. Pure read; no LLM.

Parameters

NameInTypeDescription
idpathstringForm template record id

Response 200

{
  "ok": true,
  "data": {
    "_id": "FT-02",
    "name": "New Property",
    "namespace": "pms",
    "typeName": "properties",
    "htmlTemplate": "{{field:city|label=City}}",
    "status": "active",
    "version": 1
  }
}
PUT/forms/templates/{id}Bearer SOAR + Serverless

Applies the supplied JSON as a partial update to the form template document in _System.FormTemplates. Accepts an arbitrary object body. No LLM.

Parameters

NameInTypeDescription
idpathstringForm template record id

Request

{
  "name": "New Property (v2)",
  "style": {
    "primaryColor": "#48176a"
  }
}

Response 200

{
  "ok": true
}
GET/forms/{id}/renderBearer SOAR + Serverless

Loads the template, resolves {{field}}, {{lookup}}, {{section}}, {{row}}, {{hidden}}, {{validate}} placeholders into inputs, and returns a self-contained HTML page (embedded CSS + FormEngine JS) for creating a new record. Content-Type text/html. No LLM.

Parameters

NameInTypeDescription
idpathstringForm template record id

Response 200

<!DOCTYPE html><html lang="en">... New Property form ...</html>
GET/forms/{id}/render/{record_id}Bearer SOAR + Serverless

Same as render but loads the target record (namespace/type from the template) and pre-fills inputs and lookup chips for editing. Submit label becomes 'Save Changes'. Returns a self-contained HTML page. No LLM.

Parameters

NameInTypeDescription
idpathstringForm template record id
record_idpathstringId of the record to edit, e.g. 'P-5001'

Response 200

<!DOCTYPE html><html lang="en">... Editing properties record P-5001 ...</html>
POST/forms/validateBearer SOAR + Serverless

Substitutes :param placeholders in the supplied SQL with values from params (single-quote escaped), executes it against the database, and returns the resulting rows and count. Used by the form engine to enforce {{validate:*}} rules before submit. Executes SQL only; no LLM.

Request

{
  "sql": "SELECT COUNT(*) AS c FROM pms.properties WHERE property_id = :property_id",
  "params": {
    "property_id": "P-5001"
  }
}

Response 200

{
  "ok": true,
  "rows": [
    {
      "c": 1
    }
  ],
  "count": 1
}
POST/forms/{id}/submitBearer SOAR + Serverless

Resolves the template's target namespace/type, stamps created_by and modified_by from the caller's claims, and inserts the submitted data as a new record. Returns 201 with the new id. No LLM.

Parameters

NameInTypeDescription
idpathstringForm template record id

Request

{
  "property_id": "P-5001",
  "owner_id": "O-1001",
  "city": "Austin",
  "state": "TX",
  "market_rent": 2450,
  "status": "vacant",
  "beds": 3,
  "baths": 2,
  "sqft": 1650,
  "market_value": 412000
}

Response 201

{
  "ok": true,
  "id": "P-5001"
}
PUT/forms/{id}/submit/{record_id}Bearer SOAR + Serverless

Resolves the template's target namespace/type, stamps modified_by from the caller's claims, and updates the identified record with the submitted data. Returns 200 with the record id. No LLM.

Parameters

NameInTypeDescription
idpathstringForm template record id
record_idpathstringId of the record to update, e.g. 'P-5001'

Request

{
  "market_rent": 2600,
  "status": "occupied"
}

Response 200

{
  "ok": true,
  "id": "P-5001"
}

Transactions Any tier

Multi-statement ACID transaction lifecycle: begin, execute buffered CRUD/SQL operations, then commit or rollback.

POST/txBearer SOAR + Serverless

Opens a new server-side transaction and returns its generated transaction id. Subsequent execute/commit/rollback calls reference this id. No LLM involved.

Response 201

{
  "ok": true,
  "txId": "tx-8f3c2a10-4b7e-4d21-9a55-1c0e2f9b6d84"
}
POST/tx/{txId}/commitBearer SOAR + Serverless

Durably commits all buffered operations for the given transaction id. Returns 200 on success or 400 with an error message if the commit fails (e.g. conflict or unknown/expired txId). No LLM involved.

Parameters

NameInTypeDescription
txIdpathstringTransaction id returned by POST /tx

Response 200

{
  "ok": true
}
POST/tx/{txId}/rollbackBearer SOAR + Serverless

Discards all buffered operations for the given transaction id and releases it. Always returns 200 ok (rollback is idempotent/best-effort). No LLM involved.

Parameters

NameInTypeDescription
txIdpathstringTransaction id returned by POST /tx

Response 200

{
  "ok": true
}
POST/tx/{txId}/executeBearer SOAR + Serverless

Runs a single operation within the open transaction. The request body is tagged by an 'operation' discriminator: insert, update, delete, get, or query. insert/update run referential-integrity validation (returns 409 on RI violation); delete runs RI-for-delete validation (409 on violation). update/delete/get return 404 when the target document does not exist. query runs SQL within the transaction and returns the result rows array. Response carries ExecuteResponse { ok, data, id, error }; success uses HTTP 201 for insert and 200 for update/delete/get/query. No LLM involved — plain buffered CRUD/SQL.

Parameters

NameInTypeDescription
txIdpathstringTransaction id returned by POST /tx

Request

{
  "operation": "insert",
  "namespace": "pms",
  "type": "properties",
  "document": {
    "property_id": "P-5001",
    "owner_id": "O-1001",
    "city": "Austin",
    "state": "TX",
    "market_rent": 2450,
    "status": "occupied",
    "beds": 3,
    "baths": 2,
    "sqft": 1680,
    "market_value": 415000
  }
}

Response 201

{
  "ok": true,
  "id": "01J8ZC7K3QET9F2M0PROP5001",
  "data": {
    "_id": "01J8ZC7K3QET9F2M0PROP5001",
    "property_id": "P-5001",
    "owner_id": "O-1001",
    "city": "Austin",
    "state": "TX",
    "market_rent": 2450,
    "status": "occupied",
    "beds": 3,
    "baths": 2,
    "sqft": 1680,
    "market_value": 415000
  }
}

Account Any tier

Instance-native account card: who the caller is plus this instance's company, plan tier, and live AI-dollar allowance in one call.

GET/api/account/summaryBearer SOAR + Serverless

Returns, in a single call, the calling user's identity plus the company, plan tier, and live AI-dollar allowance for THIS instance so the SOAR profile card and the Console settings page can show company/tier/credits without deep-linking back to the customer portal. The 'user' block is resolved from the caller's own _system.users row (self-introspection pinned to _id = JWT subject under a system principal), falling back to claims-only data for service-account subjects with no row. The 'customer' and 'allowance' blocks come from the cmgmt_account poller cache (HMAC-signed hourly pull from cmgmt); both serialize as null on a STANDALONE instance with no cmgmt configured, and the frontends degrade to user-only. NOT AI-gated: serverless instances still surface user + company + plan (the allowance just stays null/empty there since serverless has no built-in LLM budget). Auth-protected by the global AuthMiddleware (Bearer JWT).

Response 200

{
  "user": {
    "id": "usr_9c1f2a",
    "username": "asha.pm",
    "email": "asha.pm@pms.example.com",
    "role": "user"
  },
  "customer": {
    "name": "Copperline Property Management",
    "tierCode": "ai-platform",
    "tierProduct": "AI Platform",
    "planStatus": "active",
    "renewsAt": "2026-08-01T00:00:00Z"
  },
  "allowance": {
    "dollarsAllocated": 50,
    "dollarsUsed": 12.47,
    "periodMonth": "2026-07",
    "tierBase": 50
  }
}

Users, Roles & Permissions Any tier

Admin CRUD for users, per-user/role permissions (RBAC grants + row-level filters), and named/app-scoped roles.

GET/api/usersBearer SOAR + Serverless

List all users from _system.users. Admin only. Returns a flat JSON array of user objects directly (C#-API compatible, not wrapped). Supports optional activeOnly and search (username/email substring, case-insensitive) query filters.

Parameters

NameInTypeDescription
activeOnlyquerybooleanWhen true, only users with is_active=true are returned.
searchquerystringCase-insensitive substring match on username or email.

Response 200

[
  {
    "_id": "1f5387b6-487d-44e1-a869-c48f97e06b39",
    "username": "manager1",
    "email": "manager1@example.com",
    "role": "writer",
    "isActive": true,
    "createdAt": "2026-07-13T15:24:18.648185700+00:00",
    "updatedAt": "2026-07-13T15:24:18.648185700+00:00"
  },
  {
    "_id": "3ebe362f-79eb-4726-803c-70d56c36a2e8",
    "username": "O-1001",
    "email": null,
    "role": "rbac_owner_rw",
    "isActive": true,
    "createdAt": "2026-07-04T13:15:54.528755300+00:00",
    "updatedAt": "2026-07-04T13:15:54.528755300+00:00"
  }
]
POST/api/usersBearer SOAR + Serverless

Create a new user in _system.users. Admin only. Hashes the password, generates a UUID _id, defaults role to 'reader'. 409 if the username already exists; 400 if username is empty or >64 chars. Returns the created user wrapped in {ok,user}.

Request

{
  "username": "manager1",
  "password": "S3cret!pass",
  "email": "manager1@example.com",
  "role": "writer"
}

Response 201

{
  "ok": true,
  "user": {
    "_id": "a1b2c3d4-0009",
    "username": "manager1",
    "email": "manager1@example.com",
    "role": "writer",
    "isActive": true,
    "createdAt": "2026-06-23T10:00:00Z",
    "updatedAt": "2026-06-23T10:00:00Z"
  }
}
GET/api/users/{userId}Bearer SOAR + Serverless

Fetch one user by _id, or by username as a fallback (the Console passes the username). Admin only. Returns {ok,user}; 404 when neither _id nor username matches.

Parameters

NameInTypeDescription
userIdpathstringUser _id or username.

Response 200

{
  "ok": true,
  "user": {
    "_id": "1b37c660-8a99-4f08-a6ed-ae77d7948960",
    "username": "manager1",
    "email": "manager1@example.com",
    "role": "writer",
    "isActive": true,
    "createdAt": "2026-07-13T16:08:46.428791800+00:00",
    "updatedAt": "2026-07-13T16:08:46.428791800+00:00"
  }
}
PUT/api/users/{userId}Bearer SOAR + Serverless

Update a user's email, role, and/or password (any subset). Admin only. Password is re-hashed if supplied. Returns the updated user in {ok,user}; 404 if not found.

Parameters

NameInTypeDescription
userIdpathstringUser _id.

Request

{
  "email": "newmail@example.com",
  "role": "analyst"
}

Response 200

{
  "ok": true,
  "user": {
    "_id": "a1b2c3d4-0002",
    "username": "O-1001",
    "email": "newmail@example.com",
    "role": "analyst",
    "isActive": true,
    "createdAt": "2026-05-02T09:00:00Z",
    "updatedAt": "2026-06-23T10:05:00Z"
  }
}
DELETE/api/users/{userId}Bearer SOAR + Serverless

Delete a user by _id. Admin only. Refuses to delete the caller's own account (400). Returns {ok:true}; 404 if not found.

Parameters

NameInTypeDescription
userIdpathstringUser _id to delete.

Response 200

{
  "ok": true
}
PUT/api/users/{userId}/activateBearer SOAR + Serverless

Set is_active=true on the user. Admin only. Returns the updated user in {ok,user}; 404 if not found.

Parameters

NameInTypeDescription
userIdpathstringUser _id.

Response 200

{
  "ok": true,
  "user": {
    "_id": "a1b2c3d4-0002",
    "username": "O-1001",
    "email": "owner@example.com",
    "role": "reader",
    "isActive": true,
    "createdAt": "2026-05-02T09:00:00Z",
    "updatedAt": "2026-06-23T10:06:00Z"
  }
}
PUT/api/users/{userId}/deactivateBearer SOAR + Serverless

Set is_active=false on the user. Admin only. Returns the updated user in {ok,user}; 404 if not found.

Parameters

NameInTypeDescription
userIdpathstringUser _id.

Response 200

{
  "ok": true,
  "user": {
    "_id": "a1b2c3d4-0002",
    "username": "O-1001",
    "email": "owner@example.com",
    "role": "reader",
    "isActive": false,
    "createdAt": "2026-05-02T09:00:00Z",
    "updatedAt": "2026-06-23T10:07:00Z"
  }
}
PUT/api/users/{userId}/roleBearer SOAR + Serverless

Change the user's global (JWT primary) role. Admin only. Valid roles: reader, writer, analyst, admin, superadmin. Only a superadmin may grant the superadmin role (403 otherwise); invalid role => 400. Returns {ok,user}.

Parameters

NameInTypeDescription
userIdpathstringUser _id.

Request

{
  "role": "analyst"
}

Response 200

{
  "ok": true,
  "user": {
    "_id": "a1b2c3d4-0002",
    "username": "O-1001",
    "email": "owner@example.com",
    "role": "analyst",
    "isActive": true,
    "createdAt": "2026-05-02T09:00:00Z",
    "updatedAt": "2026-06-23T10:08:00Z"
  }
}
PUT/api/users/{userId}/app-roles/{app}Bearer SOAR + Serverless

Set app_roles[<app>]=<role> on a user (dynamic-roles design). Admin only. Validates that (app, role) exists in _system.roles; '_global' is rejected as an app (400). Returns {ok,user}; 404 if the user is not found.

Parameters

NameInTypeDescription
userIdpathstringUser _id.
apppathstringApp id whose role catalog the role belongs to (e.g. inventdb-tenantry).

Request

{
  "role": "Manager"
}

Response 200

{
  "ok": true,
  "user": {
    "_id": "a1b2c3d4-0002",
    "username": "O-1001",
    "email": "owner@example.com",
    "role": "reader",
    "isActive": true,
    "createdAt": "2026-05-02T09:00:00Z",
    "updatedAt": "2026-06-23T10:09:00Z"
  }
}
DELETE/api/users/{userId}/app-roles/{app}Bearer SOAR + Serverless

Remove app_roles[<app>] from a user. Admin only. Returns {ok,user}; 404 if the user is not found.

Parameters

NameInTypeDescription
userIdpathstringUser _id.
apppathstringApp id to unassign.

Response 200

{
  "ok": true,
  "user": {
    "_id": "a1b2c3d4-0002",
    "username": "O-1001",
    "email": "owner@example.com",
    "role": "reader",
    "isActive": true,
    "createdAt": "2026-05-02T09:00:00Z",
    "updatedAt": "2026-06-23T10:10:00Z"
  }
}
GET/api/permissions/checkBearer SOAR + Serverless

Check whether the CURRENT user has a given access level on a resource. Admin/superadmin short-circuit to has_permission=true. Delegates to the same rbac helpers the enforcement middleware uses (direct grants + role grants, case-insensitive, access-level hierarchy) so the UI never shows buttons that 403 on click. resource is either 'ns' or 'ns.type'.

Parameters

NameInTypeDescription
resourcequerystringNamespace ('pms') or fully-qualified type ('pms.properties').
levelquerystringAccess level to test: read|write|delete|admin (case-insensitive).

Response 200

{
  "ok": true,
  "has_permission": true
}
GET/api/permissions/{username}Bearer SOAR + Serverless

Return the flat array of direct permission grants for a user (Archivus-compatible shape). Admin only. Accepts username or _id in the path; storage is keyed by resolved _id. Enriches each grant with grantedByUsername (batched _id->username lookup). Returns [] when the user has no grants.

Parameters

NameInTypeDescription
usernamepathstringUsername or _id of the subject user.

Response 200

[
  {
    "_id": "perm-uuid-1",
    "scope": "type",
    "resource": "pms.properties",
    "accessLevel": "read",
    "grantedBy": "a1b2c3d4-0001",
    "grantedByUsername": "admin",
    "grantedAt": "2026-06-01T12:00:00Z",
    "rowFilter": "_createdBy = :current_user"
  }
]
POST/api/permissions/{username}Bearer SOAR + Serverless

Grant a direct permission to a user. Admin only. accessLevel must be read|write|admin (400 otherwise); target user must exist (404, except the 'superadmin' pseudo-user). Optional rowFilter (v1 DSL) and rowFilterSql (v2 SQL) scope the grant to a row subset. Returns 201 with the created permission.

Parameters

NameInTypeDescription
usernamepathstringUsername or _id of the subject user.

Request

{
  "scope": "type",
  "resource": "pms.properties",
  "accessLevel": "read",
  "rowFilter": "owner_id = :current_user"
}

Response 201

{
  "_id": "perm-uuid-2",
  "scope": "type",
  "resource": "pms.properties",
  "accessLevel": "read",
  "grantedBy": "a1b2c3d4-0001",
  "grantedByUsername": "admin",
  "grantedAt": "2026-06-23T10:12:00Z",
  "rowFilter": "owner_id = :current_user"
}
DELETE/api/permissions/{username}/{permissionId}Bearer SOAR + Serverless

Revoke a single grant from a user by permission _id. Admin only. 404 if the user has no permissions doc or the permissionId is not present. Returns 204 No Content on success.

Parameters

NameInTypeDescription
usernamepathstringUsername or _id of the subject user.
permissionIdpathstringThe _id of the grant to revoke.

Response 204

GET/api/rolesBearer SOAR + Serverless

List all role records from _system.roles (legacy bundle-named + app-scoped). Admin only. Returns {ok:true,data:[Role]}.

Response 200

{
  "ok": true,
  "data": [
    {
      "_id": "leasing_agent",
      "name": "leasing_agent",
      "description": "Read/write on leases, read-only on all other pms data",
      "grants": [
        {
          "scope": "type",
          "resource": "pms.leases",
          "accessLevel": "write"
        },
        {
          "scope": "namespace",
          "resource": "pms",
          "accessLevel": "read"
        }
      ],
      "system": false,
      "owner": "user",
      "createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
      "createdAt": "2026-06-14T12:26:13.963286700+00:00"
    },
    {
      "_id": "pms:owner",
      "name": "pms:owner",
      "description": "Owner sees only their own properties",
      "grants": [
        {
          "scope": "type",
          "resource": "pms.properties",
          "accessLevel": "read",
          "rowFilter": "owner_id = :current_user"
        }
      ],
      "system": false,
      "owner": "user",
      "createdBy": "a9b4e239-27ff-4f38-949b-38b332b3f8ed",
      "createdAt": "2026-07-13T15:24:18.734618100+00:00"
    }
  ]
}
POST/api/rolesBearer SOAR + Serverless

Create a legacy bundle-named role (_id = name; users opt in via roles[]). Admin only. Validates the name (1-64 chars, [A-Za-z0-9_-:], not a reserved name), every grant, and any rowFilter. 409 if the name already exists or is bundle-owned. Returns {ok:true,data:Role}.

Request

{
  "name": "pms:owner",
  "description": "Owner sees only their own properties",
  "grants": [
    {
      "scope": "type",
      "resource": "pms.properties",
      "accessLevel": "read",
      "rowFilter": "owner_id = :current_user"
    }
  ]
}

Response 200

{
  "ok": true,
  "data": {
    "_id": "pms:owner",
    "name": "pms:owner",
    "description": "Owner sees only their own properties",
    "grants": [
      {
        "scope": "type",
        "resource": "pms.properties",
        "accessLevel": "read",
        "rowFilter": "owner_id = :current_user"
      }
    ],
    "system": false,
    "owner": "user",
    "createdBy": "a1b2c3d4-0001",
    "createdAt": "2026-06-23T10:13:00Z"
  }
}
GET/api/roles/{name}Bearer SOAR + Serverless

Fetch one role by bare name (e.g. 'analyst', resolves the seeded role__global_<name> doc) or by fully-qualified internal _id. Admin only. Returns {ok:true,data:Role}; 404 if not found.

Parameters

NameInTypeDescription
namepathstringRole name or internal role _id.

Response 200

{
  "ok": true,
  "data": {
    "_id": "role__global_analyst",
    "name": "analyst",
    "app": "_global",
    "description": "Analyst - read-only + own reports.",
    "grants": [],
    "system": true,
    "owner": "system"
  }
}
PUT/api/roles/{name}Bearer SOAR + Serverless

Update a user-owned legacy role's description, grants, and/or rowFilter (rowFilter accepts null to clear). Admin only. Refuses built-in roles (403) and bundle-owned roles (409). Validates grants/rowFilter. Returns {ok:true,data:Role}.

Parameters

NameInTypeDescription
namepathstringRole _id/name to update.

Request

{
  "description": "Owner read scope (updated)",
  "grants": [
    {
      "scope": "type",
      "resource": "pms.properties",
      "accessLevel": "read",
      "rowFilter": "owner_id = :current_user"
    }
  ]
}

Response 200

{
  "ok": true,
  "data": {
    "_id": "pms:owner",
    "name": "pms:owner",
    "description": "Owner read scope (updated)",
    "grants": [
      {
        "scope": "type",
        "resource": "pms.properties",
        "accessLevel": "read",
        "rowFilter": "owner_id = :current_user"
      }
    ],
    "system": false,
    "owner": "user",
    "updatedAt": "2026-06-23T10:14:00Z"
  }
}
DELETE/api/roles/{name}Bearer SOAR + Serverless

Delete a user-owned legacy role. Admin only. Refuses built-in roles (403) and bundle-owned roles (409). By default 409s if any users still carry the role in roles[] (lists them); pass ?force=true to auto-detach from all users first. Returns {ok:true}.

Parameters

NameInTypeDescription
namepathstringRole _id/name to delete.
forcequerybooleanWhen true, detach the role from every user first, then delete.

Response 200

{
  "ok": true
}
POST/api/roles/{name}/bulk-attachBearer SOAR + Serverless

Attach one role to many users in a single admin call (idempotent). Provide exactly one of userIds (explicit _id list) or whereSql (a WHERE-clause body run as SELECT _id FROM _system.users WHERE <whereSql> under a system principal). 404 if the role does not exist; 400 if neither/both selectors given. Returns counts of attached/skipped plus per-user errors.

Parameters

NameInTypeDescription
namepathstringRole name to attach.

Request

{
  "whereSql": "username LIKE 'O-%'"
}

Response 200

{
  "ok": true,
  "attached": 42,
  "skipped": 3,
  "role": "pms:owner"
}
POST/api/users/{username}/rolesBearer SOAR + Serverless

Attach a single legacy role to one user (adds to roles[], idempotent). Admin only. Accepts username or _id in the path. 404 if the role or user is not found. Returns {ok:true,data:{username,roles}}.

Parameters

NameInTypeDescription
usernamepathstringUsername or _id of the target user.

Request

{
  "role": "pms:owner"
}

Response 200

{
  "ok": true,
  "data": {
    "username": "O-1001",
    "roles": [
      "pms:owner"
    ]
  }
}
DELETE/api/users/{username}/roles/{role}Bearer SOAR + Serverless

Remove a single legacy role from a user's roles[] array. Admin only. Accepts username or _id in the path; no-op if the user or role is absent. Returns {ok:true}.

Parameters

NameInTypeDescription
usernamepathstringUsername or _id of the target user.
rolepathstringRole name to detach.

Response 200

{
  "ok": true
}

Vectors (per-property) Any tier

Admin-only management of per-property and per-segment (universal HNSW) vector embeddings: inspect, (re)generate, backfill, and delete MiniLM embeddings.

GET/api/vectors/{namespace}/{type}Bearer SOAR + Serverless

Returns every property that has a vector (distinct-value) embedding index for the given namespace/type, plus per-property vector count, source property, embedding dimension (384 for all-MiniLM-L6-v2), on-disk file size, and last rebuild time. Also returns the type's total record count. Admin/superadmin only (403 otherwise). Vector embeddings use the local MiniLM model, not the built-in chat LLM, so this works on the Serverless tier.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'.
typepathstringType (table) name, e.g. 'properties'.

Response 200

{
  "namespace": "pms",
  "type": "properties",
  "totalRecords": 500,
  "vectorProperties": [
    {
      "propertyName": "city",
      "vectorCount": 42,
      "sourceProperty": "city",
      "dimension": 384,
      "fileSizeBytes": 65536,
      "lastRebuild": "2026-06-20T14:03:11+00:00"
    }
  ]
}
GET/api/vectors/{namespace}/{type}/available-propertiesBearer SOAR + Serverless

Samples one document from the type and returns every non-system string property (skips names starting with '_', ending in 'Id', or equal to 'id') as a candidate for vectorization, flagging which ones already have an embedding index and their current embedding count. Also returns the total record count. Admin/superadmin only (403 otherwise). Returns 404 if the type does not exist. No built-in LLM involved.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'.
typepathstringType (table) name, e.g. 'properties'.

Response 200

{
  "namespace": "pms",
  "type": "properties",
  "totalRecords": 500,
  "stringProperties": [
    {
      "propertyName": "city",
      "hasEmbedding": true,
      "embeddingCount": 42
    },
    {
      "propertyName": "state",
      "hasEmbedding": false,
      "embeddingCount": 0
    },
    {
      "propertyName": "status",
      "hasEmbedding": false,
      "embeddingCount": 0
    }
  ]
}
GET/api/vectors/{namespace}/{type}/{property}/regenerateBearer SOAR + Serverless

Builds the distinct-value MiniLM embedding cache + HNSW index for a single property and streams progress as Server-Sent Events. Append-only (incremental) by default; pass ?force=true to wipe and rebuild from scratch. Accepts an optional ?token query param so an EventSource (which cannot set Authorization headers) can authenticate. Each SSE 'message' event carries a VectorRegenerationProgress JSON payload (status regenerating/completed/failed, totalRecords=distinct count, processedRecords=cached count, progressPercentage, timestamps, elapsedMs); a final 'done' event closes the stream. Admin/superadmin only (a non-admin caller receives a failed progress event, not an HTTP 403). Uses the local MiniLM embedder, not the built-in chat LLM.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'.
typepathstringType (table) name, e.g. 'properties'.
propertypathstringProperty to embed, e.g. 'city'.
tokenquerystringOptional bearer token for EventSource auth (used in place of the Authorization header).
forcequerybooleanIf true, wipe the existing cache and rebuild from scratch. Default false (incremental append-only build).

Response 200

event: message
data: {"namespace":"pms","type":"properties","propertyName":"city","status":"regenerating","totalRecords":42,"processedRecords":0,"progressPercentage":0.0,"startTime":"2026-06-20T14:03:11+00:00","elapsedMs":0}

event: message
data: {"namespace":"pms","type":"properties","propertyName":"city","status":"completed","totalRecords":42,"processedRecords":42,"progressPercentage":100.0,"startTime":"2026-06-20T14:03:11+00:00","endTime":"2026-06-20T14:03:12+00:00","elapsedMs":840}

event: done
data: {}

GET/api/vectors/{namespace}/{type}/{property}/progressBearer SOAR + Serverless

Returns the last-known VectorRegenerationProgress for a property's embedding build from the in-memory progress tracker; if no build has been tracked it returns an 'idle' snapshot. Used to poll batch regenerations that don't stream SSE. Always HTTP 200 (a non-admin caller gets status 'error' with error_message 'Admin access required' in the body, not an HTTP 403). No built-in LLM involved.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'.
typepathstringType (table) name, e.g. 'properties'.
propertypathstringProperty whose build progress is queried, e.g. 'city'.

Response 200

{
  "namespace": "pms",
  "type": "properties",
  "propertyName": "city",
  "status": "completed",
  "totalRecords": 42,
  "processedRecords": 42,
  "progressPercentage": 100,
  "startTime": "2026-06-20T14:03:11+00:00",
  "endTime": "2026-06-20T14:03:12+00:00",
  "elapsedMs": 840
}
DELETE/api/vectors/{namespace}/{type}/{property}Bearer SOAR + Serverless

Deletes the distinct-value embedding cache file for a single property. Returns ok=true with vectorsDeleted=1 when a cache existed, ok=true with vectorsDeleted=0 when none existed, and 500 on I/O failure. Admin/superadmin only (403 otherwise). No built-in LLM involved.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'.
typepathstringType (table) name, e.g. 'properties'.
propertypathstringProperty whose embedding cache to delete, e.g. 'city'.

Response 200

{
  "ok": true,
  "message": "Deleted distinct embedding cache for pms.properties.city",
  "vectorsDeleted": 1
}
POST/api/vectors/{namespace}/{type}/regenerate-batchBearer SOAR + Serverless

Queues incremental (append-only) embedding rebuilds for a list of properties, spawning a background build per property (skips any already regenerating). Returns immediately with the list of queued properties; poll the per-property /progress endpoint to track completion. Returns 400 if the properties list is empty. Admin/superadmin only (403 otherwise). Uses the local MiniLM embedder, not the built-in chat LLM.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'.
typepathstringType (table) name, e.g. 'properties'.

Request

{
  "properties": [
    "city",
    "state",
    "status"
  ]
}

Response 200

{
  "ok": true,
  "message": "Queued 3 properties for regeneration",
  "propertiesQueued": [
    "city",
    "state",
    "status"
  ]
}
POST/api/vectors/universal/{namespace}/{type}/rebuildBearer SOAR + Serverless

Marks every sealed segment of the given type vectors_pending and runs the backfill synchronously, rebuilding all per-segment HNSW vector indexes for that type. Returns the namespace/type, segments processed, and elapsed time; 500 if the rebuild task fails. Admin/superadmin only (403 otherwise). Uses the local MiniLM embedder, not the built-in chat LLM.

Parameters

NameInTypeDescription
namespacepathstringNamespace (database) name, e.g. 'pms'.
typepathstringType (table) name, e.g. 'properties'.

Response 200

{
  "namespace": "pms",
  "typeName": "properties",
  "processedSegments": 3,
  "elapsedMs": 1240
}

Admin — AI & Config Mixed tier

Superadmin/admin configuration: built-in LLM provider/key/model, unbounded-read row cap, Google OAuth client, and Chorus plan-cache administration.

GET/api/admin/ai-configBearer SOAR only

Returns the configured built-in LLM provider, effective and default base URL/model, whether an API key is stored (never the key itself) and where it resolves from (db/env/none), the sample-data toggle, the high-level model family, and Chorus/Sparkle settings. Admin or superadmin required. Configures the built-in LLM, so it is meaningless (403) on the Serverless Database tier. 403 on the Serverless tier (this route calls the built-in LLM).

Response 200

{
  "provider": "anthropic",
  "base_url": "",
  "default_base_url": "https://api.anthropic.com",
  "model": "",
  "default_model": "claude-opus-4-8",
  "has_api_key": true,
  "api_key_source": "db",
  "include_sample_data": true,
  "model_family": "claude",
  "extractor_model": "",
  "chorus_enabled": true,
  "chorus_plan_first": true,
  "chorus_extractor_thinking": false,
  "sparkle_speculative": false,
  "is_sparkle": false
}
PUT/api/admin/ai-configBearer SOAR only

Saves the built-in LLM provider, API key, base URL, model, sample-data toggle, and Chorus/Sparkle settings. All fields are optional; an omitted field is left unchanged, an empty string clears (for provider/base_url/model/api_key it reverts to provider default or clears the key). Provider/key changes require a server restart to take effect for the running agent loop (restart_required:true). Admin or superadmin required. Configures the built-in LLM, so it 403s on the Serverless Database tier. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "provider": "anthropic",
  "api_key": "sk-ant-...",
  "base_url": "",
  "model": "",
  "include_sample_data": true,
  "extractor_model": "",
  "chorus_enabled": true,
  "chorus_plan_first": true
}

Response 200

{
  "ok": true,
  "restart_required": true,
  "error": null
}
PUT/api/admin/ai-config/model-familyBearer SOAR only

Flips the customer-level default model family, preserving the gateway client key and host. 'claude' -> Anthropic adapter (Opus 4.8); 'glm' (or deprecated alias 'open') -> OpenAI adapter (OpenRouter GLM 5.2 + Qwen3-VL); 'sparkle'/'sparkle-thinking' -> Sparkle. Only provider/base_url/model change. Takes effect on the next AI call (no restart). Admin or superadmin required. Configures the built-in LLM, so it 403s on the Serverless Database tier. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "family": "glm"
}

Response 200

{
  "ok": true,
  "family": "glm",
  "provider": "openai",
  "base_url": "https://gateway.inventdb.com/v1",
  "model": "z-ai/glm-5.2",
  "error": null
}
GET/api/admin/ai-modelsBearer SOAR only

Returns the upstream model catalog for the Settings model picker. Only 'openrouter' returns a non-empty list (fetched live and cached 5 minutes, with pricing normalized to $/Mtok); 'openai'/'anthropic' return an empty array so the UI falls back to a free-text field. Admin or superadmin required. This is built-in-LLM configuration, so it 403s on the Serverless Database tier. 403 on the Serverless tier (this route calls the built-in LLM).

Parameters

NameInTypeDescription
providerquerystringProvider whose catalog to return (e.g. openrouter). Defaults to the configured provider.

Response 200

{
  "provider": "openrouter",
  "models": [
    {
      "id": "z-ai/glm-5.2",
      "name": "GLM 5.2",
      "description": "",
      "context_length": 200000,
      "input_price_per_mtok": 0.3,
      "output_price_per_mtok": 1.1
    }
  ],
  "source_url": "https://openrouter.ai/api/v1/models"
}
GET/api/admin/query-configBearer SOAR + Serverless

Returns the unbounded-read row cap for this instance: the stored cap, the effective cap actually enforced by /sql (env MAX_QUERY_ROWS overrides the stored value), and whether an env override is masking the setting. This is a DATABASE feature, not AI-gated, so it works on the Serverless Database tier. Admin or superadmin required.

Response 200

{
  "ok": true,
  "maxRows": 1000,
  "effectiveMaxRows": 1000,
  "envOverride": false
}
PUT/api/admin/query-configBearer SOAR + Serverless

Sets the unbounded-read row cap (1..10,000,000) that bounds a single unpaginated SELECT via /sql, MCP, or the API; paginated reads (?page&pageSize) are unaffected. Live on the next read (no restart). Not AI-gated, so it works on the Serverless Database tier (cmgmt pushes the cap to serverless instances). Admin or superadmin required.

Request

{
  "maxRows": 5000
}

Response 200

{
  "ok": true,
  "maxRows": 5000,
  "effectiveMaxRows": 5000,
  "envOverride": false
}
GET/api/admin/google-oauth-configBearer SOAR + Serverless

Returns the effective Google OAuth client_id (safe to surface), whether a client secret is stored (never returned), the source of the effective values (db/env/none), and whether the OAuth flow is fully configured. Env vars (GOOGLE_OAUTH_CLIENT_ID/_SECRET, legacy GMAIL_OAUTH_*) are a fallback until a DB value is saved. Not AI-gated (OAuth connect works without the built-in LLM), so it works on the Serverless Database tier. Admin or superadmin required.

Response 200

{
  "client_id": "1234567890-abc.apps.googleusercontent.com",
  "has_client_secret": true,
  "source": "db",
  "configured": true
}
PUT/api/admin/google-oauth-configBearer SOAR + Serverless

Persists a fresh Google OAuth client_id/client_secret pair into the encrypted settings record; the OAuth flow picks it up on the next call. Each field is optional: omit to leave unchanged, empty string to clear the DB value and fall back to env, non-empty to overwrite. Returns the effective config. Not AI-gated, so it works on the Serverless Database tier. Admin or superadmin required.

Request

{
  "client_id": "1234567890-abc.apps.googleusercontent.com",
  "client_secret": "GOCSPX-..."
}

Response 200

{
  "client_id": "1234567890-abc.apps.googleusercontent.com",
  "has_client_secret": true,
  "source": "db",
  "configured": true
}
GET/api/admin/chorus/templatesBearer SOAR + Serverless

Returns every verified Chorus (semantic plan cache) template with per-shape usage metrics (hits, misses observed on the same shape, avg hit latency, last use) plus a per-namespace rollup with hit rate for the tenant view. Metrics are read/aggregated from _system.ChorusMetrics. Inspecting the cache does not invoke the built-in LLM, so it works on the Serverless Database tier. Admin or superadmin required.

Response 200

{
  "templates": [
    {
      "id": "cpc_01",
      "namespace": "pms",
      "act": "read",
      "planKind": "sql",
      "toolName": "",
      "shapeKey": "properties|city|status",
      "shapeHash": "a1b2c3",
      "expectedKind": "rows",
      "verifiedBy": "replay",
      "schemaFp": "fp_88",
      "createdAt": "2026-06-20T10:00:00Z",
      "hits": 42,
      "missesOnShape": 3,
      "avgHitMs": 12,
      "lastUsed": "2026-06-23T09:15:00Z"
    }
  ],
  "namespaces": [
    {
      "namespace": "pms",
      "templates": 1,
      "hits": 42,
      "misses": 3,
      "hitRatePct": 93.3
    }
  ],
  "total": 1
}
DELETE/api/admin/chorus/templatesBearer SOAR + Serverless

Clears the whole Chorus plan cache, or every template of one tenant namespace when ?namespace is given. Deletion is always safe: the next matching message simply misses to the agent and can re-admit through the replay gate. Does not invoke the built-in LLM, so it works on the Serverless Database tier. Admin or superadmin required.

Parameters

NameInTypeDescription
namespacequerystringOptional tenant namespace filter; omit to clear the entire cache.

Response 200

{
  "ok": true,
  "deleted": 12,
  "namespace": "pms"
}
DELETE/api/admin/chorus/templates/{id}Bearer SOAR + Serverless

Surgically removes a single Chorus plan-cache template by id from _system.ChorusPlanCache. Returns whether the template existed. Does not invoke the built-in LLM, so it works on the Serverless Database tier. Admin or superadmin required.

Parameters

NameInTypeDescription
idpathstringThe _id of the ChorusPlanCache template to delete.

Response 200

{
  "ok": true,
  "deleted": true
}
POST/api/admin/chorus/dry-runBearer SOAR only

Shows how Chorus reads a question: runs extraction + canonicalization only (one small extractor LLM call; no agent, nothing served or admitted, its own usage bucket). Powers the Songbook contrast/regression view. Because it makes a built-in extractor LLM call, it returns 403 on the Serverless Database tier. Admin or superadmin required. 403 on the Serverless tier (this route calls the built-in LLM).

Request

{
  "message": "show vacant properties in Austin over $2000 rent",
  "namespace": "pms"
}

Response 200

{
  "outcome": "skip",
  "reason": "slot outside domain",
  "shapeKey": "q|pms|properties|list:ord=:lim=false|c=[city:eq:str,market_rent:gt:num,status:eq:str]",
  "shapeHash": "786ab0a1330a2857"
}

No endpoints match your search.