HR WORKS Jobs API.
Pull structured vacancies from German employers running HR WORKS, where every tenant board ships a complete schema.org JobPosting you can parse without an API key.
Try the API.
Test Jobs and Feed endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.
What's in every response.
Data fields, real-world applications, and the companies already running on HR WORKS.
- Full Job Descriptions
- Employment Type
- Posting & Closing Dates
- Structured Locations
- Hiring Organization Name
- Direct Apply URLs
- 01German Job-Market Tracking
- 02Employer Vacancy Monitoring
- 03Recruitment Data Aggregation
- 04Careers Page Indexing
How to scrape HR WORKS.
Step-by-step guide to extracting jobs from HR WORKS-powered career pages—endpoints, authentication, and working code.
import requests
company_id = "x8e2d70" # 7-char public tenant key from the board URL
language = "de" # HR WORKS serves "de" or "en" routes
base = "https://jobapplication.hrworks.de"
listing_url = f"{base}/{language}?companyId={company_id}"FINGERPRINTS = (
"HrwMeCustomerJobOffersNestedView",
"HrwMeJobApplicationManagementSession",
)
resp = requests.get(listing_url, headers={"Accept": "text/html"}, timeout=15)
resp.raise_for_status()
html = resp.text
if not any(marker in html for marker in FINGERPRINTS):
raise RuntimeError("Document is missing the HR WORKS application fingerprint")import re
from urllib.parse import urljoin, urlparse, parse_qs
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
jobs, seen = [], set()
for a in soup.select("a.job-offer-content[href]"):
href = urljoin(listing_url, a["href"])
query = parse_qs(urlparse(href).query)
if query.get("companyId", [None])[0] != company_id:
continue
job_id = (query.get("id", [None])[0] or "").lower()
if not re.fullmatch(r"[a-f0-9]{6}", job_id) or job_id in seen:
continue
heading = a.h2.get_text(strip=True) if a.h2 else ""
title = (a.get("title") or heading).strip()
if not title:
continue
seen.add(job_id)
jobs.append({
"id": job_id,
"title": title,
"url": f"{listing_url}&id={job_id}",
"apply_url": f"{base}/{language}/apply?companyId={company_id}&id={job_id}",
})
print(f"Found {len(jobs)} jobs")import json
def fetch_details(job):
resp = requests.get(job["url"], headers={"Accept": "text/html"}, timeout=15)
if resp.status_code in (404, 410):
return None # canonical job was removed
resp.raise_for_status()
detail = BeautifulSoup(resp.text, "html.parser")
posting = None
for tag in detail.select("script[type='application/ld+json']"):
try:
data = json.loads(tag.string or "")
except json.JSONDecodeError:
continue
if data.get("@type") == "JobPosting":
posting = data
break
if posting is None or not posting.get("title") or not posting.get("description"):
return None # skip records without a usable JobPosting
org = posting.get("hiringOrganization") or {}
return {
**job,
"description": posting["description"],
"employment_type": posting.get("employmentType"),
"posted_at": posting.get("datePosted"), # interpret as Europe/Berlin
"closes_at": posting.get("validThrough"), # interpret as Europe/Berlin
"company_name": org.get("name"),
"locations": posting.get("jobLocation"), # address: locality/postalCode/country
}import time
records = []
for job in jobs:
detail = fetch_details(job)
if detail:
records.append(detail)
time.sleep(0.15) # ~150ms between requests
print(f"Enriched {len(records)} of {len(jobs)} jobs")Only jobapplication.hrworks.de/{de|en}?companyId={7-char} serves tenant boards. www.hrworks.de and hrworks.de are marketing sites and are explicitly excluded from resolution.
Accept a zero-job result only from a document containing the HrwMeCustomerJobOffersNestedView or HrwMeJobApplicationManagementSession fingerprint; treat unrecognized HTML as incomplete rather than empty.
The complete collection ships in a single document and no cursor is accepted. Fetch the board once and do not attempt cursor-based pagination.
Validate every anchor: the id must match ^[a-f0-9]{6}$ and its companyId must equal the board's tenant key before you emit the job.
Some pages may omit the ld+json JobPosting block. Skip records where the JobPosting is missing or its title/description is empty instead of emitting partial rows.
datePosted and validThrough are German local times. Interpret them in Europe/Berlin before converting to UTC to avoid off-by-one date errors.
- 1Target only jobapplication.hrworks.de/{de|en}?companyId={id}; the marketing domains carry no vacancies.
- 2Confirm the HrwMe application fingerprint before trusting a zero-job result.
- 3Fetch the board once — the full collection ships in a single document, so skip pagination.
- 4Read job data from the detail page's schema.org JobPosting rather than the rendered layout.
- 5Space detail requests (~150ms) and cap concurrency around 3 to stay polite.
- 6Interpret datePosted and validThrough as Europe/Berlin local time.
One endpoint. All HR WORKS jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=hr works" \
-H "X-Api-Key: YOUR_KEY" Access HR WORKS
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the free tier and scale as you grow.