Skip to content

Query Shape ​

Every list endpoint on v2 accepts the same query parameters. You can send them as URL parameters on GET /api/v2/{resource} or as a JSON body on POST /api/v2/{resource}/query — the shape is identical.

The full vocabulary ​

jsonc
{
  "where":  { /* filter */ },
  "order":  [ ["FieldName", "ASC" | "DESC"], ... ],
  "fields": "Id,Name,DateCreated" | ["Id","Name"] | { "exclude": ["LongField"] },
  "scopes": [ "scopeName", ["parameterizedScope", { /* args */ }] ],
  "include": [ "RelationName", { "association": "Other", "where": {...} } ],
  "limit":  50,
  "offset": 0,
  "since":  "2026-01-01T00:00:00Z"
}

All keys are optional. Unknown keys produce a 400.


where — filtering ​

Keys are field names on the resource. Values can be either the literal to match, or an operator object.

Equality (implicit $eq):

json
{ "where": { "BusinessState": "Open" } }

Operator objects:

json
{ "where": {
    "OutstandingAmount": { "$gt": 0 },
    "DateCreated": { "$gte": "2026-01-01T00:00:00Z", "$lt": "2026-06-01T00:00:00Z" },
    "ClientId": { "$in": [42, 101, 7] },
    "ProjectName": { "$iLike": "%cabin%" }
} }

Operators by field type:

Field typeOperators
String$eq $ne $in $notIn $like $notLike $iLike $notILike $startsWith $endsWith $fuzzy $is
Number$eq $ne $in $notIn $gt $gte $lt $lte $between $notBetween $is
Date / timestamp$eq $ne $in $notIn $gt $gte $lt $lte $between $notBetween $is
Enum$eq $ne $in $notIn $is
Boolean$eq $ne $is

$nin is accepted as an alias for $notIn on every type — some clients prefer the short form.

A few operator notes:

OperatorWhat it does
$like / $notLikeCase-sensitive SQL LIKE. Use % as wildcard.
$iLike / $notILikeCase-insensitive SQL ILIKE. Use % as wildcard.
$startsWith / $endsWithShortcut for $iLike with anchored wildcards.
$fuzzyPostgreSQL trigram similarity match. When present, results are auto-ranked by relevance. Good for "find anything like this".
$isMatch NULL. Use as { FieldName: { $is: null } }. The Knowify wire format uses $is for both IS NULL and IS NOT NULL — combine with $ne for the latter.
$between / $notBetweenInclusive range. Value is a 2-element tuple [low, high].

Boolean composition:

json
{ "where": { "$or": [
    { "BusinessState": "Open" },
    { "BusinessState": "Draft" }
] } }

{ "where": { "$and": [
    { "OutstandingAmount": { "$gt": 0 } },
    { "DateModified": { "$gte": "2026-01-01T00:00:00Z" } }
] } }

Referencing related-record columns (when the resource declares an includes allowlist):

json
{ "include": ["Client"], "where": { "$Client.Country$": "USA" } }

The $Relation.Column$ form filters on a joined relation. Only relation names on the resource's allowlist are accepted.

Rejected keys → 400. Random field names, internal columns, or arbitrary strings produce a problem response. Type the field you mean to filter on.

order — sorting ​

Array of [field, direction] tuples. Field must be a listable field on the resource; direction is 'ASC' or 'DESC'.

json
{ "order": [["DateCreated", "DESC"]] }

{ "order": [
    ["BusinessState", "ASC"],
    ["DateCreated", "DESC"]
] }

URL form uses bracket notation:

?order[0][0]=DateCreated&order[0][1]=DESC

fields — projection ​

Three forms:

json
{ "fields": "Id,ProjectName,BusinessState" }           // CSV
{ "fields": ["Id", "ProjectName", "BusinessState"] }   // array
{ "fields": { "exclude": ["LongDescription"] } }       // denylist

All field names must be on the resource. Empty input returns the default projection.

scopes — predefined filters ​

Resources publish a set of named scopes. Each entry can be:

  • A bare name — "active"
  • A parameterized tuple — ["forResource", { "Id": 42 }] for scopes that take arguments
  • A composite — { "$or": ["active", "maintenance"] } to combine multiple scopes

Examples:

json
{ "scopes": ["active"] }
{ "scopes": [["forProject", { "Id": 102345 }], "overdue"] }
{ "scopes": [{ "$or": ["scheduled", "active"] }] }

The exact scopes available for each resource are listed in the scopes parameter docs on that endpoint. Resources without any scopes accept no value here.

Either a bare relation name, or an object configuring the join:

json
{ "include": ["Client"] }

{ "include": [
    { "association": "LineItems", "where": { "IsTax": false }, "limit": 50 }
] }

Only relations on the resource's allowlist are accepted.

Pagination ​

json
{ "limit": 50, "offset": 0 }

limit defaults to 50, maximum 200. offset defaults to 0.

Response includes meta.total, meta.has_more, and the echoed meta.limit / meta.offset so consumers can page without re-counting.

since — incremental sync ​

Filter to records modified at or after a timestamp:

json
{ "since": "2026-01-01T00:00:00Z" }

Equivalent to where: { DateModified: { $gte: "..." } } but expressed as a top-level parameter. If both since and where.DateModified are provided, the caller's where wins.


URL form vs. JSON body ​

GET /api/v2/{resource} and POST /api/v2/{resource}/query accept the same shape:

jsonc
// GET via qs bracket notation
?where[BusinessState]=Open&where[ClientId][$in]=42,101&limit=10

// POST body — equivalent, more readable for complex queries
{ "where": { "BusinessState": "Open", "ClientId": { "$in": [42, 101] } }, "limit": 10 }

Use POST /query when:

  • The query has deep $or / $and nesting
  • $in lists exceed reasonable URL length
  • You're generating queries programmatically
  • The set of fields, scopes, or includes makes the URL hard to read

Use GET for simple cases — it's cacheable, bookmarkable, log-friendly.