Records API
The records API is the main surface for client apps. Every base/auth/view
collection exposes the same shape under /api/<collection_name>.
GET /api/<col>Query params:
| Param | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | 1-based |
perPage | int | 30 | max enforced server-side |
filter | expr | — | Rule expression language |
sort | string | -created_at | comma-sep; - prefix = DESC; aliases created/updated |
expand | string | — | comma-sep relation field paths; nested via . |
fields | string | — | comma-sep field whitelist for the response |
skipTotal | 1/true | false | omit count for faster paging |
nearVector | JSON number[] | — | Vector similarity search; pair with nearVectorField. |
nearVectorField | string | — | Vector field on this collection to search against. |
nearLimit | int | 10 | Top-K results to return (max 1000). |
nearMinScore | float | — | Drop matches with cosine similarity below this. |
Response:
{ "data": [ { "id": "...", ... } ], "page": 1, "perPage": 30, "totalItems": 42, // -1 if skipTotal "totalPages": 2 // -1 if skipTotal}Filter examples
Section titled “Filter examples”?filter=published=true?filter=author.id=u1 || author.id=u2?filter=title~"hello" && created>1730000000Quote string literals with double-quotes. Operators: = != > >= < <= ~
(substring), &&, ||, parentheses.
?sort=-created # newest first?sort=author,-created # by author asc, then newest first?sort=-updated # last-modified firstExpand
Section titled “Expand”Inline relation targets nested in the response under expand:
?expand=author # one hop?expand=author.profile # two hops?expand=author,comments # multiple{ "id": "p1", "title": "...", "author": "u1", "expand": { "author": { "id": "u1", "email": "...", "expand": { "profile": { ... } } } }}Field projection
Section titled “Field projection”?fields=id,titleReturns only id and title for each row. id is always included.
Vector similarity search
Section titled “Vector similarity search”?nearVector=[0.12,-0.05,...]&nearVectorField=embedding&nearLimit=10The candidate set is filtered by filter + list_rule first (so you can’t
read past your access scope), then re-ranked by cosine similarity. Each
returned row carries a _score field in [-1, 1]. v1 ranks in-process —
suitable up to ~50 K candidates. See Vector search.
Get one
Section titled “Get one”GET /api/<col>/<id>Returns { data: {...} } or 404. Responses carry a weak ETag header
derived from the record’s updated_at:
ETag: W/"1714502400"Pass it back as If-None-Match for a cheap conditional GET (server returns
304 Not Modified when nothing changed), or as If-Match on the next
PATCH / DELETE for optimistic concurrency control (see below).
Create
Section titled “Create”POST /api/<col>Content-Type: application/json
{ "title": "hello", "body": "world" }Returns the created record (with id, created, updated). Validation errors
return 422 with details: { fieldName: message }.
view collections return 405.
Update
Section titled “Update”PATCH /api/<col>/<id>Content-Type: application/json
{ "title": "new title" }Partial update — fields not in the body stay unchanged. view collections
return 405.
Delete
Section titled “Delete”DELETE /api/<col>/<id>Returns { data: null }. View collections → 405. Records referenced by
restrict-mode relations → 409 with details listing the blockers.
Optimistic concurrency (If-Match)
Section titled “Optimistic concurrency (If-Match)”PATCH / DELETE honour If-Match against the record’s current ETag:
PATCH /api/v1/posts/abc123If-Match: W/"1714502400"
{ "title": "new title" }- Match → proceed; response carries the new
ETag. - Mismatch →
412 Precondition Failedwith the server’s current ETag echoed in the response. Re-fetch, re-merge, and retry. If-Match: *matches any existing record (useful for “create-if-missing, block-if-changed” flows).
Both weak (W/"…") and strong-form ("…") tags compare under RFC 7232’s
weak compare semantics, so apps don’t have to track which prefix the server
sent.
The official SDK auto-attaches If-Match from a per-record cache after
every get / list / update. Disable per call with { ifMatch: false }
or override with an explicit string.
Record history
Section titled “Record history”When a collection has history_enabled: true, every write produces an
append-only audit row. See the Record history
concept page for the full overview.
List history
Section titled “List history”GET /api/<col>/<id>/history?page=1&perPage=50Returns the change log for one record. Gated by the parent record’s
view_rule — if you can view the live record (or the most recent snapshot
when it’s been deleted), you can read its history.
{ "data": { "data": [ { "id": "...", "op": "update", "snapshot": { ... }, "actor_id": "u-42", "actor_type": "user", "at": 1714502400 } ], "page": 1, "perPage": 50, "totalItems": 3, "totalPages": 1 }}404 when history is disabled on the collection.
Restore
Section titled “Restore”POST /api/<col>/<id>/restore?at=<unix-seconds>Admin-only. Restores the record to the most recent snapshot at or before
at. The live record’s updated_at advances and history records the restore
itself as a fresh update row.
409 when the record was deleted (v1 limitation — createRecord mints
its own id, so re-creating a deleted record with its original id is
unsupported).
Pass a Bearer token (user or admin):
Authorization: Bearer <jwt>@request.auth.id, @request.auth.email, @request.auth.type are then
available in API rules.
Status codes
Section titled “Status codes”| Code | When |
|---|---|
| 200 | OK (list, get, update, delete) |
| 201 | Created — POST only |
| 400 | Malformed request |
| 401 | Unauthorized (missing/invalid token where required) |
| 403 | Forbidden (rule failed) |
| 404 | Collection or record not found |
| 304 | If-None-Match ETag matched — body omitted |
| 405 | Write attempted on a view collection |
| 409 | Delete blocked by a restrict cascade · or restore-of-deleted record |
| 412 | If-Match precondition failed — response echoes the current ETag |
| 422 | Validation failed (details per field) |
| 429 | Rate limit exceeded |