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
unit"people" or "households""people"place"Lyon", "DE21", "Södermalm", "Sweden"codeother_matchescode_levelcode at, when it exists at severalpostcodenearlat, lonradius_kmradius_kmnearcountrywhere[]group_by[]metrics[]compare_to"scope"order_byfield, desccount, descendinglimitoffsetUse 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": truecounts 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.
opvalue=, !=<, <=, >, >=in, not inbetween[low, high]top_pctbottom_pcttop_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
"education""attribute": "age", "bins": [18, 30, 45, 65]"geo": "lau"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:
geocountrynuts2country, namenuts3country, namelaucountry, nameunitcountry, nameRegions 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:
opavgattributemedianattributequantileattribute, q between 0 and 1q: 0.9sumattributeratewhereEach 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:
base_countrate_pctcount / base_count × 100liftlift_shrunkbaseline 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
fieldsgroup_bygroupsgroup_bytotalcount and every metric over everyone countedpagegroup_byreturned, available, suppressed, offset, next_offsetscopelabel, level, population, and other_matches when the name was ambiguousbaselinecompare_tonotesunit, min_cell, data_vintageA 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
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.