Usage

regionalatlas — a CLI for the Regionalatlas Deutschland (Statistische Ämter des Bundes und der Länder). No API key needed.

regionalatlas [global options] <command> [command options]

Global options

Option Description
--base-url <url> ArcGIS data host base URL (default: the REGIONALATLAS_BASE_URL environment variable, else https://www.gis-idmz.nrw.de)
--catalog-url <url> indicator catalogue URL (default the statistikportal.de services.json)
--timeout <ms> time limit per request in ms, whole response included (0 = no timeout; at most 2147483647)
--user-agent <ua> User-Agent header value (not blank; no control characters or characters above U+00FF)
--max-retries <n> retries for transient 429/503 responses and reset connections (0..10); a refused connection, a DNS failure and a timeout are not retried. Each retry waits a linear backoff (200 ms, 400 ms, …), or the server’s Retry-After when that is longer (capped at 30 s) — never less, so Retry-After: 0 still waits the backoff. Each retry logs one WARN record of regionalatlas.http before it waits (HTTP 503 from host: retry 1 of 3 in 2 s). When they run out, the error says how many were made (… (retried 2 times))
--max-response-bytes <n> cap the response body size in bytes (0 = unlimited; default 100 MiB). Every command reads the indicator catalogue (about 2 MB) first, so a cap below that fails even themes; the error says which download was too big
--compact print JSON on a single line (for piping to jq)
--log-format <format> how errors, warnings and notes are written to stderr: text (default; log4j style, 2026-10-09T14:03:12.481Z WARN [regionalatlas.http] …) or jsonl (one JSON object per line: ts, level, topic, msg). stdout is not affected
-V, --version / -h, --help version / help

Every option takes one value, except --fields, which is repeatable (--fields a --fields b is --fields a,b); giving any other option twice is a usage error (exit 2), where the last value used to win silently.

--base-url and --catalog-url accept only http:/https: URLs without whitespace (around or inside them). --base-url must not have a query (?) or fragment (#) (the CLI appends the data path to it); a path prefix for a mirror is fine. A user:password@ part is sent as Basic auth and shown as ***@ in everything the CLI prints — its own error messages, commander’s usage errors (which quote a rejected value), help — whatever characters the password contains. When a server echoes them back in an error, the Basic value and the decoded user:password are shown as *** too, and on stderr also the password alone. A % in the user name or password must start an escape (write a literal % as %25); a bare one is a usage error (exit 2). The library checks baseUrl and catalogUrl by the same rules when the client is built.

--catalog-url may carry a query, and a token (or access_token) parameter in it is treated as a credential: ArcGIS services take their access token as ?token=…, so a mirror behind such a login works as --catalog-url 'https://mirror.example/services.json?token=…'. The token is sent only with the catalogue request — never to the data host (--base-url takes no query), and redirects are not followed — and everything the CLI prints shows it as token=*** (in the data on stdout a bare token value is replaced only as a whole value, never inside other text). The public catalogue needs none.

Plain http:. A remote host on plain http: gets one warning record on stderr before the first request, per URL the command contacts (the catalogue URL always, the base URL for query); stdout and the exit code are unchanged, and loopback hosts (localhost, 127.x, ::1) don’t warn. It names what travels unencrypted, never its value:

2026-10-09T14:03:12.481Z WARN  [regionalatlas.http] requests to mirror.example are sent unencrypted (http:, not https:)
2026-10-09T14:03:12.481Z WARN  [regionalatlas.http] the base URL's credentials are sent unencrypted to mirror.example (http:, not https:)
2026-10-09T14:03:12.481Z WARN  [regionalatlas.http] the catalogue URL's token is sent unencrypted to cat.example (http:, not https:)

REGIONALATLAS_BASE_URL sets the data host’s base URL for every run (a mirror, a local fixture server); --base-url overrides it, and an empty variable counts as unset. Its value is checked by the same rules before any request: a bad one is a usage error (exit 2) that names the variable, not its value (ERROR [regionalatlas.cli] REGIONALATLAS_BASE_URL: Only http: and https: URLs are supported. Fix or unset the variable.). Help works whatever it holds, and a password in it is redacted like one in --base-url.

Commands

themes — list the subject areas

regionalatlas themes → [{ title, indicatorCount }, …] (the 21 Themenbereiche).

indicators — list indicators

Option Description
--theme <substr> filter by theme title (case-insensitive substring)
--year <yyyy> only indicators offering this year (in years; a few offered years have no figures at any level — their levels entry is [])
--search <substr> filter over code + short + long title (case-insensitive; a decomposed umlaut matches too, as for --theme and --region)

regionalatlas indicators → [{ code, table, theme, titleShort, titleLong, years, levels, fields }, …], where years lists every year offered (e.g. ["2000", "2005", …], gaps included), levels maps each year to the geo levels with figures, and fields lists the value columns a query returns as { code, title, unit }, code being the key in values. titleLong is the catalogue’s long title, which --search also matches (it contains the theme name).

query <indicator-code> — fetch data rows

Option Description
--level <level> geo level: land | regierungsbezirk | kreis | gemeinde (default land)
--year <yyyy> reporting year (default: the newest year in the catalogue, which may not be loaded yet — see below)
--region <name\|ags> keep only the region with this AGS, or this exact name — else every region whose name contains the text (see below)
--fields <a,b,c> keep only these value fields (comma-separated; repeating the option adds to the list); names are checked against the indicator’s columns

The positional <indicator-code> accepts the code form (AI002-1-5) or the table form (ai002_1_5), case-insensitively. Output is [{ ags, name, typ, level, year, values }, …], one row per region. A value the upstream sent as a special-value code (2222222222 = nichts vorhanden, 6666666666 = Aussage nicht sinnvoll, …; see GLOSSARY.md) is never the code: 2222222222, the table symbol - for exactly zero, is 0, every other code is null, and the row then carries a missing object naming the code’s meaning per field ("missing": {"ai0507": "nichts vorhanden"}). Before 0.5.0 “nichts vorhanden” was null too.

--level accepts these aliases: land/laender/länder/bundesland/bundesländer (=1), regierungsbezirk/rb (=2), kreis/kreise/landkreis (=3), gemeinde/gemeinden (=5) — case-insensitively, with surrounding spaces ignored and an umlaut typed decomposed (as macOS input can produce) read like the composed one.

Every level covers all of Germany, filling in with the next coarser unit where the finer one does not exist — so ags length varies within a level. regierungsbezirk returns 38 rows for recent years: the 29 actual Regierungsbezirke plus the 9 Bundesländer that have none. kreis (400 for recent years) and gemeinde (~11 000) carry Berlin and Hamburg at 2 digits, and gemeinde carries 104 kreisfreie Städte at their 5-digit Kreis key. Each level is a non-overlapping partition, so summing or mapping one is safe; counting its rows as “the Regierungsbezirke of Germany” is not. Row counts follow the boundaries of the reporting year: for 2000, regierungsbezirk has 40 rows and kreis 440, among them two Kreise named Hannover (03201, 03253) from before the Region Hannover — compare by ags, not by name, across years.

Not every indicator is published at every level: AIGG-01 (Gesundheitsausgaben) exists only per Land, AI005 (Bundestagswahl) not per Gemeinde, and some years have no figures at all. The catalogue records which levels have figures in each year, and query refuses a level without any (exit 2, before the data request), naming the published levels and the years that do have figures at the requested one — the data host would otherwise return a row for every region with every value null. --region and --fields are applied client-side (they never enter the upstream request), but a --fields name is validated against the indicator’s value columns first — indicators lists them with their titles and units.

--region picks rows like this:

An empty result prints [], exits 0 and explains itself on stderr, by cause: either the data host returned no rows for the indicator, level and year, or it did and no row matched --region (the note then says how many rows there were, and gives no year hint). The catalogue can list a newest year the data host has not loaded yet: on 2026-09-15 AI013-1 listed 2026, and query AI013-1 --level kreis returned [], while --year 2025 returned all 400 Kreise. When the year was defaulted, the note names the previous catalogue year to try.

Examples

regionalatlas themes --compact | jq '.[].title'
regionalatlas indicators --search bevölkerung
regionalatlas indicators --theme Wahlen --year 2021
regionalatlas query AI002-1-5 --level land --year 2020                 # 16 Bundesländer
regionalatlas query AI002-1-5 --level kreis                            # ~400 Kreise, latest year
regionalatlas query AI002-1-5 --level land --region Bayern --compact
regionalatlas query AI002-1-5 --level land --fields ai0201 --compact | jq '.[] | {name, values}'

Exit codes

Code Meaning
0 success (help/version included); an empty result also exits 0, with an INFO note on stderr — from query and from indicators alike; so does a run whose output reader stops early (\| head), quietly
1 API/logical error (the ArcGIS error envelope), a catalogue the CLI refuses (not the expected shape, or two indicator codes naming the same table, or one naming the boundary table verwaltungsgrenzen_gesamt), or a catch-all
2 usage / validation error (bad flags, unknown command, unknown indicator, unknown --level, a --level the indicator has no figures at in that year, a --year outside the indicator’s range, an unknown --fields column, a non-http(s) or malformed --base-url/--catalog-url, a --base-url with a query, fragment or surrounding whitespace, redirecting base URL)
4 HTTP 404
6 network / transport failure (DNS, connection, timeout, response size-cap)

A failed run keeps its exit code when the reader of stderr has gone away (2>&1 | true).

Notes

View the source on GitHub →