# Arbeitsagentur Jobs Scraper: Bundesagentur Jobbörse (`datahamster/bundesagentur-jobs`) Actor

Scrape arbeitsagentur jobs from the Bundesagentur für Arbeit Jobbörse: search by keyword, place and radius, filter by job type and publication date, get one flat row per posting with employer, location, coordinates, dates and the full description. Monitor mode alerts on new postings.

- **URL**: https://apify.com/datahamster/bundesagentur-jobs.md
- **Developed by:** [Viktor Dubnytskiy](https://apify.com/datahamster) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 result items

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Arbeitsagentur Jobs Scraper: Bundesagentur Jobbörse

Scrape the **Bundesagentur für Arbeit** job board (arbeitsagentur.de/jobsuche) — Germany's largest job database with well over a million public postings. Search by keyword, place and radius, filter by offer type and publication date, and get one flat row per posting: employer, address, coordinates, dates, salary range and, optionally, the full description.

### What you get

One row per posting: `id` (= `refnr`), `url`, `title`, `occupation`, `employer`, `employerHash`, `city`, `zip`, `region`, `country`, `lat`, `lon`, `publishedAt`, `firstPublishedAt`, `startsAt`, `modifiedAt`, `jobType`, `trainingType`, `fullTime`, `homeOffice`, `homeOfficeType`, `contractDuration`, `salaryFrom`, `salaryTo`, `salaryFixed`, `salaryUnit`, `salaryUnitRaw`, `salaryCurrency`, `apprenticeshipSalaryYear1`, `apprenticeshipSalaryYear2`, `apprenticeshipSalaryYear3`, `externalUrl`, `distanceKm`, `otherLocations`, `query`, `page`, `rank`, `source`, `scrapedAt`.

**Salary always carries its unit.** `salaryUnit` is `hour`, `month`, `year` or `apprenticeship_month` (`salaryUnitRaw` keeps the source code, e.g. `STUNDENLOHN`), `salaryCurrency` is `EUR`, and an employer who published one fixed amount instead of a range gets it in `salaryFixed` and on both ends of `salaryFrom`/`salaryTo`, so a numeric filter works on either shape. Apprenticeships list their monthly pay per training year.

With `fetchDetails: true` each row also carries `description`, `employerDescription`, `employerSize`, `employerHq`, `employerWebsite` and `benefits`.

Employer-level facts only. Postings that name a contact person are **not** mined for that person: no names, no personal phone numbers, no personal e-mail addresses.

### Example output

Real rows from the example dataset (search `{"was": "python", "wo": "Berlin"}`):

| Field | Example value |
|---|---|
| `id` / `refnr` | `10001-1003356732-S` |
| `url` | `https://www.arbeitsagentur.de/jobsuche/jobdetail/10001-1003356732-S` |
| `title` | `AI Lead, Python Tools (m/f/d)` |
| `employer` / `occupation` | `JetBrains GmbH` / `Software-Architect` |
| `city` / `zip` / `region` | `Berlin` / `10997` / `BERLIN` |
| `lat` / `lon` | `52.499898` / `13.4472735` |
| `publishedAt` / `modifiedAt` | `2026-07-09` / `2026-09-14T00:10:43.373` |
| `jobType` / `homeOffice` | `ARBEIT` / `true` |
| `employerSize` / `employerHq` | `1000+` / `München` (with `fetchDetails`) |

Salary, as published: a yearly range comes back as `salaryFrom: 34000`, `salaryTo: 52000`, `salaryUnit: "year"`, `salaryCurrency: "EUR"`; an hourly fixed rate as `salaryFixed: 17.25`, `salaryFrom: 17.25`, `salaryTo: 17.25`, `salaryUnit: "hour"`; an apprenticeship as `apprenticeshipSalaryYear1..3: 1625` with `salaryUnit: "apprenticeship_month"`.

### Use cases

- Build a German job feed for one keyword and region and refresh it daily.
- Watch a search and get a webhook or Telegram alert the moment a new posting appears.
- Map hiring by employer, occupation and postcode — every row carries coordinates.
- Pull apprenticeships (`jobType: AUSBILDUNG`) or internships (`PRAKTIKUM`) for a school or university job wall.

### Input

| Field | Meaning | Default |
|---|---|---|
| `searches` | `[{"was": "python", "wo": "Berlin"}]`; a plain string is taken as `was` | `[{"was": "python", "wo": "Berlin"}]` |
| `radiusKm` | Radius around `wo` in km | `25` |
| `jobType` | `any`, `ARBEIT`, `AUSBILDUNG`, `PRAKTIKUM`, `SELBSTAENDIGKEIT` | `any` |
| `publishedSinceDays` | Only postings published in the last N days | empty |
| `maxPagesPerSearch` | Result pages per search (25 postings each) | `3` |
| `fetchDetails` | Open each posting for description and employer facts | `false` |
| `maxItems` | Stop after this many rows | `100` |
| `mode` | `scrape` or `monitor` (only new/changed since last run) | `scrape` |
| `monitorKey`, `webhookUrl`, `telegramBotToken`, `telegramChatId` | Monitor-mode state key and alert targets | empty |

### Monitor mode for new postings

Run the actor as a saved task with `mode: monitor` on a schedule. Each run compares the current result set against the previous one and returns only new postings and postings whose title, employer or modification date changed, optionally pushing a summary to a webhook or a Telegram chat. You are charged a monitor-check event per run plus a change event per changed posting — never for unchanged rows.

### Pricing

| Event | Price |
|---|---|
| result | $0.001 per posting ($1 per 1,000) |
| monitor-check | $0.005 per monitor run |
| change | $0.001 per new or changed posting |

Charged only for rows actually pushed. **A run that finds nothing costs nothing**: no rows, no result events.

### How it works

1. Each search is turned into a public Jobbörse search URL (`was`, `wo`, `umkreis`, `angebotsart`, `veroeffentlichtseit`, `page`) and read from the server-rendered page, which carries the same structured objects the site's own front end uses — so the rows are the source's own fields, not scraped label text.
2. Pagination stops at the last page of the result set, so a search with 30 hits costs two pages, not `maxPagesPerSearch`.
3. `fetchDetails` opens each posting for the description and the employer's business profile. A posting removed between the list and the detail page keeps its search row instead of disappearing.
4. No proxy tier is needed; the board answers plain requests from a German exit.

### Limits

- Result pages hold 25 postings each; the board itself caps a search result set, so very broad searches are best split by place or occupation.
- `hashId` (the id of the mobile REST API) is not published on the web pages and stays empty; `refnr` is the stable id and the one the detail URL uses.
- Salary is only present when the employer published one (most German postings do not); when it is absent, `salaryUnit` and `salaryCurrency` stay empty too, so a missing salary is never readable as 0 EUR.
- Descriptions are the employer's own text, in German unless the employer wrote in another language.

### FAQ

**Is this the official API?** No. The Jobbörse REST API needs a key and refused platform traffic when this actor was built (403), so the actor reads the public web pages and the structured payload they embed.

**Does it return contact persons?** No. Only employer-level facts. Postings that print a contact person are not mined for personal data.

**What happens when a search has no hits?** No rows are pushed and no result events are charged. The `RUN_SUMMARY` record in the run's key-value store carries `emptyReason: no_matches`, which is kept strictly apart from `blocked` — an empty dataset never hides a refusal.

**Can I search all of Germany?** Yes — leave `wo` out of the search object.

### Changelog

- 0.1: initial release — keyword/place search with radius, offer-type and freshness filters, optional detail pages with employer facts, monitor mode.

***

If this actor saved you time, a short review on its Store page genuinely helps other people find it. Found a bug or need a field that is missing? Open a ticket on the **Issues** tab.

# Actor input Schema

## `maxItems` (type: `integer`):

Stop after this many results (you are charged only for pushed items)

## `mode` (type: `string`):

scrape = full results; monitor = only new/changed items since the previous run of this task

## `monitorKey` (type: `string`):

Optional state key when not running as a saved task

## `webhookUrl` (type: `string`):

POST a change summary here in monitor mode

## `telegramBotToken` (type: `string`):

Optional: bot token for monitor-mode change summaries

## `telegramChatId` (type: `string`):

Optional: chat id that receives monitor-mode summaries

## `searches` (type: `array`):

List of searches as JSON objects: {"was": "python", "wo": "Berlin"}. `was` is the search term (job title, skill, employer) typed into the Jobbörse; `wo` is a city, postcode or region and drives the radius search — leave it out to search all of Germany. A plain string is taken as `was`, e.g. "pflege". Example: \[{"was": "python", "wo": "Berlin"}, {"was": "pflege", "wo": "Hamburg"}].

## `radiusKm` (type: `integer`):

Search radius in km around the place given in `wo`, e.g. 25. Ignored for searches without a place. Range 0-200.

## `jobType` (type: `string`):

Restrict the search to one offer type of the Jobbörse, e.g. AUSBILDUNG for apprenticeships and dual study places. Default: any.

## `publishedSinceDays` (type: `integer`):

Only postings published within this many days, e.g. 7 for the last week. Leave empty for all postings.

## `maxPagesPerSearch` (type: `integer`):

How many result pages to read per search; one page holds 25 postings, so 3 pages = up to 75 rows per search. Range 1-40.

## `fetchDetails` (type: `boolean`):

true = open every posting to add the full description text and the employer's business facts (size, headquarters, website, benefits) — one extra request per posting. false = the search row only, which already carries employer, location, coordinates and dates. Example: false.

## Actor input object example

```json
{
  "maxItems": 100,
  "mode": "scrape",
  "searches": [
    {
      "was": "python",
      "wo": "Berlin"
    }
  ],
  "radiusKm": 25,
  "jobType": "any",
  "publishedSinceDays": 7,
  "maxPagesPerSearch": 3,
  "fetchDetails": false
}
```

# Actor output Schema

## `results` (type: `string`):

All pushed rows (dataset, JSON)

## `resultsTable` (type: `string`):

Dataset in the Console viewer

## `runSummary` (type: `string`):

RUN\_SUMMARY record

# 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 = {
    "searches": [
        {
            "was": "python",
            "wo": "Berlin"
        }
    ],
    "radiusKm": 25,
    "publishedSinceDays": 7,
    "maxPagesPerSearch": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("datahamster/bundesagentur-jobs").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 = {
    "searches": [{
            "was": "python",
            "wo": "Berlin",
        }],
    "radiusKm": 25,
    "publishedSinceDays": 7,
    "maxPagesPerSearch": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("datahamster/bundesagentur-jobs").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 '{
  "searches": [
    {
      "was": "python",
      "wo": "Berlin"
    }
  ],
  "radiusKm": 25,
  "publishedSinceDays": 7,
  "maxPagesPerSearch": 3
}' |
apify call datahamster/bundesagentur-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datahamster/bundesagentur-jobs"
        }
    }
}
```

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/lUxpc43sBFZonKjIS/builds/eukmQAbnrq4awt9jb/openapi.json
