Query API recipes
Each recipe is a complete query you can send as it is, then change to fit. They run on any account with Builder access, build on the overview, and use the fields described in the reference.
A small client
Two helpers cover everything below. In Python:
import os
import requests
API = "https://mcp.hiasynth.co/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['HIASYNTH_KEY']}"
def query(spec):
r = session.post(f"{API}/query", json=spec, timeout=100)
body = r.json()
if r.status_code != 200:
raise RuntimeError(f"{r.status_code} {body.get('error')}: {body.get('message')}")
return body
def query_all(spec):
"""Every group, following the pages."""
groups, offset = [], 0
while offset is not None:
body = query({**spec, "offset": offset, "limit": 1000})
groups += body.get("groups", [])
offset = body.get("page", {}).get("next_offset")
return groups
And in JavaScript or TypeScript:
const API = "https://mcp.hiasynth.co/v1";
export async function query(spec) {
const r = await fetch(`${API}/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HIASYNTH_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(spec),
});
const body = await r.json();
if (!r.ok) throw new Error(`${r.status} ${body.error}: ${body.message}`);
return body;
}
Keep the key on your server. The Hiasynth-Data-Version response header tells you which release answered, so a cache keyed on it stays correct across releases.
An either-or audience
Women in Copenhagen who are either studying or job-seeking, or under 25 and working, by education:
{
"place": "Copenhagen",
"where": [
{ "attribute": "sex", "op": "=", "value": "F" },
{ "any": [
{ "attribute": "weekly_schedule_profile", "op": "=", "value": "student_or_seeking" },
{ "all": [
{ "attribute": "age", "op": "<", "value": 25 },
{ "attribute": "employment", "op": "=", "value": "employed" }
]}
]}
],
"group_by": ["education"]
}
any is or, all is and, and they nest. Check a coded attribute's values in GET /v1/attributes?search=schedule before filtering on it; a value the attribute does not hold comes back as a 400 listing the valid ones.
A population pyramid with income
Everyone in Vienna aged 20 and over, by age band, sex and income quartile, with the median personal income in each group:
{
"place": "Vienna",
"where": [{ "attribute": "age", "op": ">=", "value": 20 }],
"group_by": [
{ "attribute": "age", "bins": [30, 40, 50, 60, 70] },
"sex",
"income_q"
],
"metrics": [{ "op": "median", "attribute": "person_net_income_tlc", "as": "median_income" }],
"order_by": { "field": "age", "desc": false },
"limit": 1000
}
That is 6 bands × 2 sexes × 4 quartiles, 48 groups in one call. median_income is in thousands of euros here, because Vienna is in one country.
Where an audience concentrates
Households with an electric car, as people, in every municipality of Bavaria, ranked by how concentrated they are:
{
"place": "Bavaria",
"where": [{ "attribute": "bev_owner", "op": "=", "value": true }],
"group_by": [{ "geo": "lau" }],
"compare_to": "scope",
"order_by": { "field": "lift_shrunk", "desc": true },
"limit": 25
}
Each group carries count (people in an EV home), base_count (everyone there), rate_pct and lift against Bavaria as a whole. Sorting by lift_shrunk keeps a 400-person village with three EVs from topping the list on luck. Sort by count instead for the biggest markets in absolute numbers.
People, by their home
Children under 15 in Lyon, by whether their home is owned or rented, with the average income decile of those homes and the share living in a flat:
{
"place": "Lyon",
"where": [{ "attribute": "age", "op": "<", "value": 15 }],
"group_by": ["tenure"],
"metrics": [
{ "op": "avg", "attribute": "hh_net_income_decile", "as": "avg_income_decile" },
{ "op": "rate",
"where": [{ "attribute": "dwelling_type", "op": "=", "value": "apartment" }],
"as": "in_apartment_pct" }
]
}
tenure, hh_net_income_decile and dwelling_type describe the home. Counting people, each child carries their home's values, so the counts are children and the averages are over children.
Spending and footprint
Among the fifth of Stockholm households that spend most on eating out, by tenure, with the median carbon footprint and the share of the budget that goes on eating out:
{
"unit": "households",
"place": "Stockholm",
"where": [{ "attribute": "spend_restaurants_cafes_tlc", "op": "top_pct", "value": 20 }],
"group_by": ["tenure"],
"metrics": [
{ "op": "median", "attribute": "co2_total_t", "as": "median_co2_t" },
{ "op": "avg", "attribute": "spend_restaurants_cafes_share", "as": "eating_out_budget_share" }
],
"compare_to": "scope"
}
top_pct cuts within Sweden, so the top fifth means the same thing anywhere you run it. With compare_to, rate_pct is the share of each kind of home that belongs to the big spenders, and lift shows which are over-represented compared with all Stockholm households.
A full map layer
The share of people aged 65 and over in every NUTS3 region of Germany, for a choropleth:
groups = query_all({
"place": "Germany",
"group_by": [{"geo": "nuts3"}],
"metrics": [{"op": "rate",
"where": [{"attribute": "age", "op": ">=", "value": 65}],
"as": "over_65_pct"}],
})
layer = {g["nuts3"]: g["over_65_pct"] for g in groups}
The nuts3 codes match the official NUTS 2021 boundaries, so layer joins straight onto a GISCO or Eurostat shapefile. For municipalities use "geo": "lau", and for the finest statistical units "geo": "unit" inside a city.
Percentiles within a country
Personal net income by education in the Netherlands, at the median, the 75th and the 90th percentile:
{
"country": "Netherlands",
"where": [{ "attribute": "age", "op": "between", "value": [25, 64] }],
"group_by": ["education"],
"metrics": [
{ "op": "median", "attribute": "person_net_income_tlc", "as": "p50" },
{ "op": "quantile", "attribute": "person_net_income_tlc", "q": 0.75, "as": "p75" },
{ "op": "quantile", "attribute": "person_net_income_tlc", "q": 0.9, "as": "p90" }
]
}
To compare income across countries, group by "geo": "country" and each country's figures come back in its own currency, or use income_q and hh_net_income_decile, which are national ladders.
A dashboard in one request
Three panels for one city, sent together:
const r = await fetch("https://mcp.hiasynth.co/v1/query/batch", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.HIASYNTH_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
queries: [
{ place: "Gothenburg", group_by: [{ attribute: "age", bins: [18, 30, 45, 65] }] },
{ place: "Gothenburg", unit: "households", group_by: ["tenure"] },
{ place: "Gothenburg", group_by: [{ geo: "unit" }], order_by: { field: "count" }, limit: 10 },
],
}),
});
const { results } = await r.json();
const [ages, homes, districts] = results.map((x) => x.body);
Each result carries its own status, so one panel's error leaves the others intact. Each query that answers is charged as one.