Appearance
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 type | Operators |
|---|---|
| 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:
| Operator | What it does |
|---|---|
$like / $notLike | Case-sensitive SQL LIKE. Use % as wildcard. |
$iLike / $notILike | Case-insensitive SQL ILIKE. Use % as wildcard. |
$startsWith / $endsWith | Shortcut for $iLike with anchored wildcards. |
$fuzzy | PostgreSQL trigram similarity match. When present, results are auto-ranked by relevance. Good for "find anything like this". |
$is | Match 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 / $notBetween | Inclusive 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]=DESCfields — projection
Three forms:
json
{ "fields": "Id,ProjectName,BusinessState" } // CSV
{ "fields": ["Id", "ProjectName", "BusinessState"] } // array
{ "fields": { "exclude": ["LongDescription"] } } // denylistAll 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.
include — load related records
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/$andnesting $inlists 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.