Skip to content

Pagination and filtering

Every list takes one where grammar, a sort and a keyset cursor, so a query pages the same way on every Interface and never skips a record.

Every list on api.sb returns one page at a time, newest first, behind a cursor. One where grammar filters it, and the same grammar selects Events for Watches.

A first query

paperbill.io is a Headless SaaS that sends and collects Invoices for freelancers. Its collections screen asks for unpaid Invoices over $500, oldest due date first:

Text
Which unpaid invoices are over $500? Oldest due date first.

The response is a list envelope: the page under invoices, the cursor in meta.after, and the next page in links.next.

The where grammar

A filter is one expression over the Noun's properties. Dotted paths reach into objects and one level into related records: total.amount, customer.country.

OperatorExample
=, !=status = open
>, >=, <, <=due < 2026-11-01
instatus in (open, overdue)
containsmemo contains "retainer"
= null, != nullpaidAt = null
and, or, not, parentheses(status = open or status = overdue) and total.amount > 500

Values take the property's type: money compares on amount in its own currency, and dates accept ISO 8601. A filter on a property the Noun does not have fails with invalid-input, naming the property.

The SDK accepts the string or an object. An object's keys join with and, and each value is either a literal or an operator map such as { gt: 500 }.

The grammar is the platform's one selector language. A Watch selector Invoice.paid where total.amount > 500 uses the same clause, so a filter tested on a list behaves identically on a live feed.

Sorting

sort takes a comma-separated list of properties. A leading minus sorts descending: sort=-total.amount,due. Without sort, a list returns newest first.

Every sort ends with the record id as a tiebreaker, added for you. Two Invoices due the same day always come back in the same order.

Cursors

Lists page by keyset. The cursor encodes the last record's position in the sort, not an offset, so a record written mid-scan never shifts later pages or appears twice.

To read the next page, pass after from the previous one, or follow links.next. A page without after is the last. Cursors are opaque and never expire, and each is bound to its query: reusing one with another where or sort returns invalid-input.

limit sets the page size: 20 by default, 100 at most. To walk a whole collection, the SDK iterates pages for you:

TypeScript
for await (const invoice of $.Invoice.list({ where: 'paidAt = null' }).all()) {
  await $.Invoice.collect({ invoice: invoice.$id })
}

Totals and facets

A list returns no count unless asked, because counting a large collection costs a full scan. Add total=true and meta.total holds the count. It is exact up to 10,000 matches; above that it is an estimate, and meta.totalExact is false.

facets returns a count per value for the properties you name, from the same snapshot as the total:

Terminal
curl -G https://api.sb/paperbill.io/invoices \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  --data-urlencode "where=paidAt = null" \
  --data-urlencode "total=true" \
  --data-urlencode "facets=status,customer.country"

meta.facets.status might read { "open": 41, "overdue": 9 }, summing to meta.total. A filter panel built on facets never shows a count the list disagrees with.

Shaping the result

fields trims each record to the properties named: fields=$id,total,due. $id and $type always come back. include embeds related records, and depth follows them further, as the envelope describes.

Agents pay for every token they read. A list call with fields and Accept: text/markdown returns the same page at a fraction of the size.

q matches words across a Noun's text properties, ranked by relevance: q=late fee. Add match=meaning to rank by meaning instead, so q=client refuses to pay finds Invoices whose notes say "disputed". Both combine with where, and both page with the same cursors.

Next

  • Envelope: the list shape, include and depth.
  • Webhooks: the same where clause, on a Watch.
  • Realtime: a query that stays open and streams its changes.