CLARA API
Hyper-local environmental intelligence, building by building. CLARA simulates how air quality, wind and heat behave between a city's buildings, at up to 1×1 m, and serves the results as a JSON API: annual statistics, live conditions, forecasts, reports and map layers.
What CLARA provides
One API, several products. A key can hold any combination of them.
| Product | What it answers | Endpoints |
|---|---|---|
| Historical statistics | What a full reference year looks like at this address: air quality, pedestrian wind comfort and thermal comfort, with ratings. | GET /v1/historical/stats |
| Real-time | Conditions right now, hour by hour. | GET /v1/realtime |
| Forecast | The coming hours, up to 48 hours ahead. | GET /v1/forecast |
| Reports | A complete Environmental Intelligence Report for an address, as HTML and PDF. | POST /v1/reports |
| Map | City-wide map layers as vector tiles, realtime and forecast. See the map guide, or skip the plumbing with the browser SDK. | GET /v1/map/{city} |
| Geocoding helper | Turns an address into coordinates for CLARA queries. Results may only be used to query CLARA, never stored or redistributed. | GET /v1/geocode |
API keys
Every data endpoint needs a key, sent as a bearer token. Ask for one
with the request form, or write to
support@clara.city.
Each key is scoped four ways: cities, products, metrics and horizons;
GET /v1/key shows all of it at any time.
There are two kinds, told apart by prefix:
| Prefix | Kind | Where it lives |
|---|---|---|
clara_sk_ |
Secret key | Your server only. Never ship it in a browser page, an app bundle or a public repository. Requests carrying a secret key from a browser are rejected. |
clara_pk_ |
Publishable key | Safe inside a browser page. Locked to the origins you register and limited in what it can do and how fast. |
https://example.com, https://www.example.com and
http://localhost:3000 are three separate entries. Tell us
every origin you will use, production, staging and your local dev port,
or your first day will be a wall of 403s that have nothing to do with
your code. Serve your test page over http, not from disk: a
file:// page sends Origin: null, which matches
nothing.
Send the key with either header, whichever fits your HTTP client:
Authorization: Bearer YOUR_CLARA_KEY
X-API-Key: YOUR_CLARA_KEY
Your first call
Start with GET /v1/key. It costs nothing, confirms your key
works and shows exactly what it can reach.
curl -sS https://api.clara.city/v1/key \
-H "Authorization: Bearer YOUR_CLARA_KEY"
import requests
r = requests.get(
"https://api.clara.city/v1/key",
headers={"Authorization": "Bearer YOUR_CLARA_KEY"},
timeout=30,
)
r.raise_for_status()
print(r.json())
The response is your key's scope:
{
"label": "your-company",
"type": "sk",
"cities": ["brussels"],
"products": ["geocode", "historical", "realtime", "forecast", "reports"],
"metrics": ["air", "wind", "thermal"],
"horizons": ["realtime", "forecast"],
"limits": { "rate_per_min": 100, "reports_per_day": 100 },
"origins": [],
"created": "2026-08-20",
"expires_at": null
}
Your first data call
Ask for the historical statistics of an address. Pass an
address directly, or geocode once and pass coordinates;
coordinates are the primary way and skip a geocoding step on every call.
curl -sS "https://api.clara.city/v1/historical/stats?lat=50.82068&lon=4.35821" \
-H "Authorization: Bearer YOUR_CLARA_KEY"
import requests
r = requests.get(
"https://api.clara.city/v1/historical/stats",
params={"lat": 50.82068, "lon": 4.35821},
headers={"Authorization": "Bearer YOUR_CLARA_KEY"},
timeout=300,
)
r.raise_for_status()
stats = r.json()
print(stats["air"]["rating"], stats["wind"]["rating"], stats["thermal"]["rating"])
X-Cache header
says which happened. Set your client timeout generously on the first
call.
Abridged response:
{
"city": { "id": "brussels", "name": "Brussels", "country": "BE" },
"location": { "address": null, "lat": 50.82068, "lon": 4.35821 },
"meta": {
"dataset": "brussels_extended",
"resolution_m": 8,
"reference_year": 2025,
"sampling": "nearest_outdoor",
"distance_m": 7.5,
"n_cells": 3,
"hours": 8760,
"generated_utc": "2026-08-20T09:12:04Z"
},
"air": { "rating": 7.3, "good_pct": 72.7, "dominant_pollutant": "NO2", "...": "..." },
"wind": { "rating": 8.6, "calm_pct": 96.2, "...": "..." },
"thermal": { "rating": 5.7, "tropical_nights": 8, "...": "..." }
}
Reading a response
Every data response opens the same way, so once you can read one, you can read them all.
| Block | Meaning |
|---|---|
city |
Which city answered. Present when the location falls inside a covered city; absent when a nationwide dataset answered. |
location |
The resolved address label (or null for raw
coordinates) and the coordinates actually used, rounded to five
decimals. |
meta |
What produced the answer: the dataset, its grid resolution in metres, the reference year, how the location was sampled and when the numbers were computed. Store it alongside any value you keep. |
After the envelope come the data blocks, air,
wind and thermal. Each carries a
rating, one number from 0 to 10 that is the headline answer,
plus the full statistics behind it. The
API reference documents every field.
Where CLARA works
CLARA covers a growing set of cities, each with its own products, and the list is itself an endpoint. No key needed:
curl -sS https://api.clara.city/v1/cities
Each entry carries the city identifier, its bounding box and its
products; GET /v1/cities/{city} adds the exact boundary
polygon as GeoJSON, ready to draw or to pre-filter locations before
spending requests. With a key attached, each entry also says whether
your key is scoped to it.
In Belgium, historical statistics and reports go further: any Belgian
address is served. Inside the Brussels coverage zone the answer comes
from the hyper-local model, elsewhere from a lower-resolution nationwide
dataset; meta.dataset says which.
Scales and colours
Every classified value CLARA returns, air quality classes, thermal stress categories, wind comfort classes, follows a published scale with official labels, bounds and colours, served by the API so you never hardcode them:
curl -sS https://api.clara.city/v1/scales/air-belaqi
Among the scales: air-belaqi (the Belgian air quality
index, 1 to 10), thermal-utci (thermal stress categories)
and wind-comfort-standard (pedestrian wind comfort classes);
GET /v1/scales lists them all. Responses are
public and cacheable for an hour.
Errors
Errors are RFC 9457 problem documents, application/problem+json,
and every one carries a machine-readable code to branch on
and a request_id to quote when something needs
investigating:
{
"type": "https://www.clara.city/docs/errors#outside_coverage",
"code": "outside_coverage",
"title": "Outside coverage area",
"status": 422,
"detail": "this location is outside the CLARA service area",
"request_id": "7c1f4c2a-0f3e-4d2b-9a6e-5b8d0c1e2f3a"
}
One rule:
- A 403 is always about your key: deactivated, expired, or not scoped to that city or product. Fix it by talking to us, not by changing your request.
- A 404 or 422 is always about the data: the resource does not exist, the location is not covered, or the product does not exist there. Your key is fine.
| Code | Status | Meaning |
|---|---|---|
validation_error | 400 | A parameter is missing or malformed. |
unauthorized | 401 | The key is missing or unknown. |
forbidden | 403 | The key is valid but not entitled to this. |
not_found | 404 | No such resource. |
outside_coverage | 422 | The location is outside every covered area. |
product_unavailable | 422 | The city exists but does not have this product. |
rate_limited | 429 | Too many requests this minute; retry after Retry-After seconds. |
quota_exceeded | 429 | The daily report quota is used up; it resets at midnight UTC. |
upstream_error | 502 | Something behind the API failed; retry. |
upstream_timeout | 504 | The computation did not finish in time; retry. |
Rate limits
Limits are per key: a per-minute request limit and a daily report quota,
both visible in GET /v1/key. Every authenticated response
reports where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute. |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | When the next minute window starts, epoch seconds. |
Retry-After | On a 429: seconds to wait before retrying. |
If the defaults do not fit, ask: limits are adjustable per key.