# German Council Radar – Sales Signals from Council Papers (`previsa/german-council-radar`) Actor

Spot public projects while the council is still discussing them – often before a tender is published (not every council paper leads to a tender). Sales signals from German city council papers (OParl): construction, IT, energy, consulting – with score, EUR amount and reasons.

- **URL**: https://apify.com/previsa/german-council-radar.md
- **Developed by:** [Previsa](https://apify.com/previsa) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## German Council Radar – Sales Signals from City Council Papers (Kommunal-Radar)

**Spot public projects while the council is still discussing them – often before a tender is published (not every
council paper leads to a tender).** A new kindergarten, a school renovation, Wi-Fi for all schools, solar panels on
public roofs, a heat plan, a new cycle path: this actor reads new council papers and tells you which ones contain a
sales signal for you.

> **🇩🇪 Kurz auf Deutsch:** Liest neue **Beschlussvorlagen, Anträge und Mitteilungen** aus den
> Ratsinformationssystemen deutscher Kommunen (offene OParl-Schnittstelle) und **erkennt Vertriebs-Signale, oft schon
> vor der Ausschreibung (nicht jede Vorlage führt zu einer Ausschreibung)**: Neubau/Sanierung (Kita, Schule,
> Feuerwehr), Digitalisierung/IT, Energie/PV/Wärmepumpe, Beratung/Gutachten, Mobilität, angekündigte Vergabe,
> Haushalts-/Fördermittel. Pro Vorlage: Kategorien, **Signal-Wert 0–100 mit Begründung**, erkannter **Betrag in €**,
> Link. Mit **Wächter-Modus** kommen jeden Morgen nur die **neuen** Vorlagen. Bezahlt wird nur pro Vorlage **mit**
> Signal. Abdeckung ehrlich: **ca. 85 erreichbare Kommunen, vor allem NRW und Hessen** (u. a. Köln, Dresden, Münster,
> Freiburg, Krefeld, Neuss) – nicht ganz Deutschland. Namen werden geschwärzt, aber nicht jeder Name wird sicher
> erkannt; für die Weiterverwendung sind Sie verantwortlich. Previsa speichert keine Ergebnisse; Löschanfragen an
> kontakt@previsa.de. Keine Rechtsberatung. Anbieter: Previsa · KI & Automation, https://previsa.de

### The chain: signal → tender → funding

1. **Signal** – the council discusses or decides a project: *this actor*.
2. **Tender** – if the project is put out to tender, weeks or months later:
   [German Public Tenders](https://apify.com/previsa/german-tender-finder) (open tenders + eligibility check).
3. **Funding** – the municipality (or you) applies for subsidies: [German Funding Radar](https://apify.com/previsa/german-funding-radar) (by the same provider).

Use all three with the same keywords and a daily schedule, and you can follow a project from the first council paper
to the tender.

### What you get

- ✅ **New council papers only** (created or dated within `daysBack`), straight from the official council systems
- ✅ **Signals in 7 categories** — `bau` (construction/renovation), `digital` (IT, Wi-Fi, fibre), `energie`
  (PV, heat pumps, heat planning, LED), `beratung` (expert reports, concepts, planning services), `mobilitaet`
  (cycle paths, public transport, charging points), `vergabe` (tender/award announced), `geld` (budget, subsidies,
  unplanned spending) — plus **your own keywords**
- ✅ **Signal score 0–100** with `matchReasons` — every point is explained, no black box
- ✅ **Amount in EUR** when the document names one (`1,5 Mio. €`, `450 TEUR`, `1.000.000,- €`)
- ✅ Optional: **read the main document** (`readMainFile`) for more categories, the amount and a 300-character excerpt
- ✅ **Daily watch:** only papers you have not seen before (`watchId`)
- ✅ Optional **3-sentence AI summary** per paper with **your own** Anthropic key
- ✅ A **run summary** (`RUN_SUMMARY`) that names every municipality that could not be read, and why

The document text never creates a signal on its own: a paper is only returned if its **title** shows a signal; the
document adds categories, the amount and the excerpt. Form phrases such as „keine Haushaltsmittel“ or „Beratung im
Ausschuss“ never count.

### Who uses it

| You are … | Typical setup |
|---|---|
| Solar / heat pump installer, energy consultant | topics `energie`, `geld`, municipalities in your region |
| IT service provider, software for public authorities | topics `digital`, `vergabe` |
| Architect, engineering office, construction company | topics `bau`, `beratung`, `vergabe` |
| Consultancy, planning office | topics `beratung`, keywords `Machbarkeitsstudie`, `Klimaschutzkonzept` |
| Bid manager / sales team for the public sector | several watches, one per region (`watchId` per watch) |

### Quick start: a daily signal alert in 2 minutes

1. Enter your **municipalities** (e.g. `Köln`, `Dresden`, `Freiburg`, `Neuss`) — only municipalities from the OParl
   list, own URLs are not possible — or leave empty for all reachable ones (slow). Names are also found inside shared systems (Neuss sits in the ITK-Rheinland system); `ü`, `ue` and
   `u` are treated alike.
2. Pick your **signal categories** and add **your own keywords** if you like.
3. Give the watch a name in **`watchId`**, e.g. `bau-rheinland`.
4. Click **Schedule** → daily. Every morning you get only the new papers, best signals first. Connect Apify's
   integrations (e-mail, Slack, Google Sheets, Make, Zapier) to have them delivered.

Example input:

```json
{
  "bodies": ["Köln", "Dresden", "Freiburg", "Neuss"],
  "topics": ["energie", "bau", "vergabe", "geld"],
  "keywords": ["Wärmenetz"],
  "excludeKeywords": ["Straßenreinigung"],
  "daysBack": 7,
  "maxResults": 50,
  "minSignalScore": 30,
  "readMainFile": true,
  "watchId": "bau-rheinland"
}
```

### Output fields

| Field | Meaning |
|---|---|
| `municipality` | name of the municipality / body |
| `title` | title of the council paper (names scrubbed) |
| `reference` | file number of the paper, e.g. `G-26/139` |
| `paperType` | e.g. Beschlussvorlage, Antrag, Mitteilung |
| `date` | date of the paper |
| `signals` | categories found (`bau`, `digital`, `energie`, `beratung`, `mobilitaet`, `vergabe`, `geld`, `custom`) |
| `signalScore` | 0–100, see `matchReasons` |
| `matchReasons` | every word and every point of the score — words from the title and from the document are listed separately |
| `amountEur` | largest euro amount found in title or document (or `null`) |
| `excerpt` | ≤ 300 characters of the document, starting at the first signal word (or `null`) |
| `url` | link to the paper in the council system |
| `fileUrl` | link to the main document (PDF), if the municipality publishes one |
| `mainFileRead` | `true` if document text was used for this row |
| `license` | licence URL published by the municipality (or `null`) |
| `source` | the OParl system the row comes from |
| `aiSummary`, `aiGenerated` | only with your own AI key |

Example row (real run against Freiburg on 08.10.2026, with `readMainFile`):

```json
{
  "municipality": "Stadtverwaltung Freiburg",
  "title": "Sanierung Bestandsgebäude der Johannes-Schwartz-Schule - Grundsatzbeschluss",
  "reference": "G-26/139",
  "paperType": "BESCHLUSS-VORLAGE",
  "date": "2026-10-06",
  "signals": ["bau", "vergabe", "geld"],
  "signalScore": 88,
  "matchReasons": ["bau: title: Sanierung, Schule; document: Neubau, Kita, Kindertagesstätte (+29)",
                   "vergabe: document: Vergabe (+12)",
                   "geld: document: Zuschuss (+12)",
                   "procurement word together with a topic (+10)",
                   "amount 11.000.000 EUR (+15)",
                   "decision paper 'BESCHLUSS-VORLAGE' (+10)"],
  "amountEur": 11000000.0,
  "excerpt": "Sanierung Bestandsgebäude der Johannes-Schwartz-Schule – Grundsatzbeschluss Beratungsfolge Sitzungstermin Öff. N.Ö. Empfehlung Beschluss 1. BaUStA 04.11.2026 X X 2. ASW 11.11.2026 X X 3. HFA 16.11.2026 X X 4. GR 24.11.2026 X X Anhörung Ortschaftsrat (§ 70 Abs. 1 GemO): ja, durchgeführt in Lehen am",
  "url": "https://ris.freiburg.de/oparl/paper/5941607100138",
  "fileUrl": "https://ris.freiburg.de/api.php?document_type_id=4&submission_id=5941607100138&id=69&inline=true",
  "mainFileRead": true,
  "license": null,
  "source": "OParl council information system (Stadtverwaltung Freiburg) – https://ris.freiburg.de/oparl"
}
```

### What it costs

You pay **only for papers with at least one signal** (and, by default, a score of at least 20); papers without a
signal are read but never charged. Your spending limit is always respected — nothing is downloaded for a row that
could no longer be delivered.

| Event | Price | When |
|---|---|---|
| `signal` | $0.02 | one council paper with at least one signal |
| `signal-read` | $0.04 | the same, with `readMainFile` switched on and at least 40 characters of the main document really read |
| `ai-summary` | $0.005 | only with your own Anthropic key, on top of the row |

Some municipalities deliver the document text together with the list (e.g. Freiburg). That text is used for the
signals even without `readMainFile`, but then the row is charged as a plain `signal`.

**Example:** a daily watch over 10 cities returns about 3–8 new papers with signals per day →
5 × $0.02 = **$0.10 per day**, about **$3 per month**. With `readMainFile` for all of them: 5 × $0.04 = $0.20 per day.
A one-off research run with `maxResults: 50` costs at most 50 × $0.02 = **$1.00** (or $2.00 with documents read).

### Coverage — honest

The actor uses the public list of OParl endpoints maintained by the OParl project
(`https://raw.githubusercontent.com/OParl/resources/main/endpoints.yml`). Measured on 08.10.2026: **about 85
municipalities deliver current papers**, mostly in **North Rhine-Westphalia and Hesse**, plus e.g. **Köln, Dresden,
Münster, Freiburg, Krefeld, Neuss** and many towns using the SD.NET RIM / ekom21 systems.

Not covered: **Munich** (the OParl paper list answers 404), **Hamburg** (not in the list), and the **Berlin
districts** in the list (reachable, but their newest papers are years old — 2006 to 2017 — or the interface answers
with an error). Many other cities have no OParl interface at all.

Before each municipality is read, a health check goes down to the paper list. Municipalities that are not reachable,
answer 404, have OParl switched off, or show a **captcha / "Zugriff prüfen" / HTTP 403** are **skipped and named in
the run summary** — such barriers are never bypassed.

### How the actor treats the source (fair use)

- Strictly **one request after another**, a short pause after each, at most **2 retries** on 429/5xx
- Per municipality at most **60 seconds** and **5 list pages**, plus a total time limit (`timeLimitMinutes`)
- "New" papers via the OParl filter `created_since`; where a system ignores it (checked on every run), the actor
  falls back to reading the list from the newest end and filters by the paper date
- Main documents only on request (`readMainFile`), only for papers whose title already shows a signal, max. 20 pages,
  one after another
- Only systems from the checked OParl list are contacted; own URLs are not accepted. Every request goes to a checked
  public address of that system (links leading elsewhere are not followed)

### Personal data

- Only these paper fields are ever used: `id, name, reference, date, paperType, mainFile.accessUrl, web, license`
  (plus `created`/`deleted` for filtering). Authors, persons, memberships, consultations and responsible committees
  are **never requested or resolved**.
- All text fields are scrubbed: e-mail addresses, phone numbers and names after "Herr/Frau", council roles
  ("Ratsfrau", "Stadtverordnete", "Beigeordneter", "StR", "gez.", "Bürgermeisterin" …), before a party
  ("… (SPD)", "…, SPD-Fraktion") and after "von …" are replaced by `[removed]`.
- The main document text is used internally for signals only; at most a **300-character excerpt** is output. It
  starts at the first signal word of the content (not at the document head, where applicants and clerks stand).
- **Despite redaction, individual names (e.g. applicants without a role word) may remain in a title or excerpt. You
  are responsible for your further use. Previsa does not store results. Deletion requests: kontakt@previsa.de**
- 🇩🇪 Trotz Schwärzung können einzelne Namen (z. B. Antragsteller ohne Funktionsbezeichnung) in Titel oder Auszug
  stehen bleiben. Für die Weiterverwendung sind Sie verantwortlich. Previsa speichert keine Ergebnisse.
  Löschanfragen: kontakt@previsa.de

### AI summary (optional)

The AI summary sends title and excerpt with your key to Anthropic (USA); your own contract with Anthropic applies.
Without your key, nothing is sent anywhere.

### Source and licence

Council papers are official documents of the municipalities; licences differ per municipality (e.g. Dresden:
Datenlizenz Deutschland – Zero 2.0). This actor therefore outputs **only title, metadata, a short excerpt (≤ 300
characters) and the link** to the original — never full texts. The licence URL of the municipality is given per row
(`license`, where published) and the source system in `source`. Please check the licence of the municipality before
re-publishing data.

### FAQ

**Is this legal advice?** No. A signal is a hint that a project is being discussed, not a tender and not a
promise of a contract. Not every council paper leads to a tender. Always read the original paper.

**Why do I get few results for some cities?** Many papers are questions, minutes or personnel matters without a
signal — those are not output (and not charged). Some systems update only once a week.

**Why is `fileUrl` empty for Köln?** Some systems (e.g. Köln) publish no "main file" for a paper; then `readMainFile`
cannot read anything and the row is charged as a plain `signal`. Some systems also publish file links that no longer
work — then nothing is read and the row is charged as a plain `signal`.

**Can I add a municipality that is not in the list?** No. Only municipalities from the OParl list can be used; own
URLs are not possible (for security, the actor only contacts systems from the checked list). If your municipality has
an OParl interface, it can be added to the public list at github.com/OParl/resources.

**How does the score work?** Category in the title 20 points (12 if only in the document), +3 per further word,
+10 if a procurement word comes together with a topic, +5/+10/+15 for an amount, +10 for a decision paper,
−10 for a question/answer/information only. Capped at 0–100. By default (`minSignalScore` 20) a mere
question or answer with a single hit (score 10) is not returned and not charged; set 0 to see everything.

### Provider

Previsa · KI & Automation — https://previsa.de · Imprint: https://previsa.de/impressum.html · kontakt@previsa.de

# Actor input Schema

## `bodies` (type: `array`):

Names of municipalities from the OParl list or of a municipality inside a shared system (tested 08.10.2026: 'Köln', 'Dresden', 'Freiburg', 'Neuss', 'Aarbergen'). Only municipalities from the OParl list – own URLs are not possible (for security). Every word must be a whole word of the name ('Münster' does not match 'Bad Münstereifel'); ü/ue/u are treated alike. Empty = all reachable municipalities in the list (slow – about 85 active, mostly NRW and Hesse).

## `topics` (type: `array`):

Which kinds of signals to look for. Empty = all (unless you give your own keywords).

## `keywords` (type: `array`):

Optional extra words (German works best), e.g. 'Spielplatz', 'Reinigung', 'Webseite'. Matched at the start of a word; words written in capitals (like 'KI') only as a whole word. Shown as signal 'custom'.

## `excludeKeywords` (type: `array`):

Papers whose title contains one of these words are dropped (not read, not charged).

## `daysBack` (type: `integer`):

Only papers created (or dated) within this many days.

## `maxResults` (type: `integer`):

How many papers with signals to return (1–500). You pay per returned paper.

## `minSignalScore` (type: `integer`):

Default 20 hides papers that are only a question/answer/information with a single weak hit (score 10). 0 = every paper with at least one signal. You only pay for papers that are returned.

## `readMainFile` (type: `boolean`):

Reads the text of the main PDF (max. 20 pages, one after another) for papers whose title already shows a signal. Adds amounts in EUR, more categories and a 300-character excerpt. Charged as 'signal-read' only if at least 40 characters were really read. Text that a municipality delivers with the list (e.g. Freiburg) is used anyway, but charged as a plain 'signal'. Some systems (e.g. Köln) publish no main file.

## `watchId` (type: `string`):

Optional. Give your watch a name (e.g. 'pv-nrw'). Runs with the same name return only papers not reported before — perfect for a daily schedule. Stored in your own Apify account.

## `timeLimitMinutes` (type: `integer`):

The run stops politely after this time and reports which municipalities were not reached. Each municipality gets at most 60 seconds and 5 list pages.

## `companyDescription` (type: `string`):

Optional, 1–3 sentences about what you sell. Only used for the AI summary.

## `anthropicApiKey` (type: `string`):

Bring your own key: adds a 3-sentence summary per paper. Title and excerpt are sent with your key to Anthropic (USA); your own contract with Anthropic applies. Your key is used only for your run and never stored by us.

## `summaryLanguage` (type: `string`):

Language of the AI summary.

## Actor input object example

```json
{
  "bodies": [
    "Köln",
    "Dresden",
    "Freiburg"
  ],
  "topics": [
    "bau",
    "digital",
    "energie",
    "beratung",
    "mobilitaet",
    "vergabe",
    "geld"
  ],
  "daysBack": 7,
  "maxResults": 50,
  "minSignalScore": 20,
  "readMainFile": false,
  "timeLimitMinutes": 20,
  "summaryLanguage": "de"
}
```

# Actor output Schema

## `signals` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "bodies": [
        "Köln",
        "Dresden",
        "Freiburg"
    ],
    "topics": [
        "bau",
        "digital",
        "energie",
        "beratung",
        "mobilitaet",
        "vergabe",
        "geld"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("previsa/german-council-radar").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "bodies": [
        "Köln",
        "Dresden",
        "Freiburg",
    ],
    "topics": [
        "bau",
        "digital",
        "energie",
        "beratung",
        "mobilitaet",
        "vergabe",
        "geld",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("previsa/german-council-radar").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "bodies": [
    "Köln",
    "Dresden",
    "Freiburg"
  ],
  "topics": [
    "bau",
    "digital",
    "energie",
    "beratung",
    "mobilitaet",
    "vergabe",
    "geld"
  ]
}' |
apify call previsa/german-council-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,previsa/german-council-radar"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/X6dKbmf7J5HvJh37q/builds/jyd236VBcQvTNhu7B/openapi.json
