All platforms

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.

Get API access
HR WORKS
Live
<3haverage discovery time
1hrefresh interval
Developer tools

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.

Data fields
  • Full Job Descriptions
  • Employment Type
  • Posting & Closing Dates
  • Structured Locations
  • Hiring Organization Name
  • Direct Apply URLs
Use cases
  1. 01German Job-Market Tracking
  2. 02Employer Vacancy Monitoring
  3. 03Recruitment Data Aggregation
  4. 04Careers Page Indexing
DIY GUIDE

How to scrape HR WORKS.

Step-by-step guide to extracting jobs from HR WORKS-powered career pages—endpoints, authentication, and working code.

HTMLintermediateNo published limit; ~150ms between requests, cap detail fetches at ~3 concurrentNo auth

Build the tenant board URL

HR WORKS publishes each employer at jobapplication.hrworks.de under a seven-character companyId and a de or en language route. The marketing domains (www.hrworks.de, hrworks.de) carry no vacancies, so target only the jobapplication host.

Step 1: Build the tenant board URL
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}"

Fetch the board and verify the fingerprint

The full vacancy collection is server-rendered into a single document. Confirm the HR WORKS application runtime markers are present before trusting the result, so an error or placeholder page is never mistaken for an empty board.

Step 2: Fetch the board and verify the fingerprint
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")

Extract job anchors from the collection

Each vacancy is an a.job-offer-content link carrying the tenant companyId and a six-hex job id. Reject anchors whose companyId differs from the board or whose id is not six hex characters, then read the title from the link's title attribute (falling back to its h2).

Step 3: Extract job anchors from the collection
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")

Parse the schema.org JobPosting from each detail page

Every detail document embeds a schema.org JobPosting in a script[type='application/ld+json'] block. Read title, description, employmentType, dates, hiring organization, and jobLocation from that block rather than scraping the visible layout. A 404 or 410 means the vacancy has been removed.

Step 4: Parse the schema.org JobPosting from each detail page
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
    }

Collect the board without paginating

HR WORKS returns the entire collection in one document and does not accept a pagination cursor, so fetch the board once and enrich each job. Space the detail requests to stay polite.

Step 5: Collect the board without paginating
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")
Common issues
highScraping the marketing domain returns no vacancies

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.

highAn empty page is mistaken for a board with zero jobs

Accept a zero-job result only from a document containing the HrwMeCustomerJobOffersNestedView or HrwMeJobApplicationManagementSession fingerprint; treat unrecognized HTML as incomplete rather than empty.

mediumAttempting to paginate yields errors or nothing

The complete collection ships in a single document and no cursor is accepted. Fetch the board once and do not attempt cursor-based pagination.

mediumCross-tenant or navigation anchors leak into results

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.

mediumDetail page lacks a schema.org JobPosting

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.

lowPosting and closing dates land on the wrong day

datePosted and validThrough are German local times. Interpret them in Europe/Berlin before converting to UTC to avoid off-by-one date errors.

Best practices
  1. 1Target only jobapplication.hrworks.de/{de|en}?companyId={id}; the marketing domains carry no vacancies.
  2. 2Confirm the HrwMe application fingerprint before trusting a zero-job result.
  3. 3Fetch the board once — the full collection ships in a single document, so skip pagination.
  4. 4Read job data from the detail page's schema.org JobPosting rather than the rendered layout.
  5. 5Space detail requests (~150ms) and cap concurrency around 3 to stay polite.
  6. 6Interpret datePosted and validThrough as Europe/Berlin local time.
Or skip the complexity

One endpoint. All HR WORKS jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=hr works" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

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.

99.9%API uptime
<200msAvg response
50M+Jobs processed