Skip to content

Filtering

POST …/records/query/ and POST …/records/aggregate/ take a filter tree built from three node types: $and, $or, and a leaf.

A tree is scoped to the one object type in the URL, so every property in it must belong to that type. This example queries ahoy_company:

{
"filter": {
"$and": [
{ "property": "ahoy_industry", "operator": "eq", "value": "Software" },
{ "$or": [
{ "property": "ahoy_country", "operator": "in", "value": ["Ireland", "United Kingdom"] },
{ "property": "ahoy_annual_revenue", "operator": "gte", "value": "50000" }
]}
]
},
"sort": { "property": "ahoy_created_at", "direction": "desc" },
"properties": ["ahoy_name", "ahoy_domain", "ahoy_annual_revenue"]
}

Operators and values are validated against each property’s field type — not accepted blindly and failed later. A bare number against a currency property, or an unparseable date, is rejected as request_invalid with reason: "invalid_filter" and a field pointer to the offending node.

GET /rest/v1/field_types/ is the authority on what each type supports: its value shape, valid operators, available aggregations, and whether it can be sorted on.

Related failures:

reasonCause
unknown_propertyNo such property on this object type
operator_not_supportedOperator invalid for that property’s field type
invalid_filterValue didn’t match the field type
unsupported_sortThat property can’t be sorted on
unsupported_aggregationThat aggregation isn’t available for the field type

The tree is capped, deliberately, so one query can’t degrade the service:

  • Nesting depth: 8. Deeper returns reason: "filter_too_deep".
  • in / not_in lists: 100 values. Longer returns reason: "invalid_filter".
  • Group count is capped. Too many $and/$or groups returns reason: "too_many_filter_groups".

A structurally malformed tree — an unknown key, an empty $and, a missing value, an operator outside the vocabulary — is a 400. A tree that is well-formed but semantically wrong is a 422. Both carry request_invalid.

search runs a free-text match alongside the filter tree, and composes with it:

{ "search": "acme", "filter": { "property": "ahoy_industry", "operator": "eq", "value": "Software" } }

Sorting or filtering on a currency property requires exchange rates for that day. On the rare occasions they aren’t available, the request returns service_unavailable with reason: "exchange_rate_unavailable" and a Retry-After header. Retry rather than treating it as a bad request.