Usage
Practical, use-case-driven examples for the lobbyregister CLI — a command-line
client for the open German Lobbyregister
search API (/sucheJson), the federal register of interest representatives
(lobbyists) before the Bundestag and the federal government.
The CLI is read-only and needs no API key. It exposes two commands — search
and count — and prints JSON to stdout, so it composes cleanly with jq.
Install
npm i -g @maschinenlesbar.org/lobbyregister-cli
This installs a single bin named lobbyregister. Confirm it works:
lobbyregister --version
lobbyregister --help
Without a global install you can also run it from a checkout with
node apps/cli/dist/src/cli/index.js … (after npm run build).
Use cases
1. Count how many entries mention a topic
Gauge how strongly a sector is represented, as a single number.
lobbyregister count Energie
Prints an envelope with the query and the API-reported total:
{ "query": "Energie", "resultCount": 1234 }
count takes an optional query, --filter and the global options — no paging or
sorting flags. It is not a cheaper request: the API has no count-only mode, so
count downloads the same records as search (the whole register, about 18 MB,
without a query or filter) and prints only resultCount. A loop over several topics
(§7) downloads each topic’s full set. Should resultCount ever disagree with the number of
records downloaded, count still prints resultCount and says so on stderr, in a WARN record:
the register reports resultCount 2351 but returned 2350 results; the count shown
is resultCount (the exit code stays 0).
2. Search lobbyists by keyword
Retrieve the full result set for a search term as the API returns it.
lobbyregister search Wasserstoff
Prints the full envelope: resultCount plus the results array of register
entries (each a raw, schema-versioned RegisterEntry JSON object).
3. Get only the entries, not the envelope
When you want to pipe the records straight into another tool and don’t care about
the surrounding resultCount wrapper.
lobbyregister search Wasserstoff --results-only
--results-only prints just the results array instead of the full envelope.
4. Sort results by newest registration first
See which interest representatives most recently registered for a topic.
lobbyregister search Pharma --sort REGISTRATION_DESC
--sort values (the orders of the register’s website search):
RELEVANCE_DESC (the default with a query), REGISTRATION_DESC (first published),
UPDATE_DESC (last updated), INACTIVITY_DESC (went inactive), NAME_ASC,
FINANCIALEXPENSES_DESC (declared spend), DONATIONAMOUNT_DESC, MEMBERSHIPFEES_DESC,
NUMBEROFREGULATORYPROJECTS_DESC, NUMBEROFSTATEMENTS_DESC, NUMBEROFFTE_DESC,
NUMBEROFENTRUSTEDPERSONS_DESC, NUMBEROFCONTRACTS_DESC, NUMBEROFMEMBERS_DESC,
NUMBEROFMEMBERSHIPS_DESC — each also in the other direction (_ASC / _DESC).
The value is passed through verbatim and is case-sensitive. The API ignores an
unrecognised value (HTTP 200, default ordering), so a bad sort never raises a
400; the CLI compares the order the API reports (searchParameters.sortOrder)
with the one you asked for and logs a WARN record on stderr when they differ
(exit 0). The record quotes the reported order cleaned, as it does an error message:
control characters dropped, on one line, at most 200 characters.
# Biggest declared lobbying budgets first
lobbyregister search Energie --sort FINANCIALEXPENSES_DESC --page-size 10 --results-only
5. Page through a large result set
Browse results in fixed-size chunks instead of dumping everything at once. The
live endpoint ignores paging parameters and returns all matches, so paging is
applied client-side: every run downloads the full set and the client slices the
results array (nothing paging-related is sent). --page is 1-based and requires --page-size; both must be 1 or more
(0 is a usage error, exit 2).
# First 10 entries
lobbyregister search Digitalisierung --sort REGISTRATION_DESC --page-size 10 --page 1
# Next 10 entries
lobbyregister search Digitalisierung --sort REGISTRATION_DESC --page-size 10 --page 2
resultCount still reflects the true total; only the visible slice is trimmed.
Each run downloads the set again, and the register’s relevance order
(RELEVANCE_DESC, the default with a query) is not stable between requests:
two identical searches return the entries in a different order, so page 1 and
page 2 from separate runs can overlap or skip entries. Page with a date sort
(REGISTRATION_DESC, stable in our checks), or fetch the whole set once and
slice it yourself. When it pages a relevance-ordered set, the CLI logs an
INFO record on stderr (exit 0).
6. Extract just the names of matching organisations with jq
Turn raw register entries into a flat list for a report or spreadsheet.
lobbyregister search Klimaschutz --results-only \
| jq -r '.[].lobbyistIdentity.name // empty'
--results-only gives jq a plain array to iterate. (Adjust the field path to
the entry shape in your results; jq '.[0] | keys' reveals the available keys.)
7. Compare topic coverage with a one-liner
Pull just the count for several topics to compare their footprint in the register.
for topic in Energie Verkehr Gesundheit Landwirtschaft; do
printf '%s\t' "$topic"
lobbyregister count "$topic" | jq '.resultCount'
done
8. List the newest registrations in a topic as a compact table
Combine sorting, paging and jq to skim recent entrants.
lobbyregister search Rüstung --sort REGISTRATION_DESC --page-size 5 --results-only \
| jq -r '.[] | [.registerNumber, .lobbyistIdentity.name] | @tsv'
9. Stream single-line JSON into a logging or ETL pipeline
--compact prints JSON on one line, which is friendlier for line-oriented tools.
lobbyregister search Chemie --results-only --compact \
| jq -c '.[] | {nr: .registerNumber}'
10. Narrow a search with the register’s own filters
--filter <attribute=value> passes the facet filters of the register’s
website search to the API, on search
and count. It can be repeated: values of one attribute are alternatives (OR),
different attributes must all match (AND).
# Entries with revolving-door data: a lobbyist, legal representative, entrusted
# person or contractor who recently held public office
lobbyregister count --filter revolvingdoordata=true
# ... among the energy entries, only where it concerns an entrusted person
lobbyregister search Energie --filter revolvingdoordata=true \
--filter revolvingdoorpersontypes=ENTRUSTED_PERSON --results-only
# Inactive entries with the field of interest "Energie"
lobbyregister search --filter activelobbyist=false --filter fieldsofinterest=FOI_ENERGY
# Entries that declared donations received (the code, not "true")
lobbyregister count --filter donationsreceived=DONATIONS_RECEIVED
# A field-of-interest code as it appears in the entries' fieldsOfInterest[].code
lobbyregister count --filter fieldsofinterest=FOI_EU_LAWS
# -> sent as fieldsofinterest=FOI_EUROPEAN_UNION|FOI_EU_LAWS, the form the register matches
lobbyregister search --help lists the attributes. The values are the register’s codes
(true/false for yes/no facets; FOI_ENERGY, DONATIONS_RECEIVED, ACT_TRADE_ASSOC
and so on otherwise), matched case-insensitively; a sub-field is FOI_WORK|FOI_WORK_POLICY,
and a bare sub-field code from the data (FOI_WORK_POLICY) is sent in that form. An
unknown attribute is a usage error (exit 2) because the API would silently ignore it
and return the whole set; an unknown value is a usage error too, naming the valid
codes, because the API would match nothing (resultCount: 0) and “nobody” would look
like an answer. --allow-unknown-filters sends either anyway (for a code the register
added after this release); a reply of 0 then logs an INFO record on stderr. The library
exports the codes as SEARCH_FILTER_VALUES. If a reply does not
confirm a filter in searchParameters.facets, the CLI exits 1 rather than print an
unfiltered set. The website’s number ranges (spend, staff, members from/to) are not
supported.
11. Search a term that begins with a dash
A leading - would otherwise be parsed as an option. End the options with --.
lobbyregister search -- -Energie
This searches for the literal term -Energie.
12. What the query understands (observed, not documented)
The register publishes no query syntax. What follows is what its search answered on
2026-10-05 (live, search/count, register of 6 996 entries); it is observed behaviour,
not a promise, and may change without notice. Check a surprising count before relying on it.
| Query | Observed | Reading |
|---|---|---|
Tabak |
66 | one word: the plain search |
R000534, r000534 |
that entry | a register number, either case |
R000534 R002822 |
0 (each alone: 1) | words separated by spaces must all match |
Tabak AND Wasserstoff |
3 | AND (capitals) — all terms |
Tabak OR Wasserstoff |
480 | OR (capitals) — any term |
'"Deutscher Bauernverband"' |
75 | a phrase in double quotes is accepted (not compared with the unquoted words) |
* |
6 996 | the whole register |
Taba* |
3 | not a prefix search: fewer than Tabak alone — don’t rely on * inside a word |
-Tabak (after --) |
66, same as Tabak |
a leading - is not NOT |
Tabak: |
66, same as Tabak |
a trailing colon is ignored |
"Tabak, Tabak (, ~2 000 characters |
exit 1 | rejected by the register (redirect to its /fehler page; see the README’s troubleshooting) |
Quote the whole query for the shell, as above: lobbyregister count 'Tabak OR Wasserstoff'.
Global options
These apply to every command and may be given before or after the command
name (e.g. both lobbyregister --compact count Energie and
lobbyregister count Energie --compact work):
| Option | Description |
|---|---|
-V, --version |
Print the version and exit |
--base-url <url> |
API base URL (default https://www.lobbyregister.bundestag.de); http(s) only, given once. A user name or password in it is a usage error (exit 2; the API needs no credentials, and the message never repeats them). A plain http: URL to a host other than loopback (localhost, 127.0.0.0/8, ::1) logs one WARN record of lobbyregister.http (requests to <host> are sent unencrypted (http:, not https:)) on stderr before the first request; stdout and the exit code are unchanged |
--timeout <ms> |
Per-request timeout in milliseconds (default 30000) |
--user-agent <ua> |
User-Agent header value (default lobbyregister-cli; not blank, no control characters, Latin-1 only) |
--max-retries <n> |
Retries for transient 429/503 responses and reset connections (default 2); each waits 200 ms × attempt, or the server’s Retry-After when that is longer (up to 60 s). A timeout is not retried. Each retry logs one WARN record of lobbyregister.http before it waits (HTTP 503 from host: retry 1 of 3 in 2 s) |
--max-redirects <n> |
HTTP redirects to follow (0 = none; default 5) |
--max-response-bytes <n> |
Cap response body size in bytes (0 = unlimited; default ~100 MiB) |
--compact |
Print JSON on a single line instead of pretty-printed |
--log-format <format> |
How errors, warnings and notes are written to stderr: text (default; log4j style, 2026-10-09T14:03:12.481Z WARN [lobbyregister.http] …) or jsonl (one JSON object per line: ts, level, topic, msg). stdout is not affected |
-h, --help |
Show help for the program or a command |
Exit codes: 0 success, 2 usage error, 4 on a 404 from the API, 1 for
any other error (network, parse, or other non-404 HTTP status).