Skip to Content
ConnectItems API

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/:collection

Query Parameters

ParameterDescriptionExample
fieldsComma-separated field names to return?fields=id,title,author
filterFilter rules (see Filter Rules)?filter[status][_eq]=published
sortField to sort by. Prefix with - for descending?sort=-created_at
limitNumber of items to return (default 25). limit=-1 disables the limit and returns all rows, but must be combined with page=0limit=-1 with the default page=1 returns an empty page?limit=20
pagePage number for pagination (default 1)?page=2
searchSubstring search (ILIKE) across string/text fields; json/jsonb columns only when opted in via meta.searchable — see Search behaviour?search=hello
countHow 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
aggregateAggregate functions?aggregate[count]=*
groupByGroup results by one or more fields (comma-separated). Requires aggregate?groupBy=status&aggregate[count]=*

Example

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/:id
curl "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.searchableEffect
unset (default)String and text fields are searched; json/jsonb fields are not
trueForce-includes a json/jsonb field. No effect on other non-text types (uuid, integer, date, boolean), which are never searched
falseNever 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/:collection
curl -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/:id

Creates 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/:id

Only 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/:id

Returns 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.name
  • author.name — M2O: the author’s name
  • tags.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:

CodeMeaning
400Bad request (invalid parameters, missing required filter)
401Not authenticated
403Forbidden (insufficient permissions)
404Item or collection not found
500Internal 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

MethodBehavior
PATCHPartial update — only the fields in the request body are changed
PUTUpsert — creates the item if it doesn’t exist, otherwise updates the supplied fields (partial, like PATCH)
Last updated on