Usage

bundesrat — a CLI for the Bundesrat’s public data feeds. This is the use-case-driven cookbook; for the option reference see the README, and for domain terms the Glossary. No API key is required.

bundesrat [global options] <command>

Global options

Option Description
--base-url <url> API base URL (only http:/https:; no query, fragment, whitespace or control characters; a literal % in a password is written %25)
--timeout <ms> time limit per request in ms, whole response included (0 = no timeout; at most 2147483647); it bounds each attempt, the waits between retries come on top
--user-agent <ua> User-Agent header value
--max-retries <n> retries for transient 429/503 responses and reset connections (0..10; each waits 200 ms × attempt, or a 429/503’s Retry-After when that is longer, up to 30 s — a longer one is not retried and the error names the requested wait). Each retry logs one WARN record of bundesrat.http before it waits (HTTP 503 from host: retry 1 of 3 in 2 s).
--max-response-bytes <n> cap the response body size in bytes (0 = unlimited; default 100 MiB)
--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 [bundesrat.http] …) or jsonl (one JSON object per line: ts, level, topic, msg). stdout is not affected
-o, --output <file> write output to a file instead of stdout (- = stdout; a blank path is a usage error)
-V, --version / -h, --help version / help

A base URL on plain http: to a host other than loopback (localhost, 127.0.0.0/8, ::1) works, but the CLI writes one WARN record of bundesrat.http to stderr before the first request, e.g. … WARN [bundesrat.http] requests to mirror.example are sent unencrypted (http:, not https:), or … WARN [bundesrat.http] the base URL's credentials are sent unencrypted to mirror.example (http:, not https:) when it carries a user:password@ (never printed). stdout, -o files and the exit code are unchanged.

session — the current plenary sitting

The heart of the tool: the current sitting’s title, date, and every agenda item (Tagesordnungspunkt / TOP) with its Drucksache.

bundesrat session
{
  "title": "1067. Sitzung des Bundesrates | Tagesordnung Entwurf",
  "header": "am Freitag, dem 10. Juli 2026, 9:30 Uhr",
  "tops": [
    { "toptitle": "TOP 67", "topdrucksache": "Drucksache 371/26", "topheader": "…" }
  ]
}

Every field is a plain string. Empty fields are dropped (and so is a field whose value isn’t plain text, e.g. inline markup or HTML inside CDATA), so linkedtop appears only when a TOP has a cross-reference, and a TOP without a Drucksache has no topdrucksache key. A TOP that covers several Drucksachen lists them all, joined with "; " (“Drucksache 343/26; Drucksache 344/26”); the recipe below splits them. The feed lists the TOPs out of order (e.g. TOP 31, TOP 24, TOP 81), so sort them by number and letter suffix before printing an agenda.

# Just the agenda, one line per TOP, in TOP order
bundesrat session \
  | jq -r '.tops | sort_by(.toptitle // "" | capture("(?<n>\\d+)(?<s>[a-z]*)") | [(.n | tonumber), .s])[] | "\(.toptitle)\t\(.topdrucksache // "—")\t\(.topheader)"'

# How many agenda items?
bundesrat session | jq '.tops | length'

# Every Drucksache on the agenda
bundesrat session | jq -r '.tops[].topdrucksache | select(.) | split("; ")[]'

The agenda item’s HTML description (topdetail) is copyright-protected editorial content and is not surfaced — only the factual TOP number, Drucksache and short header. See DATA_LICENSE.md.

Between sittings the agenda feed can be sparse: title and header are optional and may be absent, and tops may be []. Guard for them in scripts (.title // "—").

members — the members of the Bundesrat

bundesrat members                      # everyone (~190, all Länder)
bundesrat members --state Bayern       # one of the 16 Länder (case-insensitive, exact match)
bundesrat members --party grüne        # party substring, case-insensitive
bundesrat members --state Hessen --party CDU

Each member carries honorificTitle, firstname, name, party, state, a url, and the boolean-ish flags brmitglied / mitglied / bv / designiert (as "true"/"false" strings). The feed’s HTML biography (detail) and portrait image (imagePath) are copyright-protected and are not surfaced (see DATA_LICENSE.md).

The list holds more than the 69 members: mitglied == "true" marks a member, brmitglied == "true" with mitglied == "false" a deputy (stellvertretendes Mitglied), and bv == "true" alone a plenipotentiary, who may have no party.

# Names + parties of a Land's members
bundesrat members --state "Nordrhein-Westfalen" \
  | jq -r '.[] | select(.mitglied == "true") | "\(.firstname) \(.name) — \(.party)"'

# Party head-count of the 69 members
bundesrat members | jq -r '[.[] | select(.mitglied == "true")] | group_by(.party)[] | "\(.[0].party // "no party given"): \(length)"'

Filtering is client-side, so an unmatched --party returns [] (not the full list), with a note on stderr. A --state that is not one of the 16 Länder (Thueringen, Bay) is a usage error (exit 2) that lists them.

appointments — committee dates (Termine)

bundesrat appointments

Returns an array of calendar items with their factual fields: type, id, url, title, startdate, and stopdate / date when the feed sets them (on 2026-09-26, 5 of the 20 items had a stopdate, none a date). (The item’s HTML detail/abstract body and any image are copyright-protected and are not surfaced.)

startdate and stopdate are German-format strings, "25.09.2026 09:30" or date-only "22.09.2026" (DD.MM.YYYY[ HH:MM]), which don’t sort as text. Cancelled dates stay in the list with the cancellation in the title („… entfällt / Umfrageverfahren”).

# Committee dates, sorted, start as YYYY-MM-DD HH:MM
bundesrat appointments \
  | jq -r 'map(. + {start: ((.startdate // "" | capture("(?<d>\\d{2})\\.(?<m>\\d{2})\\.(?<y>\\d{4}) ?(?<t>[0-9:]*)") | "\(.y)-\(.m)-\(.d) \(.t)") // "")}) | sort_by(.start)[] | "\(.start)\t\(.title)"'

Only open data is exposed. The Bundesrat feeds also carry news/press items, the BundesratKOMPAKT editorial summaries, the Stimmverteilung graphic, and the Präsidium / next-sitting HTML pages — all copyright-protected editorial content, so this CLI does not provide commands for them. See DATA_LICENSE.md.

Scripting recipes

# Save the current agenda to a file (stdout stays clean; a note goes to stderr)
bundesrat --output agenda.json session

# Session title + agenda size in one compact line
bundesrat --compact session | jq -c '{title, n: (.tops|length)}'

# Which parties are represented, sorted
bundesrat members | jq -r '[.[].party] | unique[]'

Exit codes

Code Meaning
0 success (help/version included); an empty feed (<iOS><list/></iOS>) also exits 0
1 a runtime error — including a response that is not a feed: the website’s HTML shell, or XML that isn’t <iOS><list>…</list></iOS> (“Unexpected response shape”)
2 usage error (bad flag, unknown command, bad --base-url, a --state that is not one of the 16 Länder)
4 HTTP 404 (not found)
6 network / transport failure (DNS, connection, timeout, response size-cap)

Notes

View the source on GitHub →