CLARA API
Street-level environmental intelligence, per address. CLARA simulates how air quality, wind and heat behave between the buildings of a city, modelled at up to 1×1 m, and serves the results as a clean JSON API: annual statistics, live conditions, forecasts, generated 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, roughly two days 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, live 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. A helper, not a standalone service: results may only be used to query CLARA and must not be stored or redistributed. | GET /v1/geocode |
API keys
Every data endpoint needs a key, sent as a bearer token. Keys are issued
by the CLARA team: write to
[email protected] to get yours. Each key is scoped to the cities and products you use, and
GET /v1/key shows you that scope 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 of integration will be a wall of 403s that has nothing
to do with your code. And serve your test page over http rather than
opening it 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 the key can reach, so you never have to
guess what you were given.
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"],
"limits": { "rate_per_min": 100, "reports_per_day": 100 },
"created": "2026-08-20",
"expires_at": null
}
Your first data call
Ask for the historical statistics of an address. You can pass an
address directly, or geocode first 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. Keep it next to any value you store, so you always know what you are holding. |
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 which
products it has; GET /v1/cities/{city} adds the exact
boundary polygon as GeoJSON, ready to draw on a map or to pre-filter
your 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 street-level model; elsewhere it comes from a lower-resolution
nationwide dataset, and meta.dataset tells you which one
you got.
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. The scales are served by the API so you never hardcode them:
curl -sS https://api.clara.city/v1/scales/air-belaqi
Available scales: belaqi (the Belgian air quality index,
1 to 10), utci (thermal stress categories) and
wind (pedestrian wind comfort classes). 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 makes them easy to reason about:
- 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 your integration, ask: limits are per key and adjustable.