Skip to main content
The json:api specification leaves a lot of room for the implementation detail of these features and you can see some variation between different API’s that follow the same specification. Each section below will detail how we have implemented these features in the Ctrl Hub API to be as consistent as possible.

Document Level Meta

Before we go into each feature, we want to provide you, as the API client, with as much information about the possible features for the endpoint you are calling. This information is provided in the meta field of the response, under the features key. Let’s consider the following example as we step through the features:
Returns:
The meta.features object provides you with the possible query parameters you can use for the endpoint. The meta.features.params key indicates that these are query parameters. For each param, the available feature is the key and within each one, the options key provides you with the possible field names that you can use. We’ll go through each param now.

Sorting

When returning a collection of resources, we can sort the results based on a field. In our example, we can see that we could choose to sort on the name, created_at or updated_at fields. To do so, you can add the sort query parameter to the request: https://api.ctrl-hub.com/v3/operations?sort=name If you would like to reverse the order, you can add a - to the field name: https://api.ctrl-hub.com/v3/operations?sort=-name
When specifying multiple sort fields, the API will only sort by the first field. Whilst it is possible to send a request that has ?sort=name,created_at and for the API to return a response, the API will only sort by the first field, so the request is the equivalent of ?sort=name.We may change this behaviour in the future, so it is recommended to only specify one sort field for now.

Filtering

When returning a collection of resources, you can narrow the results with the filter query parameter. Ctrl Hub uses a compact, function-style filter syntax. A single filter is written as operator(field,value), and the whole expression is passed in one filter parameter. For example, to filter operations by their status: https://api.ctrl-hub.com/v3/operations?filter=eq(status,in_progress) This performs an exact match, so you will get every operation whose status is exactly in_progress, within the document data. Filtering on a unique field (such as an id) would return a single resource, still inside the data array.
Values are not quoted. Write eq(status,in_progress), not eq(status,"in_progress") - any quotes are treated as part of the value. Because the syntax uses (, ), , and [], remember to URL-encode the filter value if your HTTP client does not do it for you.

Operators

The range operators are ge and le (greater/less than or equal), not gte / lte.

Matching multiple values

To match any one of several values, use in with a bracketed, comma-separated list: https://api.ctrl-hub.com/v3/operations?filter=in(status,[todo,in_progress]) This returns operations whose status is todo or in_progress.

Combining filters

To require several conditions at once, wrap them in and(...): https://api.ctrl-hub.com/v3/operations?filter=and(eq(status,in_progress),contains(name,survey)) This returns operations where both the status is in_progress and the name contains survey. Passing several comma-separated expressions at the top level is treated the same way, so filter=eq(status,in_progress),contains(name,survey) is equivalent. To match either condition, use or(...): https://api.ctrl-hub.com/v3/operations?filter=or(eq(status,todo),eq(status,in_progress)) And to negate a condition, wrap it in not(...): https://api.ctrl-hub.com/v3/operations?filter=not(eq(status,done))

Nested fields

Some resources expose nested fields, which you filter with a dotted path. For example, an operation’s scheduled dates live under dates.scheduled, so you can filter on the scheduled start: https://api.ctrl-hub.com/v3/operations?filter=ge(dates.scheduled.start,2025-06-01T00:00:00Z)
The fields (and nested paths) you can filter on differ per endpoint. Each endpoint documents its available fields on the filter query parameter in the API Reference.

Time Filters

Date and time fields accept RFC 3339 timestamps together with the range operators, so you can filter on a point in time or on a window. For example, to filter operations modified since a certain date: https://api.ctrl-hub.com/v3/operations?filter=ge(updated_at,2025-01-01T00:00:00Z) This will return all operations modified since the 1st of January 2025 until now. You could also filter on operations modified up to a certain date: https://api.ctrl-hub.com/v3/operations?filter=le(updated_at,2025-01-31T23:59:59Z) This will return all operations modified up to the 31st of January 2025. To filter on a range of dates (a window), combine both bounds with and: https://api.ctrl-hub.com/v3/operations?filter=and(ge(updated_at,2025-01-01T00:00:00Z),le(updated_at,2025-01-31T23:59:59Z)) This will return all operations modified between the 1st and 31st of January 2025.

Including Data

Requesting data to include is one of the most powerful features of the API. It allows you to request related resources in the same request, reducing the number of requests you need to make to get the data you need. GraphQL has a similar feature set, but the JSONAPI specification is more easily accessible and means you can avoid the complexity of GraphQL and it’s learning curve. Within JSONAPI, relationships are represented in each resource. Let’s consider an Operation. An operation belongs to a scheme, has an operation type, and is carried out by one or more assignees (the users doing the work). An Operation resource would look like this, with each relationship pointing at a related resource by its type and id:
If you wanted to include the related scheme in the response, you would add the include query parameter to the request: https://api.ctrl-hub.com/v3/operations/7c9e6679-7425-40de-944b-e07fc1f90ae7?include=scheme Which would return the scheme in the included array:
You now have the scheme included in the response. To pull back more than one relationship, comma-separate them. For example, to include both the scheme and the assignees (the users doing the work): https://api.ctrl-hub.com/v3/operations/7c9e6679-7425-40de-944b-e07fc1f90ae7?include=scheme,assignees Both now appear in the included array:
The relationships you can include differ per resource. The available values are listed on the include query parameter for each endpoint in the API Reference. For an operation you can include scheme, work_order, operation_type, assignees, properties, permits, streets, teams, appointment, forms, template and organisation.

Some Considerations

As you include more resources, three things will happen:
  1. The response will get larger, as you are including more data. This means there is more data to transfer over the wire, and more data for you to process.
  2. The response will get more complex, as you are including more relationships. This means you will have to navigate the response to get the data you need.
  3. The response will get slower, as you are including more data. This means the response time will increase as you include more data.
Therefore, you should only include the data you need, and not include data that you don’t need. This will keep the response size down, the response time down, and the complexity of the integration down.

Pagination

When requesting a collection of resources, we adopt the limit / offset approach. limit restricts the number of resources returned in the data of the response. If you request includes, this limit does not apply to the included data. offset is the number of resources to skip before returning the resources. For example, to get the first 10 operations:
To get the next 10 operations:
When requesting a collection of resources from the API, we will also add some meta data to the response to help you with pagination. This will be in the meta field of the response, under the pagination key:
This can be useful for building up a pagination component in your application, or for iterating through the pages of data. The counts key provides you with the total number of pages and resources, and the requested key provides you with the limit and offset you requested. meta.pagination.offsets.previous and meta.pagination.offsets.next provide you with the offset for the previous and next pages respectively. If you are on the first page, previous will be a null value. If you are on the last page, next will be a null value.
If you request a page that is out of bounds, the API will return a 200 status code and an empty data array. If you are iterating though pages, you should check the data array to see if it is empty, and if it is, you should stop iterating.
We may support cursors in the future, reach out if this is something you would like to see.