Skip to content

Search Queries

Every list endpoint speaks one query contract: the same parameters, the same operators, the same paging, and the same 400 errors. Learn it once here; an endpoint's page in the API reference lists which fields it filters and sorts on.

GET /api/v1/guilds?q=wyvern&filter[visibility][eq]=public&sort=-created_at&limit=2&total=true
ParameterDescription
filter[<field>][<operator>]A condition on one field, repeatable; all conditions apply
sortField to order by, - first for descending (sort=-created_at); some lists take several keys, comma-separated
limitRows per page; 25 by default, at most 100 unless the endpoint says otherwise
cursorResume after a page: the page.next_cursor of the previous response
pageJump to a 1-based page number instead of following cursors
totaltrue to count every matching row into page.total
qFull-text search, on lists that support it

Anything else in the query string is either one of the endpoint's own named flags (documented on its page) or ignored. The parameters of the previous grammar are refused rather than ignored, so a client learns the new name: filter=field:op:value, page_size, sort_by, sort_order, and offset all return 400 with the replacement in msg.

Filters

A filter names a field and an operator in brackets, with the value as the parameter value. Brackets are fine unencoded in most clients; encoded (filter%5Bvisibility%5D%5Beq%5D=public) is equally accepted.

OperatorMeaningExample
eqEqualsfilter[status][eq]=open
neDoes not equalfilter[status][ne]=closed
inEquals any of a comma-separated listfilter[status][in]=open,pending
allA tag list holds every value listedfilter[tags][all]=pvp,ranked
gt gte lt lteOrdered comparisonfilter[level][gte]=5
prefixText starts withfilter[path][prefix]=config/
containsEvery word occurs, any orderfilter[description][contains]=error
phraseThe words occur together, in orderfilter[name][phrase]=John Doe
fuzzyTypo-tolerant word matchfilter[name][fuzzy]=jonh
existsThe value is present (true) or absent (false)filter[data.error_code][exists]=false

Which operators a field accepts follows from its kind:

Field kindOperators
Identifier or enumerationeq, ne, in; some identifiers, such as paths, also prefix
Tag listeq, ne, in, all
Number or timestampeq, ne, in, gt, gte, lt, lte
Booleaneq, ne
Searchable textprefix, contains, phrase, fuzzy

Values:

  • Timestamps take RFC 3339 (2026-01-01T00:00:00Z), now, or a time before now: -30m, -6h, -7d. A relative time is resolved on every request, so filter[created_at][gte]=-24h keeps meaning "the last day" while you page.
  • Enumerations take only their listed values; anything else is a 400 naming the choices.
  • in lists split on commas; write \, for a literal comma and \\ for a backslash. At most 100 values.
  • Booleans are exactly true or false.
  • A request carries at most 20 filters, each value at most 200 characters, and each field and operator pair once.
  • Text matching follows the search index's words, not raw bytes: punctuation separates words, so phrase means word adjacency (a@b, a b, and a-b are the same phrase) and contains needs every word without caring about order. A value that must match as one opaque string is modelled as an identifier and filtered with eq.

A few lists also filter on keys inside free-form data, named <family>.<key>: timeline events on data.<key> and narrative actors on attributes.<key>. Their pages list the operators those families accept.

A field name used directly as a parameter (?visibility=public) is a 400 that tells you to write filter[visibility][eq]=public.

Sorting

sort=name orders ascending, sort=-name descending. Each list names the fields it sorts by and its default order; a sort on anything else is a 400 listing the choices.

Paging

Every response carries the rows and a page object:

"page": {
  "next_cursor": "eyJ2IjoxLCJzIjoiZmI1YzU2ZjM5ZTYyOTVkYyIsInAiOnsibyI6Mn19",
  "has_more": true,
  "total": 3
}
FieldPresent
next_cursorWhen another page follows; pass it back as cursor
has_moreAlways
totalWhen the request asked with total=true
pageWhen the request asked for a page number with page

Cursors are the default way to page: request the next page with the same parameters plus cursor. A cursor is bound to the list and the filters, sort, and search that produced it, so using it on another list or after changing any of them is a 400; start again without one. Cursors are opaque; do not build or edit them.

Page numbers (page=3) jump straight to a page. Use one or the other: cursor and page together is a 400. Some lists page by a position their store keeps (a stream id, a continuation token) and cannot jump to a page number; they refuse page and say so.

Counting is opt-in because it costs a full count: ask with total=true. Lists that cannot count without reading every row (filtered scans of a stream, rankings) refuse total with a 400.

q searches the entity's text fields: each word matches by prefix, so q=wyvern kni finds "Wyvern Knights". Combine it freely with filters and sort. Lists without text search refuse q.

The JSON body form

A few endpoints take a query language richer than query parameters and read it from a POST body; the ECS entity query is one. They use the same names for everything this contract covers: sort, limit, cursor, page, total, and return the same page object.

Four detail endpoints can embed related records (users, guilds, polls, and tournaments) with include. It keeps its own option syntax:

GET /api/v1/guilds/{guild_id}?include=members(limit:5,sort:-joined_at,filter:role:eq:admin)

Options are limit (100 by default, at most 1000), offset, one sort, and one filter as field:op:value with eq, bool, or range (min..max). Each endpoint's page lists the relations it embeds.

The CLI

Every CLI list command takes the same options:

snug guild list --filter 'visibility[eq]=public' --sort -created_at --limit 2 --total
snug guild list -q wyvern --all
snug guild list --limit 25 --page 3

--filter is repeatable and takes field[op]=value; --cursor resumes after a page; --all follows cursors to the last page and prints every row.

A real query

Guild service, envelope included:

GET /api/v1/guilds?q=wyvern&filter[visibility][eq]=public&sort=-created_at&limit=2&total=true
{
  "status": 200,
  "msg": "OK",
  "data": {
    "guilds": [
      { "guild_id": "KHtHtdynhuPPBqAJcJmP", "name": "Wyvern Hoard", "visibility": "public", "created_at": "2026-09-26T22:09:13.381179Z" },
      { "guild_id": "jRHERNbebDcdGNmdnWeQ", "name": "Wyvern Knights", "visibility": "public", "created_at": "2026-09-26T22:09:13.062394Z" }
    ],
    "page": {
      "next_cursor": "eyJ2IjoxLCJzIjoiZmI1YzU2ZjM5ZTYyOTVkYyIsInAiOnsibyI6Mn19",
      "has_more": true,
      "total": 3
    }
  }
}

The same request with &cursor=eyJ2Ijox... returns the last guild and "page": { "has_more": false }. The rows key is named after the entity (guilds, webhooks, ...) or is items; the page object is the same everywhere.

Errors

A query the list cannot answer returns 400 with the parameter at fault named first in msg - never a silently empty result:

msgCause
page_size is not a list parameter; use limitA retired parameter
visibility is a list field; filter it with filter[visibility][eq]=<value>A field used as a parameter
filter[name][eq] filters on an unknown field; this list accepts visibility, join_policy, ...Field not filterable here
filter[visibility][gt] is not supported; visibility accepts eq, ne, inOperator not allowed for the field's kind
filter[visibility][in] expects one of public, private, got "secret"Value outside an enumeration
filter[created_at][gte] expects an RFC 3339 timestamp such as 2026-01-01T00:00:00Z, now, or a time before now such as -30m, -6h or -7d; got "yesterday"Unreadable timestamp
sort cannot order by "owner"; this list sorts by name, member_count, ...Sort field not allowed
sort combines 2 keys; this list sorts by at most 1Too many sort keys
limit must be between 1 and 100Page size out of range
cursor belongs to a different list, filter, sort, or search; start again without a cursorCursor reused on another list or with changed parameters
cursor cannot be combined with page; use one or the otherBoth paging styles at once
total is not available on this list, which cannot count its rowsCounting a list that cannot count
page is not available on this list; follow page.next_cursor insteadPage number on a cursor-only list

Which fields each list filters and sorts on is documented on its endpoint's page in the API reference.