Read rows from one of your tables
Filtered, ordered, paged row reads. A GET carries no body, so the structured arguments travel as strings:
columnsis a comma-separated list, e.g.id,farm_name. Omit it for all columns.filtersis a JSON array of predicates, e.g.[{"column":"status","op":"eq","value":"due"}]. Predicates are AND-joined; up to 20. Supported ops:eq,neq,gt,gte,lt,lte,like,ilike,is(istakes only null / true / false).
Pattern grammar. like and ilike take a SQL LIKE pattern: % matches any sequence of characters, _ matches exactly one. * is not a wildcard and a value containing one is 400 — it is refused rather than translated because the two database backends Paige runs on would disagree about it, and a filter must mean the same thing on every project. like and ilike are read-only: they are not accepted on the update or delete endpoints.
Null comparands belong to is. Only is may be given null; on every other op both null and the string "null" are 400 null_filter_value. Same reason as *: one backend reads the null as SQL NULL and matches every null row, while the other binds it as a parameter, where col = NULL matches nothing. Send {"column":"…","op":"is","value":null} to match rows whose column is NULL.
Every column you name — projection, filter or ordering — is checked against the columns the table actually has; an unknown one is 400 unknown_column.
Ordering defaults to the primary key, which makes paging stable. A table with no primary key and no explicit order_by is returned unordered — Postgres guarantees no row order there, so paging such a table can repeat or skip rows. Pass order_by on a table without a primary key.
Pagination follows the standard cursor convention: read pagination.hasMore and pass pagination.nextCursor straight back as cursor. Treat the cursor as opaque — never construct one.
A Paige platform table is 403 platform_table_forbidden; an unknown table is 404 table_not_found.
Only base tables are addressable. A view (or any other non-table relation) answers the same 404 table_not_found an unknown name does — including a view defined over a Paige platform table, which would otherwise be a writable alias for it.
Required scope: tables:read
Authorizations
Project API key issued in Settings → API keys. Send it as Authorization: Bearer pk_live_….
Headers
Target project id, for an MCP OAuth bearer (mcp_at_…) attached to more than one project — get ids from GET /v1/projects. Matched case-insensitively.
Omit it and a READ falls back to the connection's default project; a mutation (any non-GET) on a connection with 2+ projects is rejected with 400 project_required — a write is never defaulted to a guessed project. A connection with exactly one project never needs the header.
Scopes are checked against the SELECTED project only, never a union across the connection.
For a pk_ API key the header selects nothing — one key is one project's context — but it IS validated: omit it and the key's own project is used, send it and it must name that project, otherwise the call is rejected with 403 project_not_attached (a blank value is 400 invalid_project_header, as above).
Path Parameters
Table name. Lowercase letters, digits and underscores only. Paige platform tables are rejected.
^[a-z_][a-z0-9_]{0,62}$Query Parameters
Which database schema to target: "public" (production, the default) or "preview" (dev sandbox).
public, preview Comma-separated column names, e.g. "id,farm_name". Omit for all columns.
2000A JSON array of predicates, e.g. [{"column":"status","op":"eq","value":"due"}]. AND-joined, max 20.
4000Column to order by. Omit to order by the primary key. Every primary-key column is always appended as a tie-breaker, so paging stays stable even when this column has duplicate values.
^[a-z_][a-z0-9_]{0,62}$Order direction.
asc, desc Rows per page, 1-50. Default 20.
1 <= x <= 50Opaque nextCursor from a previous page. Do not construct one by hand.
200