Items API
The Items API is the primary interface for reading and writing data in any collection. It supports full CRUD with filtering, sorting, pagination, field selection, aggregation, and deep relational reads.
GET /api/items/:collection
POST /api/items/:collection
GET /api/items/:collection/:id
PATCH /api/items/:collection/:id
PUT /api/items/:collection/:id
DELETE /api/items/:collection/:id
PATCH /api/items/:collection (batch update)
DELETE /api/items/:collection (batch delete)Replace :collection with the name of any table in your database (e.g., posts, products). Permissions are enforced per collection — a request will 403 if the user’s role doesn’t have the required permission.
List Items
GET /api/items/:collectionQuery Parameters
| Parameter | Description | Example |
|---|---|---|
fields | Comma-separated field names to return | ?fields=id,title,author |
filter | Filter rules (see Filter Rules) | ?filter[status][_eq]=published |
sort | Field to sort by. Prefix with - for descending | ?sort=-created_at |
limit | Number of items to return (default 25). limit=-1 disables the limit and returns all rows, but must be combined with page=0 — limit=-1 with the default page=1 returns an empty page | ?limit=20 |
page | Page number for pagination (default 1) | ?page=2 |
search | Substring search (ILIKE) across string/text fields; json/jsonb columns only when opted in via meta.searchable — see Search behaviour | ?search=hello |
count | How meta.total is produced: estimated (default — exact for small results, the planner’s estimate for large ones), planned (always the estimate), exact (always precise; visits every matching row, expensive on large collections) | ?count=exact |
aggregate | Aggregate functions | ?aggregate[count]=* |
groupBy | Group results by one or more fields (comma-separated). Requires aggregate | ?groupBy=status&aggregate[count]=* |
Example
curl
curl "https://your-domain.com/api/items/posts?fields=id,title,status&filter[status][_eq]=published&sort=-created_at&limit=10" \
-H "Authorization: Bearer <token>"Response:
{
"data": [
{ "id": 1, "title": "Hello World", "status": "published" },
{ "id": 2, "title": "Second Post", "status": "published" }
],
"meta": {
"total_count": 42,
"total": 42,
"page": 1,
"limit": 25
}
}By default meta.total is exact for small result sets and switches to the Postgres planner’s estimate on large ones. Pass ?count=exact when you need a precise total (at the cost of scanning every matching row).
Get Single Item
GET /api/items/:collection/:idcurl "https://your-domain.com/api/items/posts/1?fields=*,author.*" \
-H "Authorization: Bearer <token>"The fields=*,author.* pattern includes all top-level fields plus all fields from the related author record (deep read).
Pass ?version=<key> to retrieve the item with a specific content version’s delta merged in (e.g., ?version=draft).
Search behaviour
?search= performs case-insensitive partial matching (ILIKE) across all string and text fields in the collection.
Which fields participate is controlled per field by meta.searchable:
meta.searchable | Effect |
|---|---|
| unset (default) | String and text fields are searched; json/jsonb fields are not |
true | Force-includes a json/jsonb field. No effect on other non-text types (uuid, integer, date, boolean), which are never searched |
false | Never searched, whatever the type |
If every field on a collection ends up excluded, ?search= matches nothing. It does not fall back to returning all rows.
json/jsonb fields are excluded by default for performance. Searching one requires casting it to text, which forces Postgres to detoast and decompress the entire value for every row scanned. On a collection whose json averaged 57 KB per row this measured ~1.4 ms/row — about 17 seconds at 12,000 rows, past the statement timeout, so search failed with HTTP 500. Text columns on the same table cost ~0.06 ms/row. Opt a json field in only when its contents genuinely need to be searchable and the collection stays small.
For an opted-in json/jsonb field, search matches the serialised content of the column — keys and values. This is a coarse match: searching name will match any row that has a name key anywhere in the column, not just rows where that key equals a specific value. For precise per-key querying, store the value in a typed column instead.
Matches inside an opted-in json/jsonb field are resolved by a dedicated RPC that returns up to 150 matching rows by default (controlled by the JSONB_SEARCH_MATCH_LIMIT environment variable). Text-column matches on the same collection are matched natively and are not capped. The RPC also selects its rows before your filter is applied, so combining a filter with a term that matches only json content can under-report on very common terms. Increase the limit or use typed columns for high-cardinality datasets.
Searching an exact value is better served by a filter than by ?search=, since a filter can use an index while ?search= cannot:
# Fast — uses the b-tree index on record_id
curl "https://your-domain.com/api/items/records?filter[record_id][_eq]=R26071006217468" \
-H "Authorization: Bearer <token>"Create Item
POST /api/items/:collectioncurl -X POST "https://your-domain.com/api/items/posts" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"title": "New Post", "status": "draft", "body": "Hello!"}'Returns the created item with its generated id and status 201 Created.
Upsert Item
PUT /api/items/:collection/:idCreates the item if it doesn’t exist, or updates it if it does. When the item exists, PUT behaves like PATCH: only the fields present in the request body are changed — omitted fields keep their current values (they are not reset to null).
curl -X PUT "https://your-domain.com/api/items/posts/1" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title", "status": "published", "body": "Full content"}'Update Item (Partial)
PATCH /api/items/:collection/:idOnly the fields included in the request body are updated (partial update).
curl -X PATCH "https://your-domain.com/api/items/posts/1" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"status": "published"}'Delete Item
DELETE /api/items/:collection/:idReturns 200 with { data: { deleted: true, id } } on success.
Batch Operations
The filter parameter is required for batch update and batch delete. Omitting it returns 400 with { "error": "Filter parameter required for batch update" }.
Batch update
Update multiple items matching a filter. The filter query parameter must be JSON-encoded:
curl -X PATCH "https://your-domain.com/api/items/posts?filter=%7B%22status%22%3A%7B%22_eq%22%3A%22draft%22%7D%7D" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"status": "archived"}'Or pass the filter as a JSON string in the URL:
# URL-decoded for readability: ?filter={"status":{"_eq":"draft"}}
curl -X PATCH "https://your-domain.com/api/items/posts" \
-G --data-urlencode 'filter={"status":{"_eq":"draft"}}' \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"status": "archived"}'Batch delete
Delete multiple items matching a filter. The filter query parameter must be JSON-encoded:
curl -X DELETE "https://your-domain.com/api/items/posts" \
-G --data-urlencode 'filter={"status":{"_eq":"archived"}}' \
-H "Authorization: Bearer <token>"Batch update response (200):
{
"data": {
"updated": 3,
"keys": [1, 5, 12]
}
}Batch delete response (200):
{
"data": {
"deleted": 2,
"keys": [8, 15]
}
}The MAX_BATCH_MUTATION environment variable controls the maximum number of items that can be mutated in a single batch request (default: 100).
Aggregates
GET /api/items/posts?aggregate[count]=*&aggregate[avg]=views&groupBy=status{
"data": [
{ "count": { "*": 15 }, "avg": { "views": 243.6 }, "status": "published" },
{ "count": { "*": 8 }, "avg": { "views": 12.1 }, "status": "draft" }
]
}Supported functions: count, countDistinct, countAll, sum, sumDistinct, avg, avgDistinct, min, max.
count/countDistinct: Counts non-null values of a specific field (e.g.,?aggregate[count]=id).countAll: Counts all rows regardless of field values (e.g.,?aggregate[countAll]=*). Use*as the field argument.
Relational Data
Fetch related items inline using dot notation in fields:
?fields=id,title,author.name,author.email,tags.tag_id.nameauthor.name— M2O: the author’s nametags.tag_id.name— M2M junction: the tag name through the junction table
Use the * wildcard to include all fields from a related collection:
?fields=*,author.*This returns all top-level fields plus all fields from the author relation.
Error Responses
Errors return a JSON body with at least an error key, and some errors include a code field for programmatic handling:
{ "error": "Not authenticated" }
{ "error": "Item not found", "code": "NOT_FOUND" }Common HTTP status codes:
| Code | Meaning |
|---|---|
400 | Bad request (invalid parameters, missing required filter) |
401 | Not authenticated |
403 | Forbidden (insufficient permissions) |
404 | Item or collection not found |
500 | Internal server error |
Scope & Multi-tenancy
Every request is scoped to a resource URI via the X-Resource-Uri header or a cookie. This header controls which tenant/project the request operates within. Omitting the header/cookie targets the root scope: permissions resolve from root-scope assignments and public policies only. A resource URI that doesn’t exist returns 400 (INVALID_SCOPE); a valid URI where the user holds no policies returns 403 (FORBIDDEN_SCOPE).
Delegation
Service accounts can act on behalf of other users by setting the X-On-Behalf-Of header to the target user ID. This is useful for automation and background jobs.
Authentication
Requests can be authenticated using either:
- Bearer token:
Authorization: Bearer <token>header - Cookie-based auth: Browser clients with an active session cookie
Field-Level Permissions
Non-admin users receive filtered response objects — fields they don’t have permission to read are stripped from the response. Admins see all fields.
PATCH vs PUT
| Method | Behavior |
|---|---|
PATCH | Partial update — only the fields in the request body are changed |
PUT | Upsert — creates the item if it doesn’t exist, otherwise updates the supplied fields (partial, like PATCH) |