# Arbeitsagentur Jobs Scraper (Germany) (`hexagone-data/arbeitsagentur-jobs-scraper`) Actor

Scrape the Bundesagentur für Arbeit Jobsuche: whole searches read by publication date, beyond the site's 10,000-row limit with no setup, the same list as the site's page, full ad included. Pay per job.

- **URL**: https://apify.com/hexagone-data/arbeitsagentur-jobs-scraper.md
- **Developed by:** [Hexagone Data](https://apify.com/hexagone-data) (community)
- **Categories:** Jobs, Automation
- **Security:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.53 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Arbeitsagentur Jobs Scraper (Germany)

Scrape the **Jobsuche of the Bundesagentur für Arbeit**, Germany's public job board. Type a search term or paste the URL your browser shows, filters included, and get one
record per job: title, company, every location, salary, contract, working time, dates, apprentice pay, and the full ad text.

What it does, measured on the site on 2026-11-03:

- **It reads every search by first publication date, 500 rows at a time.** The site's default order changes from one page to the next, so reading it page after page repeats some jobs and never shows others, while the row count looks
  complete: 1,786 distinct jobs out of 1,797 for `Verkäufer` around Hamburg; for `Koch`, out of 8,507: 8,446 at 50 rows per page, 8,480 to 8,495 at 100, 8,502 at 500. Its runs on Apify, checked against separate reads of the site: `Verkäufer` around Hamburg 1,792 of 1,792, `Koch` 8,509 for 8,509 announced (one job replaced between the two reads), while a read at the site's order the same hour found 1,765 and 8,483.
  If the list moves while a slice is read (a job removed, a job posted), that slice is read again, up to 3 times in all.
- **Beyond 10,000 results with no setup.** The site refuses to go further than 10,000 rows. The Actor splits a larger search by the site's own filters and puts it back together, each job
  delivered once: 17,588 of 17,588 for `Büro`, 13,698 of 13,698 for `Elektriker`, 121,786 for all jobs in Bavaria (`angebotsart=1`; 121,779 announced at the end, jobs are removed during a run), 188,700 for every apprenticeship (188,687 announced at the end).
- **The same list as the site's page, by default.** The site's page hides the offers of private placement agencies and lists apprenticeships apart; so does this Actor, unless you ask (for `Steinmetz`:
  386 jobs on the page and here, plus 17 from private agencies with `includePrivateAgencies`, and 212 apprenticeships with `offerType = AUSBILDUNG`).
- **The full ad is included.** Same price with or without it. The list and the job page disagree on some jobs (17 of 200 had more locations on the job page): the job page wins.
- **The run tells you what it got.** Distinct jobs against the total announced at the start and read again at the end, with both numbers if anything is missing.

### Try it with 20 jobs

1. Keep the prefilled search term (`Steinmetz`) and **Maximum number of jobs = 20**.
2. Click **Start**. You pay for 20 jobs: $0.020 (full ads included, start fee included).
3. Open **Jobs overview** for the table, **Run summary** for what was collected and why anything is missing.

### What to paste

Type words in **Search terms** and use the filters of the form, or paste into **Start URLs**:

| You paste | Example | What you get |
|---|---|---|
| A search | `https://www.arbeitsagentur.de/jobsuche/suche?angebotsart=1&was=Koch&wo=Hamburg&umkreis=25` | every job of the search, filters kept as in the URL |
| An apprenticeship search (Ausbildung) | `https://www.arbeitsagentur.de/jobsuche/suche?was=Steinmetz&suchbereich=ausbildung` (or `angebotsart=4`) | apprenticeships and dual studies |
| The URL copied after clicking a job on the list | `https://www.arbeitsagentur.de/jobsuche/suche?angebotsart=1&was=Steinmetz&suchbereich=jobs&id=14751-373A144846-S` | the whole search, not only that job |
| A job | `https://www.arbeitsagentur.de/jobsuche/jobdetail/14751-373A144846-S`, or `suche?id=<reference>` with no search | that job with its details |
| Search with filters | `...&arbeitszeit=vz;tz&befristung=2&veroeffentlichtseit=28&zeitarbeit=false` | same filters as the site |
| Every job of the site | `https://www.arbeitsagentur.de/jobsuche/suche` with **Maximum number of jobs = 0** | the whole board, only if you ask for it (735,911 jobs when measured): see Known limits |

Supported filters, the site's own list: `was`, `wo`, `umkreis`, `suchbereich`, `angebotsart`, `pav`, `arbeitsort`, `arbeitszeit`, `befristung`, `betreuung`, `veroeffentlichtseit`, `zeitarbeit`, `verguetung`, `ausbildungsverguetung`, `behinderung`,
`quereinstieg`, `externestellenboersen`, `berufsfeld`, `beruf`, `weitereberufe`, `branche`, `arbeitgeber`, `kundennummer`, `homeoffice`, `eintrittsdatum`, `ausbildungsart`, `schulbildung`, `koordinaten`, `wb`. They are sent as written; if the site refuses a value (HTTP 400), the run reports it, and names the parameter when the site does.
What the browser adds on its own (`id` with a search, `page`, `size`, `sort`, `aktualisiertVor`, `as`) is removed and named in the run summary (`droppedParams`), and the whole search is read. Any other parameter (`utm_source`...) is not a filter of the site and has no effect: it is removed, with a warning.
As on the page, `pav=true` adds private agencies and a missing `pav` leaves them out. An address with no real filter left (`suche?utm_source=x`, `suche?page=2`, `koordinaten` without `wo`) is **rejected**: it would read the whole site. So is any other address (another site, `/ausbildungssuche/`, the home
page), **with the reason**; the other addresses of the run are still read. `arbeitsagentur.de` without `www` works. A remote list of URLs (`requestsFromUrl`) is rejected: paste the URLs themselves.

An empty form is refused: reading the whole board is something you ask for on purpose, by its URL.

### Output

One record per job and run (`id` is the key: a job found by two of your searches is delivered once). Unknown values are `null`, never `""`, `0` or `false`: the site omits many fields. Every field
is listed in the dataset schema. Real record of a run on Apify on 2026-11-03, **abridged** (ad text shortened, some fields left out):

```json
{
  "id": "10001-1003955562-S",
  "url": "https://www.arbeitsagentur.de/jobsuche/jobdetail/10001-1003955562-S",
  "title": "Pflege(fach)helfer (m/w/d)",
  "offerType": "ARBEIT",
  "occupation": "Altenpflegehelfer/in",
  "allOccupations": ["Altenpflegehelfer/in"],
  "companyName": "Bayerisches Rotes Kreuz Kreisverband Rottal-Inn",
  "locations": [{"street": "Pfarrkirchener Str.", "houseNumber": "53", "district": null, "postalCode": "84307", "city": "Eggenfelden", "region": "BAYERN", "country": "DEUTSCHLAND", "latitude": 48.405294, "longitude": 12.767409}],
  "city": "Eggenfelden",
  "postalCode": "84307",
  "region": "BAYERN",
  "country": "DEUTSCHLAND",
  "fullTime": null,
  "miniJob": null,
  "contractDuration": "KEINE_ANGABE",
  "startDate": "2026-11-03",
  "salaryType": "KEINE_ANGABEN",
  "salaryMin": null,
  "homeOffice": null,
  "publishedAt": "2026-11-03",
  "firstPublishedAt": "2026-11-03",
  "modifiedAt": "2026-11-03T13:46:59.232",
  "sourceUrl": "https://www.arbeitsagentur.de/jobsuche/suche?angebotsart=1&wo=Bayern&berufsfeld=Altenpflege",
  "scrapedAt": "2026-11-03T12:52:03Z",
  "detailsStatus": "ok",
  "description": "Das Bayerische Rote Kreuz – Körperschaft des öffentlichen Rechtes – ist einer der größten ...",
  "isManagedByEmploymentAgency": false,
  "partnerName": "arbeitsagentur.de"
}
```

`detailsStatus`: `ok`; `failed` (the job page could not be read, the job stays delivered from the list); `skipped` (details stopped for this run after repeated refusals); `not_requested`.

### Run summary

Each run writes a readable **Run summary** (and the same as JSON in `OUTPUT`), per start URL: `status`, `jobs`, `jobsDistinct` (distinct jobs seen for this URL), `totalAnnounced`, `totalAtEnd` (read again after the
last page), `missing`, `slices`, `slicesOver10000`, `slicesReread` (extra readings because the list moved), `slicesStillMoving`, `passes`, `droppedParams`, `orderNote`, `detail`. For the run: `detailsRead`, `detailsFailed`, `detailsStopped`, `accessRefusals`, `anomalies`.

| Status | Meaning |
|---|---|
| `done` | every distinct job counted against the total read at the end; at most 1 job or 0.05% missing, shown with its number (warning `MISSING_WITHIN_TOLERANCE`) |
| `partial_collection` | something is missing: both numbers and the cause; warning `PARTIAL_COLLECTION` |
| `stopped_at_limit` | your `maxItems` or maximum charge was reached |
| `no_results` | the site announced 0 jobs: the search is empty, the URL is wrong or the site is down (`NO_RESULTS` warning) |
| `no_new_jobs` | every job of this URL was already delivered by an earlier URL |
| `job_not_available` | a pasted job does not exist any more; no job charged |
| `blocked_by_site`, `rate_limited`, `network_error`, `http_error`, `not_found`, `site_maintenance` | the site refused or did not answer even after new sessions: never reported as an empty search |
| `unrecognized_page`, `structure_changed` | the answer is not a search result the Actor can read; never reported as an empty search |
| `not_processed`, `interrupted`, `internal_error` | the URL was not reached or finished (limit reached before it, run stopped, unexpected error) |

If jobs are missing after the last page, the first page of each slice is read again, and the slices that still lack jobs are read to the end; at most 3 readings per slice in all, passes included. Only what was missing is added, nothing is delivered or charged twice. A job delivered without its full ad is counted in `detailsFailed` or `detailsStopped` and in the warning `DETAILS_FAILED`. A run where no URL delivers anything **fails** with the exact cause.
Warnings are listed under **Checks**; "Checks passed" is shown only when there is none. Codes starting with `ANO_` come from the shared run checks.

### Pricing

Pay per event: **$0.75 per 1,000 jobs**, **the full ad included**: the same price with or without `includeDetails`, and for a pasted job URL. Plus a **$0.005 start fee per run** (charged by Apify once per GB of memory; the default is 1 GB).
Apify subscribers pay less automatically, on every event: 10% off on the Bronze plan, 20% on Silver, 30% on Gold and above.

| Jobs | With full ads, start fee included | Without full ads, start fee included |
|---|---|---|
| 20 | $0.020 | $0.020 |
| 1,000 | $0.755 | $0.755 |

Only jobs written to your dataset are paid, each once, even if two of your searches find it or the list is read twice. Your maximum charge per run is respected: the run stops cleanly before the job that would exceed it, and no ad is read for a job that could not be paid.

### Known limits

- **Six German states have a place of the same name.** Typed as is, `Sachsen` gives a place in Bavaria (`Koch`: 32 jobs, against 484 with `Sachsen (Bundesland)`), `Hessen` a place in the north, none of its jobs in Hesse (35 against 462), `Brandenburg` the city (8 against 276). Write `Berlin (Bundesland)`, `Brandenburg (Bundesland)`, `Bremen (Bundesland)`, `Hamburg (Bundesland)`, `Hessen (Bundesland)` or `Sachsen (Bundesland)` for the state. For Sachsen, Hessen and Brandenburg, in any case, the run warns (`LOCATION_NOT_THE_STATE`); `Berlin`, `Hamburg` and `Bremen` alone give the city and its surroundings, without a warning.
- **Always newest first, by first publication date** (`firstPublishedAt`), not by `publishedAt`, which the site moves when an ad is renewed: with a small `maxItems` you get the jobs first published most recently, and a job with today's `publishedAt` can sit far down the list. A `sort` in your URL is ignored (`orderNote` says so), because the site's own order loses jobs.
- **A move that leaves no trace.** A job removed and a job posted between two pages of the same slice cancel each other out, with no repeated row and no drop in the total: it can hide a job and nothing shows it. Likewise, a job that moves into a slice already read, while another leaves it: the total does not change. Rare, not quantified. A slice still moving after 3 readings is reported (`SLICE_KEPT_MOVING`).
- **No e-mails or phone numbers extracted**, on purpose: they are not fields of the site, only text written by the employer. The ad text stays delivered as published. **Contacts protected by the site's captcha** are not read.
- **No incremental mode**: each run reads what you ask, from the newest down, and has no memory of earlier runs.
- **Reading the whole country is long and needs more memory**, and has not been run: largest run measured, 188,700 jobs without ads in 18 minutes using 427 MB. At that rate, the 824,166 jobs with private agencies would take about 1 h 20 without ads and about 1.8 GB (calculated): **set 4 GB** in the run options. Ads add about 20 per second (1,792 in 88 s), about 11 hours; repeated readings of slices that move add time, not measured. The default run timeout is 24 hours. A search too large for the run's memory gets a `MEMORY_LOW` warning at its start, with the value to set.
- **Over 10,000 jobs**: the site's filters split the search. A slice still above 10,000 with no filter left gives its newest 10,000 rows and `partial_collection` with the numbers, never `done`. The last resort, by German state, is taken only if the 16 states hold at least 99.5% of the slice (not often the case for apprenticeships: 94%); otherwise, or if some jobs stay out, the numbers are reported. Measured dry on the site: all jobs 824,166 in 853 slices, none above 10,000; apprenticeships 188,659 in 166 slices, one above 10,000 (10,474).
- Jobs removed while the list is read still count as seen, so `jobsDistinct` can pass `totalAtEnd`. New jobs keep coming during a long run: a few may be missing at the end.
- A job whose ad could not be read is still delivered and charged: its list data is complete, `detailsStatus` tells which. After 10 refused ads in a row, the run pauses 60 s and tries one; if it is refused, the pause doubles; after 3 refused tries, ads stop for the run (`detailsStopped`).
- No proxy is used by default: no refusal in any measured run from Apify's servers. If the site refuses, the run retries, then falls back to Apify's datacenter proxy; this fallback has been tested offline only.

### Support

Questions, a URL that does not work, a field you need: open an issue on the Actor's **Issues** tab with the run link.

This Actor is not affiliated with, endorsed or sponsored by the Bundesagentur für Arbeit. The name is used only to describe the website the data comes from. Only publicly visible job ads are collected;
ad texts can contain personal data such as a contact name: use the data in line with the site's terms and the privacy laws that apply to you.

# Actor input Schema

## `searchTerms` (type: `array`):

One search per term, as typed in the site's 'Was suchen Sie?' box (e.g. Koch, Steinmetz). The filters below apply to these searches.

## `startUrls` (type: `array`):

Jobsuche URLs exactly as your browser shows them, filters included: a search (https://www.arbeitsagentur.de/jobsuche/suche?angebotsart=1\&was=Koch\&wo=Hamburg\&umkreis=25), the URL copied after clicking a job on the list (the whole search is read), or a job (https://www.arbeitsagentur.de/jobsuche/jobdetail/<reference>). Read in addition to the search terms; a URL keeps its own filters. A remote list of URLs is not supported. To read every job of the site, paste https://www.arbeitsagentur.de/jobsuche/suche on purpose, with Maximum number of jobs = 0.

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

Stops at exactly this number across all searches, newest first (by first publication date, firstPublishedAt). 0 = every job of every search.

## `includeDetails` (type: `boolean`):

Reads each job's own page data: full ad text, required education, private placement and temporary staffing flags, employer partner. Same price per job with or without; turn it off only to go faster.

## `location` (type: `string`):

As in the site's 'Wo suchen Sie?' box: a city, a German state (Bayern) or a country (Österreich). Six states need the site's exact form, otherwise the site reads a place of that name: Berlin (Bundesland), Brandenburg (Bundesland), Bremen (Bundesland), Hamburg (Bundesland), Hessen (Bundesland), Sachsen (Bundesland).

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

0, 5, 10, 25, 50, 100 or 200, as on the site. Needs a location. Empty: sent as the site's page does without a radius.

## `offerType` (type: `string`):

The site lists apprenticeships apart: choose AUSBILDUNG to get them.

## `includePrivateAgencies` (type: `boolean`):

Off, as the site's default page. On adds the offers of private placement agencies (measured on a search: 17 more jobs on 386).

## `includeTemporaryAgencies` (type: `boolean`):

On, as the site's default page. Off keeps only jobs not offered by temporary staffing.

## `publishedWithinDays` (type: `integer`):

0 (today), 1, 7, 14 or 28, as on the site; the site ignores any other value. Empty = any date.

## `contractType` (type: `string`):

Empty = all.

## `workingTime` (type: `array`):

Any of: vz (full time), tz (part time), snw (shift, night, weekend), mj (mini job).

## `employer` (type: `string`):

Exact employer name as the site writes it.

## `occupationField` (type: `string`):

Exact name of the site's field, e.g. Büro und Sekretariat.

## `proxyConfiguration` (type: `object`):

Leave empty: the site's API answered without a proxy when measured; if the site refuses the requests, the run moves to the Apify datacenter proxy by itself. Your own setting is always respected.

## Actor input object example

```json
{
  "searchTerms": [
    "Steinmetz"
  ],
  "maxItems": 20,
  "includeDetails": true,
  "offerType": "jobs",
  "includePrivateAgencies": false,
  "includeTemporaryAgencies": true
}
```

# Actor output Schema

## `jobs` (type: `string`):

All job records, one per job offer, with every field.

## `jobsOverview` (type: `string`):

Title, company, city, offer type, salary, dates, link.

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

Readable per-URL summary: status, distinct jobs against the total announced at the start and at the end, slices, reason for anything missing.

## `summaryJson` (type: `string`):

The same summary as JSON (OUTPUT 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 = {
    "searchTerms": [
        "Steinmetz"
    ],
    "maxItems": 20,
    "includeDetails": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("hexagone-data/arbeitsagentur-jobs-scraper").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 = {
    "searchTerms": ["Steinmetz"],
    "maxItems": 20,
    "includeDetails": True,
}

# Run the Actor and wait for it to finish
run = client.actor("hexagone-data/arbeitsagentur-jobs-scraper").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 '{
  "searchTerms": [
    "Steinmetz"
  ],
  "maxItems": 20,
  "includeDetails": true
}' |
apify call hexagone-data/arbeitsagentur-jobs-scraper --silent --output-dataset

```

## MCP server setup

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

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/E28SOvLNHZXSXj3s1/builds/6SBE8zhzuJ7czoESd/openapi.json
