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 pathWhat it does
GET /api/collections/{name}/recordsList records (paged)
POST /api/collections/{name}/recordsCreate; 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}/aggregateCount, sum, avg, min or max, grouped

List query parameters:

Parameter
filtere.g. status = "open" && priority > 2 (see below)
sortComma-separated fields, - for descending: -created,title
page, perPagePaging
searchFull-text search in searchable fields
asOfRead the collection as of a date, if it has history: 2026-06-30
near, onSemantic search: most similar first; on picks the fields
expandRelated 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 itstatus 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.

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=subject

History and the recycle bin

For collections with history enabled:

Method and path
GET …/records/{id}/historyAll versions of a record, newest first, with who and why
POST …/records/{id}/history/{seq}/restoreRestore a version
GET /api/collections/{name}/changes?since=NAll 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-refreshExchange a token for a fresh one
POST /api/collections/{name}/signupCreate an account, if sign-up is allowed
POST /api/collections/{name}/request-password-resetEmail 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}.