Dieses Dokument gibt es nur auf Englisch.

Usage

Use-case-driven recipes for the oparl CLI, which reads German council information systems through OParl, their open standard API. Every municipality runs its own server, so each recipe starts from a System or Body URL. The examples use the City of Cologne’s server; substitute any endpoint from oparl endpoints.

SYSTEM=https://buergerinfo.stadt-koeln.de/oparl/system
BODY=https://buergerinfo.stadt-koeln.de/oparl/bodies/stadtverwaltung_koeln

Council systems differ in speed and in how completely they implement OParl. Start with one page (the default), and add filters one at a time.

Install

npm i -g @maschinenlesbar.org/oparl-cli

This installs the oparl command. All examples pipe JSON to jq, which is optional.

Finding a server

1. Find a municipality’s endpoint

oparl endpoints --search köln | jq -r '.[] | "\(.title)\t\(.working)\t\(.url)"'

endpoints lists the public registry at dev.oparl.org, followed by a curated list of servers the registry lacks (source says which). working is the result of the last live check (checked, with the reason in problem); for a registry entry nobody has checked, it is the registry’s own last fetch (fetched). A server that moved has replacedBy, the new System URL. Search by place name or by a part of the URL (e.g. --search ratsinfomanagement). The search covers the title, the URL and the note, and ignores case and accents, so köln and koln match the same entries. Only when nothing matches literally does it try the umlaut spellings, so koeln still finds Köln while an ordinary word like Aue keeps its own meaning. When dev.oparl.org cannot be reached, endpoints notes that on stderr and lists the curated servers alone, as of their last check — --source curated does the same without trying the network.

2. All working endpoints on OParl 1.1

oparl endpoints --working --oparl-version 1.1 | jq -r '.[] | "\(.title)\t\(.systemName)"'

# Only the servers the registry doesn't list (no request is made)
oparl endpoints --source curated --working | jq -r '.[] | "\(.title)\t\(.url)"'

--oparl-version takes the short form (1.0, 1.1) or the whole version URI a System reports (https://schema.oparl.org/1.1/); anything else is a usage error rather than an empty result.

Not listed at all? Any System URL works: oparl system <url>. Council portals often link their OParl interface, and many vendors use fixed paths (e.g. …/webservice/oparl/v1.1/system on SD.NET, …/oparl/system on Session).

3. Check an endpoint before relying on it

oparl system "$SYSTEM" | jq '{name, oparlVersion, vendor, license}'

A System with a body URL is a live OParl server. Exit 1 with “not an OParl System” means the URL is something else — often a portal page or a moved endpoint. Exit 1 with “is an OParl System, but its body is …” means the URL is right and the server does not publish the URL of its list of bodies; oparl get "$SYSTEM" shows what it sends instead.

Bodies and their lists

4. List the bodies on a server

oparl bodies "$SYSTEM" | jq -r '.data[] | "\(.name)\t\(.id)"'

Most servers host one body; regional providers host several municipalities.

5. See which lists a body offers

oparl get "$BODY" | jq -r 'to_entries[] | select(.key | test("^(organization|person|meeting|paper|agendaItem|consultation|consultations|file|files|membership|locationList|legislativeTermList)$")) | .key'

Every other URL a body carries — its own id, its system, its website — is not an object list, which is why the recipe matches the list names instead of “looks like a URL”. OParl 1.0 bodies link only organization, person, meeting and paper; some servers use the plural consultations/files, which oparl list follows too.

Meetings, papers, committees

6. Recent papers (motions, inquiries, proposals)

oparl list paper "$BODY" --modified-since 2026-09-01 \
  | jq -r '.data[] | select(.deleted != true) | "\(.date)\t\(.reference)\t\(.paperType // "")\t\(.name)"'

If a server ignores --modified-since, you get its first page unfiltered — compare the modified field. Even when it honours the filter, the list holds every paper changed since the date, including old papers edited since and deleted ones (deleted: true, with empty fields), which the select drops. For new papers only, also compare date.

The filter is sent with every page: your value replaces the one a server put into its own next link (several get the encoding wrong) or into a list URL, and it is sent again if the server redirects the request elsewhere. So every page of the result is filtered alike.

Some servers answer a filter they cannot satisfy with an error instead of an empty list: SD.NET RIM (Bremen, Essen and most other …/webservice/oparl/… endpoints) replies HTTP 404 when no object falls into the window, so the CLI exits 4 “not found” although the list exists. Repeat the call without the filter: if that returns data, the window was simply empty.

7. Meetings, several pages at once

oparl list meeting "$BODY" --max-pages 3 | jq -r '.data[] | "\(.start)\t\(.name)"'

The order is the server’s; some list the newest or future meetings first.

8. Continue where a list stopped

next=$(oparl list meeting "$BODY" | jq -r .next)
oparl get "$next" | jq -r '.data[] | .name'

next is null on the last page. It is also the way on when a walk gave up early: with looped: true and a note on stderr (“stopped after page N …”), next still points at the page after the last one fetched, so oparl get "$next" continues from there.

9. Committees and groups

oparl list organization "$BODY" --max-pages 0 | jq -r '.data[] | "\(.classification // "")\t\(.name)"'

10. Legislative terms

oparl list legislative-term "$BODY" | jq -r '.data[] | "\(.startDate)\t\(.name)"'

OParl 1.0 bodies embed their terms instead of linking a list; those come back with pages: 0 and the date filters applied locally. Where such a term carries no created/modified (ALLRIS 1.0 servers omit them), it is listed anyway and a note says how many terms the filter could not be applied to.

11. Follow a reference

Objects point to each other by URL. Open a meeting’s agenda item or a paper’s consultation with get:

paper=$(oparl list paper "$BODY" | jq -r '.data[0].id')
oparl get "$paper" | jq '{reference, name, consultation}'

Keeping a local copy in sync

Fetch everything once, then only what changed:

oparl list paper "$BODY" --max-pages 0 -o papers-full.json
oparl list paper "$BODY" --max-pages 0 --modified-since 2026-09-01T00:00:00+02:00 -o papers-delta.json

Deleted objects may appear with deleted: true — apply them to your copy as deletions. Each object appears once per id, and where a page repeated it (a list that changes while it is being walked does that), the delta holds the last copy the server sent, so an object edited or deleted mid-walk is not kept stale. On large servers --max-pages 0 can take a long time; raise --timeout rather than lowering it. An empty window can come back as exit 4 on SD.NET servers — see recipe 6.

Global options

# A slow server: 5 minutes per request, no retries
oparl --timeout 300000 --max-retries 0 list meeting "$BODY"

# Compact JSON straight into jq
oparl --compact bodies "$SYSTEM" | jq -c '.data[]'

# Say who is calling (header values are ASCII or Latin-1; anything else exits 2)
oparl --user-agent "stadtdaten-bot (kontakt@example.de)" system "$SYSTEM"

See the README for the full table and the exit codes.

Quelle auf GitHub ansehen →