TotalBase
Data API

Reading

Read rows: choose fields and aliases, follow relations, search, sort, paginate and count.
GET /api/{schema}/table/{name}
GET /api/{schema}/view/{name}
GET /api/{schema}/table/{name}/{id}

A read needs READ on the resource. It returns only the rows your row filters allow and only the fields you may read.

Query parameters

ParameterDoes
fieldswhich columns and relation paths to return
wherewhich rows: Filtering
searchrows containing some words
search_fieldswhere search looks
order_bythe order
limit, offsetpagination
countalso return the total
flagscheck each row against named conditions
group_by, having, _sum, _count, _avg, _min, _maxAggregating

The order of parameters doesn't matter. The order inside a list (fields, order_by) does.

Fields

fields=number,status,due_at
fields=number,status,site.name,site.customer.name

Without fields, a read returns every column you may read, silently leaving out the others. Asking for a column you may not read is 403 FIELD_PERMISSION_DENIED.

Dotted paths follow foreign keys: see Tables › Relations. A path's output key is the path itself ("site.name"). To name the output, use aliases:

fields[number]=Order&fields[site.customer.name]=Customer&fields[due_at]=Due
{ "data": [ { "Order": "WO-000412", "Customer": "Mercado del Norte S.A.", "Due": "2026-10-15T12:00:00Z" } ] }

A request uses either the list form or the alias form, not both.

How values come back

Values are JSON, following each column's PostgreSQL type:

PostgreSQL typeJSONExample
integer, smallint, real, double precisionnumber42
bigint, numericstring, so no precision is lost"1250.50"
text, varchar, uuid, enumsstring"ASSIGNED"
booleantrue / falsetrue
datestring, the day as stored"2026-10-14"
timestamptzstring, ISO 8601 in UTC"2026-10-14T13:05:30.000Z"
timestring"09:30:00"
pointobject, x as longitude{ "lng": -65.4117, "lat": -24.7821 }
json, jsonbthe JSON value{ "plan": "PREMIUM" }
arraysarray["HVAC", "SOLAR"]
NULLnullnull

Writes take the same shapes: a point is written as { "lng": …, "lat": … }, and a numeric as a number or a string.

One row

GET /api/public/table/work_orders/8f1c2e0a-…
{ "data": { "id": "8f1c…", "number": "WO-000412", "status": "ASSIGNED" }, "meta": { "version": "48213" } }

A row that doesn't exist, or that your row filter hides, is 404 ROW_NOT_FOUND. meta.version (also sent as the ETag header) lets a later update make sure nobody changed the row in between. See Writing. Reading by key needs a single-column primary key, and takes no query parameters.

search=mercado salta
search=mercado&search_fields=name,site.name

search finds rows where every word appears in some field. By default it looks in the fields the request returns, or in exactly search_fields when given.

Field typeA word matches when
textthe field contains it, ignoring case and accents: perez finds "Pérez"
numberthe word is that exact number: 412 finds order 412, 41 doesn't
uuidthe word is the whole uuid
date, timestampthe word is that day, as YYYY-MM-DD
timethe word is that minute, as HH:mm

Other types (booleans, JSON, arrays) aren't searched. Fields you can't read are skipped. % and _ in a search are literal characters.

On a large table, search uses a LIKE '%word%', which a normal index can't serve. Add a trigram index on the expression TotalBase uses: CREATE INDEX ON customers USING gin (totalbase.search_normalize(name) gin_trgm_ops); (needs the pg_trgm extension).

Sorting

order_by=-priority,due_at,site.name

A field sorts ascending, and -field sorts descending. Without order_by, rows come in the resource's default order (set in Database › table › Columns › Order), then by primary key, so pages never overlap.

Pagination

limit=50&offset=100

limit defaults to 50 (api.default_limit) and is capped at 500 (api.max_limit). offset is capped at 100 000. Values over a cap are lowered to it, not rejected.

Counting

count=true adds the number of matching rows, before limit and offset:

{ "data": [ … ], "meta": { "count": 184 } }

The rows and the count come from the same snapshot. Without count=true, no count query runs.

Flags

flags checks each returned row against named conditions, without filtering anything. Each flag uses the where syntax:

flags[overdue][due_at][_lt]=$CURRENT_TIMESTAMP&flags[urgent][priority][_eq]=URGENT
{ "data": [ {…}, {…} ], "meta": { "flags": [ ["overdue", "urgent"], [] ] } }

meta.flags has one list per row, in the same order as data. PostgreSQL evaluates the conditions with the rows. DataViews use this for conditional formatting. At most 32 flags per request.

Reads and the audit log

Reads aren't audited by default. For sensitive tables, turn on Audit reads in the table's Auditing section. Every read then records who read, which fields, with which filter, and how many rows came back, but not the rows themselves.

The SQL it runs

This is the SQL TotalBase sent to PostgreSQL for the example above, as PostgreSQL's own log recorded it - ids shortened, long column lists cut with …. Every value travels as a parameter ($1), never inside the SQL text. A dispatcher reads urgent assigned orders with their site's name:

GET /api/public/table/work_orders?fields=number,status,site.name&where[status][_eq]=ASSIGNED&where[priority][_eq]=URGENT&order_by=-reported_at&limit=20
BEGIN;
SELECT set_config('statement_timeout', '30000', true);
SELECT t0."number" AS "number", t0."status" AS "status", t1."name" AS "site.name"
FROM "public"."work_orders" AS t0
LEFT JOIN "public"."sites" AS t1 ON t1."id" = t0."site"       -- site.name follows the foreign key
WHERE (t0."status" = $1 AND t0."priority" = $2)               -- the request's where
  AND t0."branch" = $3                                        -- AND the role's row filter: their branch
ORDER BY t0."reported_at" DESC
LIMIT $4 OFFSET $5;                                           -- $1 = 'ASSIGNED', $2 = 'URGENT', $3 = 'de01…', $4 = '20', $5 = '0'
COMMIT;

The row filter is added by TotalBase, never by the request, so no query string can widen it. An administrator's read has no such condition.

Next steps