---
title: REST API
description: The HTTP API every collection gets, with its filters, sorting, paging and errors.
section: Data
order: 4
---

# REST API

<p class="lead">Every collection gets a REST API as soon as it exists. The browser client (<code>sluurp.js</code>) is a thin wrapper, so anything it does can be done with plain HTTP.</p>

Authenticate with `Authorization: Bearer <token>` (from sign-in); without it, requests are anonymous. Either way, the collection's [rules](/docs/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](/docs/history): `2026-06-30` |
| `near`, `on` | [Semantic search](/docs/search): most similar first; `on` picks the fields |
| `expand` | Related records embedded: `expand=pupil,pupil.class` (see below) |

```http title="HTTP"
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` |

```text title="filter"
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:

```text title="filter"
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](/docs/openapi).

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:

```http title="HTTP"
GET /api/collections/grades/records?expand=pupil,pupil.class
```

```json
{ "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](/docs/sync), 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](/docs/search) for details and embeddings setup.

```js title="client"
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:

```http title="HTTP"
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}/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`](/docs/batch). Server functions: `GET` or `POST /api/fn/{name}` ([Server functions](/docs/server-functions)). Live data over WebSocket: [Sync](/docs/sync). External events: [`POST /api/events/{name}`](/docs/hooks-and-events).
