REST API
Every collection gets a REST API as soon as it exists. The browser client (sluurp.js) is a thin wrapper, so anything it does can be done with plain HTTP.
Authenticate with Authorization: Bearer <token> (from sign-in); without it, requests are anonymous. Either way, the collection’s rules decide what’s allowed.
Records
| Method and path | What it does |
|---|---|
GET /api/collections/{name}/records | List records (paged) |
POST /api/collections/{name}/records | Create; returns 201 and the record |
GET /api/collections/{name}/records/{id} | Get one record |
PATCH /api/collections/{name}/records/{id} | Update only the given fields |
DELETE /api/collections/{name}/records/{id} | Delete (to the recycle bin); returns { "bin": … } |
GET /api/collections/{name}/aggregate | Count, sum, avg, min or max, grouped |
List query parameters:
| Parameter | |
|---|---|
filter | e.g. status = "open" && priority > 2 (see below) |
sort | Comma-separated fields, - for descending: -created,title |
page, perPage | Paging |
search | Full-text search in searchable fields |
asOf | Read the collection as of a date, if it has history: 2026-06-30 |
near, on | Semantic search: most similar first; on picks the fields |
expand | Related records embedded: expand=pupil,pupil.class (see below) |
GET /api/collections/grades/records?filter=value%20%3E%3D%208&sort=-date&perPage=20
Authorization: Bearer eyJ…The response contains items, page, perPage, totalItems and totalPages.
Filters
Filters are a small expression language compiled to SQL; they never reach the database as raw text. Field names are validated against the collection, and values are always bound parameters.
| Compare | = != > >= < <= |
| Contains / doesn’t contain | ~ !~: title ~ "trip" |
| Starts / ends with | ^= $=: title ^= "Re:", file $= ".pdf" |
| In a list / not in it | status in ("open", "waiting"), status not in ("done") |
| Any of a list field’s values | ?= ?!= ?~: tags ?= "urgent" (a JSON array; a plain value counts as a list of one) |
| Combine | && ||, parentheses, and !(…) to negate a group |
| Values | "text" or 'text', numbers, true, false, null |
status in ("open", "waiting") && priority >= 2 && !(title ~ "test" || tags ?= "draft")Through relations
A dot follows a relation field to a field of the record it points at, up to four relations deep:
pupil.class.name = "4A" && pupil.first_name ^= "Li"It compiles to a subquery, not one query per row. Each related collection’s list rule applies as it would to the caller listing it directly: a path through records they can’t list finds nothing, and a path can’t be used to probe fields they can’t see. Paths work in filter for lists, /aggregate and deleteMany.
OpenAPI
GET /api/openapi.json describes the API as it is now, as an OpenAPI 3.1 document:
- every collection’s endpoints, with its fields as a schema and its rules written out;
- signing in;
- the app’s own functions, with their method and rule.
It’s generated from the live schema, so it can’t drift. It’s for superusers, since it describes every collection. Point a client generator at it for a typed client in any language; see OpenAPI and client generators.
The admin UI’s API screen reads it. Pick an endpoint, fill in its parameters and body, and send it as yourself: you see the status, the time and the response, and can copy the request as curl.
Related records
expand embeds the records a relation field points at, under expand, next to the ids. Dotted paths go further, up to four relations deep:
GET /api/collections/grades/records?expand=pupil,pupil.class{ "id": "…", "value": 9, "pupil": "u1",
"expand": { "pupil": { "id": "u1", "name": "Lisa", "class": "c4a",
"expand": { "class": { "id": "c4a", "name": "4A" } } } } }It costs one query per relation, not one per row. Related records go through their own collection’s list rule: one the caller couldn’t list directly is left out, and only its id is there. expand works on a single record too (/records/{id}?expand=pupil), and in the SDK: list({ expand }), getOne(id, { expand }), listAll({ expand }).
In a synced list, expanded rows stay current: when a related record changes, the rows showing it are sent again.
Search by meaning
near=museum trips returns the most similar rows first, each with a _score (1 = identical). It compares searchable fields, or those in on=title,body. Rules and filter apply as usual. See Search by meaning for details and embeddings setup.
const { items } = await sluurp.collection("notes").list({ near: "museum trips" });Counting and summing
GET /api/collections/{name}/aggregate takes op (count, sum, avg, min, max), field (except for count), group (one or more fields), plus filter and asOf as for lists:
GET /api/collections/grades/aggregate?op=avg&field=value&group=subjectHistory and the recycle bin
For collections with history enabled:
| Method and path | |
|---|---|
GET …/records/{id}/history | All versions of a record, newest first, with who and why |
POST …/records/{id}/history/{seq}/restore | Restore a version |
GET /api/collections/{name}/changes?since=N | All changes after sequence N, in order |
Pass a reason for any write in the X-Sluurp-Reason header; it’s stored with the version. Deleted records go to the recycle bin; POST /api/bin/{bin}/restore restores them along with anything the delete cascaded to.
Files
| Method and path | |
|---|---|
GET /api/files/{collection}/{id}/{field} | Download. For images, ?w=, ?h=, ?fit=, ?format= and ?q= resize and convert |
POST /api/files/{collection}/{id}/{field} | Upload, multipart with a part named file |
DELETE /api/files/{collection}/{id}/{field} | Delete |
Signing in
| Method and path | |
|---|---|
POST /api/collections/{name}/auth-with-password | { "identity", "password" } → token and record |
POST /api/collections/{name}/auth-refresh | Exchange a token for a fresh one |
POST /api/collections/{name}/signup | Create an account, if sign-up is allowed |
POST /api/collections/{name}/request-password-reset | Email a password reset link |
Errors
Errors are a status and a message: { "status": 404, "message": "record grades/x not found" }. 400 bad request, 401 not signed in, 403 denied by a rule, 404 not found (or not visible to you), 409 conflict, 429 rate limited (retry shortly).
And more
Transactions across writes: POST /api/batch. Server functions: GET or POST /api/fn/{name} (Server functions). Live data over WebSocket: Sync. External events: POST /api/events/{name}.