jobsuche-api

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

2posts · 30d
Nov 40 posts
  1. How to fetch German job listings from the Bundesagentur für Arbeit API with Python and Node.js (no scraping) --- title: "How to fetch German job listings from the Bundesagentur für Arbeit API with Python and Node.js (no scraping)" published: true description: "The federal job board has a JSON API behind its website. Here's how to query it directly, what can go wrong, and a ready-made option if you'd rather not maintain it yourself." tags: python, javascript, api, tutorial --- Germany's federal employment agency (Bundesagentur für Arbeit) runs the country's biggest job board, with hundreds of thousands of open positions. If you're building a job alert, a salary analysis or a dashboard of who's hiring in your city, you don't need to scrape HTML. The website runs on a JSON API you can call yourself. This post covers: 1. Calling the API directly with Python (requests) and Node.js (fetch). 2. The pitfalls you'll hit once you go past a quick demo. 3. A ready-made Apify Actor that handles them, if you'd rather not maintain that part yourself. > Disclosure: I built the Actor in part 3 (Arbeitsagentur Jobs API on Apify). Parts 1 and 2 work without it. 1. The raw API The job search at arbeitsagentur.de/jobsuche calls this endpoint: GET https://rest.arbeitsagentur.de/jobboerse/jobsuche-service/pc/v6/jobs Header: X-API-Key: jobboerse-jobsuche The X-API-Key isn't a personal secret. It's the public client ID the agency's own web app sends, and the community-maintained bundesAPI/jobsuche-api project documents it too. You don't need to sign up for anything. Useful query parameters: | Parameter | Meaning | |---|---| | was | What: keywords or a job title (Python, Pflegefachkraft, …) | | wo | Where: a city or postcode | | umkreis | Radius in km around wo | | veroeffentlichtseit | Only postings from the last n days (0–100) | | size, page | Paging (page starts at 1) | Python import base64 import requests BASE = "https://rest.arbeitsagentur.de/jobboerse/jobsuche-service" HEADERS = {"X-API-Key": "jobboerse-jobsuche"} # public client id used by the official web app def search(was, wo, umkreis=25, veroeffentlichtseit=7, size=50, max_pages=3): for page in range(1, max_pages + 1): r = requests.get(f"{BASE}/pc/v6/jobs", headers=HEADERS, timeout=30, params={ "was": was, "wo": wo, "umkreis": umkreis, "veroeffentlichtseit": veroeffentlichtseit, "size": size, "page": page, }) r.raise_for_status() data = r.json() jobs = data.get("ergebnisliste", []) yield from jobs if page * size >= int(data.get("maxErgebnisse", 0)) or not jobs: break for job in search("Python", "Berlin", max_pages=1, size=5): loc = job.get("stellenlokationen", [{}])[0].get("adresse", {}) ref = job["referenznummer"] print(job["stellenangebotsTitel"], "|", job.get("firma"), "|", loc.get("ort"), "|", f"https://www.arbeitsagentur.de/jobsuche/jobdetail/{ref}") # Details for a single posting: the reference number goes base64-encoded into the path detail_id = base64.b64encode(ref.encode()).decode() d = requests.get(f"{BASE}/pc/v4/jobdetails/{detail_id}", headers=HEADERS, timeout=30) print("detail status:", d.status_code, "| keys:", sorted(d.json())[:8] if d.ok else d.text[:100]) Output (03.11.2026, shortened): Senior Python Developer – Microservices & Data Pipelines (m/w/d) | BWI GmbH | Berlin | https://www.arbeitsagentur.de/jobsuche/jobdetail/12336-a26f539j0456738-S Versuchsingenieur (m/w/d) | FERCHAU GmbH Niederlassung Berlin | Berlin | https://www.arbeitsagentur.de/jobsuche/jobdetail/12265-485458_JB5289084-S ... detail status: 200 | keys: ['aenderungsdatum', 'allianzpartnerName', 'allianzpartnerUrl', ...] Node.js (18+, no dependencies) const BASE = 'https://rest.arbeitsagentur.de/jobboerse/jobsuche-service'; const headers = { 'X-API-Key': 'jobboerse-jobsuche' }; const params = new URLSearchParams({ was: 'Python', wo: 'Berlin', umkreis: '25', veroeffentlichtseit: '7', size: '5', page: '1', }); const res = await fetch(`${BASE}/pc/v6/jobs?${params}`, { headers }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); console.log(`${data.maxErgebnisse} results in total`); for (const job of data.ergebnisliste ?? []) { const city = job.stellenlokationen?.[0]?.adresse?.ort; console.log(`${job.stellenangebotsTitel} | ${job.firma} | ${city}`); } Each item in ergebnisliste has fields such as stellenangebotsTitel (title), firma (employer), referenznummer (reference number), stellenlokationen[].adresse (plz, ort, region), veroeffentlichungszeitraum.von (published date), arbeitszeitVollzeit (full-time) and homeofficemoeglich (remote possible). For the full description and salary, call the details endpoint /pc/v4/jobdetails/{base64(referenznummer)}. 2. Pitfalls once you go beyond a demo A one-off query is easy. A pipeline that runs every day for months hits these: - Result cap. The API returns at most about 10,000 results per query (page × size). A broad search like "all IT jobs in Germany" gets cut off silently. The fix is to split the query, for example by federal state or by location, and deduplicate the results. - German field names, nested structures. stellenangebotsTitel, stellenlokationen[0].adresse.ort, veroeffentlichungszeitraum.von… You'll want to map these to a flat, stable schema before anything downstream depends on them. - Details cost one request per job. Descriptions and salaries need a second call for every posting, so you need concurrency limits and retries. - "Only new since yesterday". For alerts you have to store the reference numbers you've already seen. - It's not an officially supported public API. Search (v6) and details (v4) already use different versions. Pin the paths and alert on errors. - Contact data is personal data. Descriptions often contain recruiters' e-mail addresses and phone numbers. If you don't need them, don't store them (GDPR). If you only need a few hundred jobs now and then, the script above is enough. Copy it and move on. 3. Ready-made option: the Apify Actor I needed this pipeline myself, so I packaged it as an Apify Actor. It: - automatically splits nationwide searches over 10,000 results by federal state, - maps the output to flat, English field names, with salary as {type, period, min, max}, - fetches details concurrently (optional, includeDetails), - remembers postings it has already seen (onlyNew), and - by default replaces e-mail addresses and phone numbers in descriptions with placeholders. You run it with the official apify-client and get a dataset back. Get a token from a free Apify account (Console → Settings → API & Integrations) and keep it in an environment variable: export APIFY_TOKEN=your_token_here Python (pip install apify-client) """Fetch German job listings (Bundesagentur für Arbeit) and save them as CSV. Actor: https://apify.com/paperlinenstudio/arbeitsagentur-jobs-api Usage: APIFY_TOKEN=... python jobs_to_csv.py """ import csv import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("paperlinenstudio/arbeitsagentur-jobs-api").call( run_input={ "keywords": ["Python Entwickler"], "locations": ["Berlin"], "radiusKm": 25, "publishedWithinDays": 7, # only jobs from the last week "maxItems": 20, } ) jobs = list(client.dataset(run.default_dataset_id).iterate_items()) with open("jobs.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["title", "employer", "city", "salary_min", "salary_max", "published", "url"]) for job in jobs: salary = job.get("salary") or {} writer.writerow([ job["title"], job["employer"], (job.get("location") or {}).get("city"), salary.get("min"), salary.get("max"), job["publishedFrom"], job["url"], ]) print(f"Saved {len(jobs)} jobs to jobs.csv") for job in jobs[:3]: print("-", job["title"], "|", job["employer"]) Saved 2 jobs to jobs.csv - Pythonentwickler:in (Schwerpunkt Api- und GIS-Entwicklung) | aconium GmbH Hauptsitz Berlin - DevOps Softwareentwickler (m/w/d) | FERCHAU GmbH Niederlassung Berlin Where the employer gives a salary, it comes back as numbers (the second job: 50000–75000), so there's no free text to parse. Node.js (npm i apify-client) // Fetch German job listings (Bundesagentur für Arbeit) and save them as CSV. // Usage: APIFY_TOKEN=... node jobs_to_csv.mjs import { writeFileSync } from 'node:fs'; import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('paperlinenstudio/arbeitsagentur-jobs-api').call({ keywords: ['Python Entwickler'], locations: ['Berlin'], radiusKm: 25, publishedWithinDays: 7, maxItems: 20, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const esc = (v) => `"${String(v ?? '').replaceAll('"', '""')}"`; const header = ['title', 'employer', 'city', 'salary_min', 'salary_max', 'published', 'url']; const rows = items.map((j) => [ j.title, j.employer, j.location?.city, j.salary?.min, j.salary?.max, j.publishedFrom, j.url, ].map(esc).join(',')); writeFileSync('jobs.csv', [header.join(','), ...rows].join('\n') + '\n'); console.log(`Saved ${items.length} jobs to jobs.csv`); for (const j of items.slice(0, 5)) console.log(`- ${j.title} | ${j.employer}`); Inputs worth knowing { "keywords": ["Data Engineer", "Data Scientist"], "locations": ["München", "Hamburg"], "radiusKm": 30, "publishedWithinDays": 1, "workingTime": ["full_time", "remote"], "includeDetails": true, "onlyNew": true, "maxItems": 500 } Add a daily schedule in the Apify Console and you have a job alert. Point a webhook at Slack, a Google Sheet or your own endpoint and you're done. Wrap-up - Small and occasional: call rest.arbeitsagentur.de directly with the snippets from part 1. - Recurring, large or needs a clean schema: use the Arbeitsagentur Jobs API Actor or borrow its ideas (state splitting, seen-ID store, field mapping). All the Actor examples are also in a small MIT-licensed examples repo. Questions or feature requests are welcome in the comments.

  2. Germany's job board has a free JSON API: 821,306 open jobs and what its facets reveal --- title: "Germany's job board has a free JSON API: 821,306 open jobs and what its facets reveal" published: true description: "The Bundesagentur für Arbeit job search has a public REST API with facet counts. Python code to query it, plus real numbers from today: temp agencies, salary transparency, new jobs per day and where software jobs are." tags: api, python, datascience, career --- If you build anything around jobs in Germany (a job alert, a recruiting dashboard, a labour-market chart), the largest source is the Bundesagentur für Arbeit (Federal Employment Agency) and its Jobsuche. What many people don't know is that the web app runs on a JSON API you can call directly, and that every response includes facet counts for the whole result set. I queried it on 24 October 2026. Below: how to call it, and what the numbers say. Disclosure: I built the Job Search API actor on Apify, which uses this endpoint among other sources. You don't need it for anything in this post; plain Python is enough. The endpoint GET https://rest.arbeitsagentur.de/jobboerse/jobsuche-service/pc/v6/jobs Header: X-API-Key: jobboerse-jobsuche The key is the public client ID that the official Jobsuche web app sends. It's not a secret, and the community project bundesAPI/jobsuche-api documents the parameters. Useful ones: | Parameter | Meaning | |---|---| | was | Keyword, e.g. Softwareentwickler, Python, Pflegefachkraft | | wo / umkreis | Place and radius in km, e.g. Berlin / 25 | | angebotsart | Offer type. 1 = regular jobs (other codes cover apprenticeships, internships and self-employment) | | veroeffentlichtseit | Published in the last N days (max 100) | | page / size | Pagination, up to 100 results per page | A minimal call: import requests URL = "https://rest.arbeitsagentur.de/jobboerse/jobsuche-service/pc/v6/jobs" HEADERS = {"X-API-Key": "jobboerse-jobsuche"} def search(**params): params = {"angebotsart": 1, "size": 100, **params} r = requests.get(URL, params=params, headers=HEADERS, timeout=30) r.raise_for_status() return r.json() data = search(was="Softwareentwickler", wo="München", umkreis=25) print(data["maxErgebnisse"]) # total matches for job in data["ergebnisliste"][:5]: print(job["stellenangebotsTitel"], "|", job["firma"]) Each job has the title, employer, location with coordinates, publish date, a reference number (referenznummer) and, when the employer states one, a salary range (gehaltsspanneVon / gehaltsspanneBis). The detail page is https://www.arbeitsagentur.de/jobsuche/jobdetail/<referenznummer>. The part most people skip: facets Next to the results, each response has a facetten object with counts for the whole result set: employers, cities, occupations, working time, temp agency or not, salary stated or not. Ask for size=1 and you get labour-market statistics in one request. f = search(size=1)["facetten"] print(f["zeitarbeit"]["counts"]) # temp agency yes/no print(f["veroeffentlichtseit"]["counts"]) # new in last 1/7/14/28 days Here's what that showed on 24 October 2026. 1. 821,306 open jobs, about 17,000 new per day | Published within | Jobs | |---|---| | last day | 17,448 | | last 7 days | 86,127 | | last 28 days | 316,370 | | all open | 821,306 | So roughly 2% of the stock is new each day. If you build an alert, polling once a day with veroeffentlichtseit=1 is enough and keeps requests small. 2. One in four jobs is with a temp agency The zeitarbeit facet marks 214,738 of 821,306 jobs (26%) as temporary agency work. The employer facet shows the same thing: the top employers by open postings are Netto (15,029), TimePartner (13,381), Randstad (9,620), Sanovetis (9,434) and PerZukunft (7,703). At least three of those five are staffing or placement firms. If you count "hiring companies" from this data, filter out agency postings first. Otherwise a few staffing firms swamp your numbers. 3. Only about a quarter state a salary The verguetung facet counts jobs with a stated pay figure: 152,586 hourly and 65,735 yearly, together 218,321 (27%). It is similar within fields: - Softwareentwickler: 812 of 3,345 (24%) state pay, almost all as a yearly figure - Pflegefachkraft (registered nurse): 5,245 of 23,263 (23%) So the salary fields are useful, but don't treat a missing value as "unpaid" or "low". Three quarters of postings simply don't say. 4. Software jobs: Munich beats Berlin within 25 km | Search | Open jobs | |---|---| | Softwareentwickler, all Germany | 3,345 | | … within 25 km of München | 241 | | … within 25 km of Berlin | 189 | | … published in the last 7 days | 444 | | Python, all Germany | 2,307 | | Pflegefachkraft, all Germany | 23,263 | Across all occupations Berlin has by far the most postings (35,344, against 15,854 for München). For software developers within a 25 km radius, Munich comes out ahead. Nursing has about seven times as many open postings as software development. In a sample of 100 Python jobs, 20 had homeofficemoeglich: true, and Baden-Württemberg (23) and Bayern (21) led the regions. Gotchas - Keyword search is broad. was=Python also matches jobs where Python is just one skill among many. Filter titles afterwards if you need precision. - Totals move during the day. Postings are added and removed all the time, so store the timestamp with any number you publish. - Deduplicate by referenznummer, not by title. The same title appears hundreds of times (Verkäufer/in alone has 37,138 postings). - The detail endpoint (full description) is a separate call per job, so only fetch it for jobs you keep. - Be polite. It's a public service. Cache results, use the facets instead of paging through everything, and don't hammer it. When to use a ready-made actor instead For one script, the code above is all you need. My Job Search API is for when you want the Bundesagentur results merged with other open sources in one deduplicated list: Arbeitnow (Germany and Europe), Remote OK and Himalayas (remote jobs), plus company career pages on Greenhouse, Lever, Ashby and SmartRecruiters. All results share one schema (title, company, location, remote, salary, posted date, URL, source), and it runs on a schedule without a server. It costs $2.50 per 1,000 jobs. If you're watching specific companies rather than keywords, the Career Site Jobs API can return only postings that are new since the last run. Takeaways 1. Germany's biggest job board has a JSON API. One header, no signup. 2. Use size=1 and the facetten object to get market statistics in a single request. 3. Before drawing conclusions, account for temp agencies (26% of postings) and missing salaries (73%). All numbers are from queries on 24 October 2026, around 07:00 UTC.

    1 reaction

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

Scan pending.

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
bundesAPI/jobsuche-api mentions in the last 30 days