# Personio Jobs API

> Query jobs from Personio boards through one API — and how the Personio XML feed, its structured keywords and its empty descriptions behave.

Source: https://hyperjobs.io/ats/personio

Personio is an HR platform built for European small and mid-sized companies, and
it's dominant in the DACH region. Boards live at `<slug>.jobs.personio.com`, and
the useful endpoint is an XML feed rather than a JSON API. It's an unusually rich
feed — Personio publishes seniority, schedule, years of experience and a genuine
keyword list as structured tags — and it has one failure mode that looks exactly
like success.

## Try it

```bash
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=personio" \
  --data-urlencode "country=Germany" \
  --data-urlencode "limit=2"
```

```json title="Illustrative — the shape, not a live response"
{
  "data": [
    {
      "id": "personio:1284517",
      "title": "Machine Learning Engineer (m/w/d)",
      "company": {
        "slug": "nordstern-tech",
        "name": "Nordstern Tech",
        "domain": "nordstern.de",
        "website": "https://nordstern.de",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Software Development",
        "employee_count": 180
      },
      "location": {
        "remote": true,
        "countries": ["Germany"],
        "cities": ["Hamburg"],
        "regions": null,
        "raw": ["Hamburg, remote"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": "Remote",
      "seniority": "Mid-Senior level",
      "experience_level": "2-5",
      "job_language": "de",
      "salary": {
        "min": 65000,
        "max": 85000,
        "currency": "EUR",
        "unit": "YEAR",
        "annual_min": 65000,
        "annual_max": 85000,
        "summary": null
      },
      "skills": ["Machine Learning", "LLM", "Python"],
      "taxonomies": ["Software"],
      "apply_url": "https://nordstern.jobs.personio.com/job/1284517",
      "url": "https://nordstern.jobs.personio.com/job/1284517",
      "source": "personio",
      "posted_at": "2026-07-08T10:22:00.000Z",
      "expires_at": null,
      "updated_at": "2026-07-13T19:44:02.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}
```

## How Personio boards actually work

One GET, no auth, no pagination, the whole board as XML:

```bash
curl "https://<slug>.jobs.personio.com/xml"
```

`<slug>` is the subdomain. The `.de` host serves byte-identical content, so
`<slug>.jobs.personio.de/xml` is the same feed and not a separate board.

There's also a `/search.json`, and it's worse: the XML feed carries `createdAt`,
`seniority`, `schedule`, `employmentType` and `yearsOfExperience` that
`search.json` simply doesn't have — including any posting date at all. The `<id>`
is identical between the two, so switching costs nothing.

The document is a list of `<position>` blocks. Per position: `<id>`, `<name>`,
`<office>` (repeatable), `<department>`, `<recruitingCategory>`,
`<employmentType>`, `<schedule>`, `<seniority>`, `<yearsOfExperience>`,
`<keywords>`, `<salaryInformation>` and a set of `<jobDescription>` sections.

### The description is titled sections

`<jobDescription>` repeats, each with a `<name>` and a `<value>`. The name is a
heading — `Your tasks`, `Ihr Profil`, `Benefits` — and the value is HTML. You
reassemble them in order, and the section names are how you find perks without an
AI pass: the section named some variant of Benefits / Vorteile / Wir bieten /
We offer / Avantages is the benefits list.

### `<keywords>` is real structured skills

A comma-separated tag list — `AI Engineer,Machine Learning,LLM,Python` — verified
at 66% fill live. That's a dedicated skills field, which almost no ATS in this
pool has. [Recruitee](/ats/recruitee) and [Pinpoint](/ats/pinpoint) make you mine
`<li>` bullets and hope; Personio just tells you.

### `<salaryInformation>` is plain units

`min`, `max`, `currencyCode` and `type`, in whole units — no cents divide like
[Greenhouse](/ats/greenhouse). Fill is about **13% overall, but up to 88% on a
single company's board**. That spread is worth internalising: pay transparency
here is a per-employer decision, not a platform average, so a sample from one
board tells you nothing about the source.

## What bites you

<Warning title="The feed is not the complete source it looks like">
  Some boards emit `<jobDescriptions></jobDescriptions>` **empty** for part of
  their positions — one live board had 14 of 15 blank — while the posting page
  carries the full body in its JobPosting JSON-LD. The feed returns 200 with a
  well-formed document and a hole in it.

  Most boards fill every one and this never fires. That's what makes it dangerous:
  you'll test on a healthy board, ship, and lose most of the descriptions on a
  board you never looked at. We detect the blanks and recover only those rows from
  the posting page, so a healthy board pays nothing.
</Warning>

**Personio has no workplace-type field.** No remote flag, no arrangement enum.
What it has is `<office>` values that are frequently *the arrangement itself* —
`Remote`, `Hybrid`, `Remote Germany`, `Hamburg, remote`. The office field is
doing double duty. Reading an arrangement out of it recovers about 24% of
postings including hybrid, and the city still parses out of the same string. It's
a workaround for a missing field, and it only works because employers keep typing
the arrangement where the office goes.

**`<keywords>` leaks raw HTML entities.** `Sales &amp; acquisition`,
`&quot;Field Service&quot;`, `R&#38;D`. Store them undecoded and your skill tags
contain literal `&amp;`.

**`<seniority>` is a clean English enum even on German boards.**
`experienced`, `entry-level`, `student` — it doesn't get localised, which is
convenient and surprising enough to be worth stating.

**A position can have several `<office>` tags.** Pass them as an array or a
multi-site job collapses to one city.

## Or use ours

Personio's feed is one request and genuinely pleasant. The reasons to take ours
are the hole and the reach.

The hole: descriptions that are silently blank on some boards, recovered from the
posting page only where the feed left them empty. The reach: we track 2,463
Personio boards, and Personio publishes no index of its customers — assembling
that list is the actual work, not the parsing.

Plus the usual: `<keywords>` decoded into clean skill tags, the arrangement
recovered from `<office>`, sections reassembled into one body with benefits
extracted, EUR bands normalised so they compare against USD ones, dedup, expiry by
board diffing, and the [company join](/docs/objects/company).

Personio is in the fast refresh tier, attempted every cycle — see
[coverage](/docs/reference/coverage).

[Quickstart](/docs/quickstart) · [Pricing](/pricing)
