Query API reference

This page is the complete specification of POST /v1/query, which comes with Builder access. The overview shows a first query and the recipes show complete ones. The same schemas are in machine-readable form at GET /v1/openapi.json.

The request

Field
Type
Default
unit
"people" or "households"
"people"
What each count counts
place
string
A name or code: "Lyon", "DE21", "Södermalm", "Sweden"
code
string
An exact NUTS, LAU or city code, such as one from other_matches
code_level
string
The level to read code at, when it exists at several
postcode
string
A postal code (Finland so far)
near
lat, lon
A point; pair it with radius_km
radius_km
number, 0.5–100
5
The radius around near
country
string
Scopes to a country on its own, or picks between places with the same name
where
list of conditions
[]
Who to count
group_by
list of dimensions, up to 4
[]
How to break the count down
metrics
list of metrics, up to 12
[]
Numbers to compute for each group
compare_to
"scope"
Adds each group's population, rate and lift
order_by
field, desc
count, descending
How to sort the groups
limit
integer, 1–1000
100
Groups per page
offset
integer
0
Where the page starts

Use one way of naming the place per query: place, code, postcode, or near. country combines with any of them.

People, their homes, and one query

Every person in Humanity lives in a household, and a query reaches both. Attributes of people describe the individual: age, sex, education, employment, personal income, values, media use, subscriptions. Attributes of households describe the home they live in: tenure, dwelling, cars, household income and wealth deciles, and what the home spends on and emits.

Any attribute works in any part of a query, whichever you count. Counting people, each person carries the attributes of their home:

  • in where, "attribute": "bev_owner", "op": "=", "value": true counts the people living in homes with an electric car;
  • in group_by, "tenure" breaks people down by whether their home is owned or rented;
  • in metrics, "op": "avg", "attribute": "hh_net_income_decile" is the average household income decile of the people counted.

A query that combines household attributes with people needs a place, a country at minimum. Set "unit": "households" to count homes instead of people, and to sum a household attribute.

GET /v1/attributes lists every attribute with its level (person or household), group, meaning, values and provenance tier. Narrow it with ?search=electric, ?level=household or ?group=…. It is free, with or without Builder.

Conditions

A condition is a filter on one attribute, or a group of conditions.

{ "attribute": "age", "op": ">=", "value": 30 }
{ "any": [ condition, condition, ... ] }
{ "all": [ condition, condition, ... ] }

The top level of where is an all. Groups nest up to three deep, with up to 40 filters in total.

op
value
Matches
=, !=
one value
equal, not equal
<, <=, >, >=
one number
the comparison
in, not in
a list
one of the values, none of them
between
[low, high]
low to high, both included
top_pct
a percentage, 0–100
the top share of the country on this attribute
bottom_pct
a percentage, 0–100
the bottom share of the country on this attribute

top_pct and bottom_pct answer "a lot" and "a little" without knowing a currency: "attribute": "spend_restaurants_cafes_tlc", "op": "top_pct", "value": 20 is the fifth of households that spend most on eating out, cut within each country. They need a place, in up to 8 countries.

Coded attributes accept their stored values and common spellings. tenure takes "owner_mortgage" or "renter", sex takes "F" or "female", income_q takes "Q4" or "top". A value an attribute does not hold is refused with the list of valid ones.

Dimensions

Dimension
Example
Groups by
An attribute
"education"
each value of a categorical attribute
Bands
"attribute": "age", "bins": [18, 30, 45, 65]
the bands between the edges
Geography
"geo": "lau"
a geographic level

Bands take 1 to 20 ascending edges on a numeric attribute. Edges [18, 30, 45, 65] give five bands, labelled <18, 18-29, 30-44, 45-64 and 65+ on whole numbers, and 18 to <30 style on decimals. People with no value land in a null group. Sorting by a banded dimension follows the bands in order.

Geography takes one of these levels, once per query:

geo
Groups by
Extra fields
country
country
nuts2
NUTS2 region
country, name
nuts3
NUTS3 region
country, name
lau
municipality
country, name
unit
the finest statistical unit
country, name

Regions are assigned from each unit's geometry against the official NUTS 2021 boundaries, so Berlin is counted in Berlin. Where a country's municipality layer is still being completed, lau groups those units under their own unit code.

Geography is grouped through geo, which places every group on the map correctly; region codes and identifiers are refused as plain dimensions. A breakdown can hold up to 20,000 groups before paging. For more, use bands, a coarser level or a smaller place.

Metrics

count and share_pct come back for every group. Add more with metrics:

op
Fields
Returns
avg
attribute
the mean
median
attribute
the median
quantile
attribute, q between 0 and 1
that percentile, e.g. q: 0.9
sum
attribute
the total
rate
where
the percentage of the group meeting the condition

Each metric comes back under its as name, or a default: avg_age, median_age, p90_age, sum_n_cars, rate_1. Names are lower_snake_case, up to 48 characters.

A rate takes a list of conditions exactly like where, so "op": "rate", "where": [{"attribute": "bev_owner", "op": "=", "value": true], "as": "bev_pct"} is the percentage of each group living with an electric car.

Medians and percentiles are exact, so the same query always returns the same number. A numeric metric computed from fewer than 100 values in a group is null, and notes names it.

Lift against the place

"compare_to": "scope" counts the whole population of the place alongside the audience, and adds to every group and to the total:

Field
Is
base_count
everyone in the group, audience or not
rate_pct
the audience as a percentage of the group: count / base_count × 100
lift
the group's rate over the whole place's rate. 2.0 means twice as concentrated
lift_shrunk
the lift, pulled toward 1 for small groups

baseline names the comparison, such as "within Germany". Use lift_shrunk to rank many small areas: a group of 300 people with a lift of 4 is weaker evidence than a group of 30,000 with a lift of 2, and the shrunk lift weighs that for you. It adds the evidence of 1,000 people at the place's own rate before dividing.

Sorting and paging

order_by.field is any dimension, count, share_pct, a metric name, or with a baseline base_count, rate_pct, lift or lift_shrunk. Numbers sort descending by default, dimensions ascending; set desc to choose. Ties are broken by the dimensions, so pages are stable.

Read every group by following page.next_offset until it is null:

groups, offset = [], 0
while offset is not None:
    answer = query({**spec, "offset": offset, "limit": 1000})
    groups.extend(answer["groups"])
    offset = answer["page"]["next_offset"]

Each page is a query and is charged as one.

The response

Field
Present
Is
fields
with group_by
the names in each group, in order
groups
with group_by
one object per group
total
always
count and every metric over everyone counted
page
with group_by
returned, available, suppressed, offset, next_offset
scope
with a place
label, level, population, and other_matches when the name was ambiguous
baseline
with compare_to
what lift is measured against
notes
when relevant
what shaped the numbers
unit, min_cell, data_vintage
always
what was counted, the privacy floor, the data year

A query whose total lands under the floor answers with "suppressed": true, reason, observed and recovery. When nothing matched at all, valid_values may list the values a text attribute does hold in that place.

A special-category attribute crossed with fine geography answers "suppressed": true with "reason": "sensitive_geo_suppressed".

The data, in four details

Money is in local currency. Attributes ending in _tlc are thousands of the country's own currency: euros in Germany, kronor in Sweden. Averages and sums of them run within one country, or grouped by "geo": "country". To compare across countries use income_q (personal income quartile, Q1–Q4), hh_net_income_decile (national, 5.5 is the median), net_wealth_decile, a _share attribute, or top_pct.

income_q is a quartile, defined from age 20. There is no Q5, and a condition on it leaves out everyone younger. For children by income, use their household's hh_net_income_decile.

Values are z-scores. The ess_* attributes are standardised: 0 is the European average, +1 one standard deviation above. Average them freely; read them as relative.

Special categories stay coarse. Health, religion, sexual orientation and political attributes work in a place as small as a NUTS2 region, and broken down by country. With a smaller place or a finer geographic breakdown they answer suppressed. Every special-category attribute is marked sensitive in /v1/attributes.

Limits

Filters per query
40, nested up to 3 deep
Dimensions
4, one of them geography
Bands per dimension
20 edges
Metrics
12
Groups per page
1,000
Groups per breakdown
20,000
Queries per batch
10
Privacy floor
100 people per group, and per metric value
Time per query
90 seconds

Batches

POST /v1/query/batch takes "queries": [spec, spec, ...], up to 10, and answers "results": [{"status", "body", ...]} in the order sent. Each query runs and is charged on its own, so one invalid spec fails alone with its own 400 in its slot. Batches suit a dashboard that needs several panels at once.