Field Filters

Filter list results by attribute using attribute__filtertype=value.

The Platform API lets you filter GET list results on most object attributes. Filters are added as query parameters and can be combined freely.

Filter syntax

For every filter except an exact match, the format is:

<attribute>__<filtertype>=<value>

A bare <attribute>=<value> performs an exact match. Examples:

  • id__in=6865acf980bcac000f4ca5ec,6865acef80bcac000f4ca5db
  • tags__contains=pharma
  • createdAt__date__gt=2026-01-01
🚧

There is no __exact suffix

Use the bare form. label__exact=temperature is not a recognised filter, so it is ignored and the endpoint returns every object in the collection — 215 variables instead of the 15 you asked for. Only label=temperature performs an exact match. The same applies to id__exact, unit__exact and createdAt__exact.

Negating a filter

Any filter can be negated by using != instead of = — handy for excluding results.

FilterReturns
tags__contains=pharmaDevices tagged pharma
tags__contains!=pharmaDevices not tagged pharma
lastActivity__gt=1700000000000Active since that time
lastActivity__gt!=1700000000000Not active since that time

Filter types by attribute

The filters available depend on the attribute's data type.

String — label, name, description, unit

iexact (case-insensitive) · contains · icontains · startswith · istartswith · endswith · iendswith · in · isnull

# Temperature variables whose label contains "temp" (case-insensitive)
curl -X GET 'https://api.surfact.com/api/v2.0/variables/?label__icontains=temp' \
  -H 'X-Auth-Token: your_token_here'

Enum — type

Bare form only: type=raw or type=synthetic. type takes no __ suffix — type__iexact, type__icontains and type__in are all ignored and return every variable.

# Synthetic (derived) variables only
curl -X GET 'https://api.surfact.com/api/v2.0/variables/?type=synthetic' \
  -H 'X-Auth-Token: your_token_here'

Number — e.g. lastActivity, lastTriggered

range · gt (>) · gte () · lt (<) · lte () · isnull

# Events triggered at or after a given POSIX timestamp (ms)
curl -X GET 'https://api.surfact.com/api/v2.0/events/?lastTriggered__gte=1583248038207' \
  -H 'X-Auth-Token: your_token_here'

# lastActivity within a range
curl -X GET 'https://api.surfact.com/api/v2.0/devices/?lastActivity__range=1700000000000,1700600000000' \
  -H 'X-Auth-Token: your_token_here'

isnull is not recognised on lastTriggered — both lastTriggered__isnull=true and lastTriggered__isnull=false return every event. Use lastTriggered__gt=0 for alerts that have fired, and negate it (lastTriggered__gt!=0) for those that never have. isnull does work on lastActivity.

Date — createdAt

date · year · quarter · month · week · day · hour · minute · second · isnull

createdAt is an ISO 8601 string (for example 2026-08-12T13:39:08.353792Z), so a Number filter applied to it directly is not recognised — createdAt__gt=1700000000000 is ignored and returns every object. Extract a date component first, then chain a filter onto it, because the extracted component is numeric:

# Devices created after May 20, 2026
curl -X GET 'https://api.surfact.com/api/v2.0/devices/?createdAt__date__gt=2026-05-20' \
  -H 'X-Auth-Token: your_token_here'

# Devices created in Q1
curl -X GET 'https://api.surfact.com/api/v2.0/devices/?createdAt__quarter=1' \
  -H 'X-Auth-Token: your_token_here'
🚧

Use the singular form

Time-frame filters are singular: day__range is correct, days__range is not.

ID — id

in — plus the bare id=<value> form for a single object

# Fetch two specific trackers by id
curl -X GET 'https://api.surfact.com/api/v2.0/devices/?id__in=6865acf980bcac000f4ca5ec,6865acef80bcac000f4ca5db' \
  -H 'X-Auth-Token: your_token_here'

Tags (string list) — tags

contains · contained_by · overlap · len · isnull

# All devices tagged for a customer's pharma cold chain
curl -X GET 'https://api.surfact.com/api/v2.0/devices/?tags__contains=pharma' \
  -H 'X-Auth-Token: your_token_here'

Properties (object) — properties

contains · contained_by · has_key · has_any_keys · has_keys · isnull

Every properties__* filter except isnull takes a JSON-encoded value, URL-encoded into the query string: has_key a JSON string, has_any_keys and has_keys a JSON array, contains and contained_by a JSON object. isnull takes a plain true or false. A bare word or a comma-separated list is rejected with 400 and {"code": 400001, "message": "Validation Error.", "detail": {"properties__has_key": ["Enter a valid JSON."]}}.

# Devices that carry a calibration certificate property
# The value is the JSON string "certificate", URL-encoded as %22certificate%22
curl -X GET 'https://api.surfact.com/api/v2.0/devices/?properties__has_key=%22certificate%22' \
  -H 'X-Auth-Token: your_token_here'

# The list operators take a JSON array — ["certificate"] URL-encoded
curl -X GET 'https://api.surfact.com/api/v2.0/devices/?properties__has_keys=%5B%22certificate%22%5D' \
  -H 'X-Auth-Token: your_token_here'
📘

Unrecognised filters are ignored — unparseable values are not

An unrecognised filter name or suffix is silently dropped and the endpoint returns all objects, so a filter that "isn't working" is usually a typo in the attribute name, the __ separator, or a suffix the attribute does not support.

A recognised filter carrying a value it cannot parse fails loudly instead: 400 with {"code": 400001, "message": "Validation Error."} and a detail naming the parameter — for example lastActivity__gt=notanumber returns "Enter a number.", createdAt__date__gt=notadate returns "Enter a valid date.", and lastActivity__range=1,2,3 returns "Range query expects two values.". See the FAQ.

Combine filters with Sorting, Pagination, and Dynamic Fields to build precise queries.


Did this page help you?