Personio Jobs by Company: Career Site Feed API avatar

Personio Jobs by Company: Career Site Feed API

Pricing

from $0.84 / 1,000 job row returneds

Go to Apify Store
Personio Jobs by Company: Career Site Feed API

Personio Jobs by Company: Career Site Feed API

Open positions of any company career site on Personio, read live from its keyless job-board feed: title, department, offices, employment type, schedule, seniority, occupation category, skill keywords, disclosed minimum salary, creation date and job page. A summary mode counts open jobs per group.

Pricing

from $0.84 / 1,000 job row returneds

Rating

0.0

(0)

Developer

Samat Makatov

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Open positions of any company career site on Personio, read live from its keyless job-board feed: title, department, offices, employment type, schedule, seniority, occupation category, skill keywords, disclosed minimum salary, creation date and job page. A summary mode counts open jobs per group.

Give it the slugs of the employers you care about (carbmee, stark, deskbird) and every position they publish right now comes back as a row — read at run time from the company's own career site, not from an index built days ago. One request per company returns the whole board, so there is no paging and no per-job call. No API key, no proxy, no browser.

Use cases

  • Hiring signals for GTM and lead generation: watch a list of target accounts on a schedule with postedWithinDays and onlyNew, and get a row the day a company opens a role — a budget and a priority you can act on.
  • Niche and regional job boards: pull the boards of the employers you cover into your own listing site, with the real apply link (url) for every posting instead of a re-indexed copy.
  • Student and internship boards: employmentTypes: ["working_student", "intern"] is a code the source publishes itself, so the slice is exact across employers instead of guessed from the title.
  • Pay-transparency research: onlySalaryDisclosed keeps the postings where the employer named a figure, with amount, currency and period, so you can benchmark who discloses and from how much.
  • Org and growth research: mode: "summary" counts open jobs per department, office, seniority or skill keyword from the same single request — which team a company is scaling, and where.
  • Agents and RAG: ask "who is hiring a data engineer in Munich" against a live board, trim rows with fields, and hand the agent url so the answer links to the real application form.

Input

FieldTypeDefaultAllowed values / notes
companiesstring[]requiredCareer-site slugs, e.g. ["carbmee", "stark", "deskbird"]. A full career-site or job URL works too — the slug is read out of it. See Finding the slug
modestringjobsjobs = a row per open position · summary = a row per group with its open-job count
groupBystringdepartmentOnly in summary mode: department, office, recruitingCategory, employmentType, schedule, seniority, occupationCategory, occupation, keyword
titleKeywordsstring[]allKeep a position whose title contains any of these words (case-insensitive)
excludeTitleKeywordsstring[]noneDrop a position whose title contains any of these words
departmentsstring[]allDepartment contains any of these. Free text per employer — see Vocabularies
officesstring[]allOffice contains any of these; matched against the main and the further offices
recruitingCategoriesstring[]allRecruiting category contains any of these. Free text per employer
employmentTypesstring[]allPlatform codes, whole-value match: permanent, fixed_term, intern, working_student, … see Employment type
senioritiesstring[]allPlatform codes: student, entry-level, experienced, executive — see Seniority
schedulestringanyany, full-time, part-time, full-or-part-time
occupationCategoriesstring[]allPlatform job family, e.g. it_software — see Occupation category
keywordsAnystring[]allKeep positions carrying one of these skill keywords, matched as whole words — see Skill keywords
onlySalaryDisclosedbooleanfalseKeep only positions with a published salary figure
postedWithinDaysintegerno limit1–3650. Keep positions created in the last N days (UTC)
onlyNewbooleanfalseWrite only positions this actor has not returned before (jobs mode) — see Watching for new postings
languagestringsourcesource, en, de, fr, es, it, nl, pt — see Language
includeDescriptionbooleanfalseAdd descriptionSections, descriptionHtml, descriptionText to every row
sortBystringcreatedDesccreatedDesc, createdAsc, titleAsc, departmentAsc, officeAsc, feedOrder
maxItemsinteger501–5000 rows in total
maxItemsPerCompanyintegerno cap1–2000 rows per career site, applied before maxItems
fieldsstring[]allKeep only these output fields, in this order

Filters combine with AND, the values inside one filter with OR. They run over the complete board the feed returned, so nothing is lost to paging. In summary mode the filters narrow the positions that are counted.

Reference

Finding the slug

A career site on this platform lives on its own subdomain: https://<slug>.jobs.personio.com. The slug is the part in front of .jobs.personio.com, and it is not always the company name — open the employer's job list and copy it out of the address bar. All three forms are accepted and end up as the same slug:

carbmee
carbmee.jobs.personio.com
https://carbmee.jobs.personio.com/job/2717004

A slug nobody publishes a career site under is answered by the platform with a redirect to its marketing site. The actor does not follow it: the run stays successful and writes one row with found: false and a message saying so.

Vocabularies

department, recruitingCategory and office are free text each employer types itself, often in German (R&D / Software, Werkstudenten, Munich HQ (Karlsfeld), Plant (Gendorf)). Do not guess them — run the board once with mode: "summary" and groupBy set to that field and you get the exact list the company uses, with a count per value. A filter value that resembles a value on the boards of the run is corrected to it (Enginering → Engineering) and the correction is logged in the run log and in the SUMMARY record; a value that resembles nothing stays as typed and simply matches nothing, so a filter never quietly widens.

Employment type

Whole-value codes; -, _ and spaces are interchangeable, so fixed_term, fixed-term and Fixed Term are the same value.

CodeMeaning
permanentOpen-ended contract — the bulk of every board
fixed_termContract with an end date
internInternship
working_studentWorking student next to university studies (common in DACH)
traineeTrainee or apprenticeship programme
freelanceFreelance or contractor engagement

Seniority

student, entry-level, experienced, executive, senior-management. Part of the postings leave the field empty; those are dropped by a seniority filter and appear as the (none) group of a seniority summary.

Working time

full-time, part-time and full-or-part-time — the third value is the source's own, for a position the employer opened for either. Ask for part-time and you get the genuinely part-time postings only.

Occupation category

The platform's own job family, so it is comparable across employers — unlike department, which every company invents. Values seen live: it_software, engineering, sales_and_business_development, marketing_and_product, accounting_and_finance, human_resources, production_and_operations, logistics_and_transportation, project_and_program_management, r_and_d_and_science, creative_and_design, customer_support_and_client_care, administrative_and_clerical, editorial_and_writing, business_and_strategic_development, security_and_protective_services, other. The narrower occupation code sits inside a category (software_and_web_development, aeronautic_and_avionic_engineering, corporate_accounting, …); group by it to see the list one employer really uses.

Language

source (the default) asks the feed for no language at all, which returns the complete board in the languages the employer published. A language code asks for one version: titles and description sections come back translated where the employer maintains that translation and in the original language where it does not. Asking for a language can also shrink a board — a posting the employer never published for that language is left out — which is why it is never the default.

Salary

The source carries a minimum amount, a currency and a period (hourly, monthly, yearly), and an upper bound on a small share of postings. salaryIsMinimum is true when the employer named a floor and no ceiling, false when salaryMax carries one. Most employers publish nothing at all, so onlySalaryDisclosed returns a short list per career site.

Skill keywords

A posting can carry a list of skills the employer attached (Supply Chain, SQL, Python, ERP Systems, …). keywords holds all of them in the order the source lists them — 1 to 14 per posting on the boards read while this actor was built, and empty on the postings where the employer filled nothing in. keywordsAny matches against that full list, by whole words, not by fragments: ["SQL"] keeps a posting tagged SQL or SQL Server and leaves one tagged only PostgreSQL out, and ["java"] does not drag in JavaScript. A multi-word tag stays reachable through its words, so ["software"] still finds Software Development, and +/# belong to the word (c++ and c# are their own skills). When none of the keywords you asked for exists on the boards of the run, the log and SUMMARY.filtersWithoutMatch say so instead of returning near-misses. mode: "summary" with groupBy: "keyword" turns the same list into a skill-demand count per employer.

Watching for new postings

With onlyNew: true a run writes only the positions it has not handed out before, which is what turns a schedule into a hiring-signal feed. What a run delivered is remembered as career site plus position id in a named key-value store in your own account:

  • personio-jobs-state for runs you start by hand or over the API;
  • personio-jobs-state-<task id> when the run comes from a task — so two schedules with different company lists or filters cannot blind each other.

The first run returns everything that matches, so start it with a maxItems you are happy to pay for. Only rows a run really wrote are remembered: positions cut off by maxItems come back on the next run. Runs that start at the same second read the same memory and therefore can return the same posting twice — schedule a watchlist as one run after another, not in parallel. An edited posting is not delivered again, because the feed publishes no "last updated" moment. Delete the store (or switch onlyNew off) to start over; the SUMMARY record names the store and how many positions it holds.

Examples

All open jobs of one career site

{ "companies": ["carbmee"], "mode": "jobs", "maxItems": 25 }

Engineering roles across several employers

{ "companies": ["carbmee", "stark", "deskbird", "tozero"], "titleKeywords": ["engineer"], "maxItems": 30 }

Working-student and internship postings

{ "companies": ["anton", "tozero", "stark"], "employmentTypes": ["working_student", "intern"], "maxItems": 30 }

IT and software roles on the platform's own taxonomy

{ "companies": ["carbmee", "stark", "deskbird", "personio"], "occupationCategories": ["it_software"], "maxItems": 30 }

Postings tagged with a specific skill

{ "companies": ["optiply", "carbmee", "stark"], "keywordsAny": ["SQL"], "maxItems": 15 }

Hiring signal: what opened in the last 60 days

{ "companies": ["carbmee", "stark", "deskbird", "optiply", "anton", "fact-finder"], "postedWithinDays": 60, "onlyNew": false, "maxItems": 25 }

Positions with a published salary

{ "companies": ["anton", "stark"], "onlySalaryDisclosed": true, "maxItems": 20 }

Open jobs per department

{ "companies": ["stark", "carbmee", "fact-finder"], "mode": "summary", "groupBy": "department", "maxItems": 30 }

Output

A real row, from a run with {"companies": ["optiply", "carbmee", "stark"], "keywordsAny": ["SQL"], "maxItems": 15} on 2026-09-29 — the only posting of the three career sites tagged with the skill SQL:

{
"companySlug": "optiply",
"companyName": null,
"positionId": "2818912",
"title": "Supply Chain Engineer",
"department": "Customer Success",
"recruitingCategory": "Optiply",
"office": "Amsterdam",
"additionalOffices": [],
"offices": ["Amsterdam"],
"officeCount": 1,
"employmentType": "fixed_term",
"schedule": "full-time",
"seniority": "experienced",
"yearsOfExperience": "2-5",
"occupation": "systems_and_process_engineering",
"occupationCategory": "engineering",
"keywords": ["Supply Chain", "Supply Chain Management", "Inventory Management", "SQL", "Python", "Data Analysis", "Demand Forecasting", "Workflow Automation", "ERP Systems", "Supply Chain Analyst", "replenishment", "safety stock", "Retool", "forecasting"],
"salaryMin": null,
"salaryMax": null,
"salaryCurrency": null,
"salaryCurrencySymbol": null,
"salaryPeriod": null,
"salaryIsMinimum": false,
"salaryDisclosed": false,
"createdAt": "2026-09-29T10:53:17.000Z",
"daysSincePosted": 0,
"language": null,
"found": true,
"message": null,
"url": "https://optiply.jobs.personio.com/job/2818912",
"boardUrl": "https://optiply.jobs.personio.com/",
"fetchedAt": "2026-09-29T20:53:44.983Z"
}
FieldTypeAlways filledMeaning
companySlugstringyesThe career site the row came from
companyNamestring|nullnoLegal entity the employer attached to the posting; empty when it publishes under one name only
positionIdstringyes (job rows)Position id on the career site; the id in url
titlestringyes (job rows)Job title as published
departmentstring|nullnoThe employer's own team name
recruitingCategorystring|nullnoThe employer's own posting category
officestring|nullyes (job rows)Main office label as the employer typed it
additionalOfficesstring[]yesFurther offices of the same posting, main one excluded
officesstring[]yesMain office plus further offices — what the office filter matches
officeCountintegeryesLength of offices
employmentTypestring|nullyes (job rows)Platform code, see Employment type
schedulestring|nullyes (job rows)full-time, part-time, full-or-part-time
senioritystring|nullnoPlatform code, see Seniority
yearsOfExperiencestring|nullnoBand the employer picked, e.g. 2-5, lt-1
occupationstring|nullyes (job rows)Narrow platform job code
occupationCategorystring|nullyes (job rows)Platform job family
keywordsstring[]noEvery skill keyword the employer attached, in source order; empty on postings without any
salaryMinnumber|nullnoPublished amount, a lower bound
salaryMaxnumber|nullnoUpper bound where the employer published one
salaryCurrencystring|nullnoISO currency code, e.g. EUR
salaryCurrencySymbolstring|nullnoSymbol as published, e.g. €
salaryPeriodstring|nullnohourly, monthly or yearly
salaryIsMinimumbooleanyestrue when the figure is a floor with no ceiling
salaryDisclosedbooleanyestrue when the employer published pay at all
createdAtstring|nullyes (job rows)Creation moment, ISO 8601 UTC
daysSincePostedinteger|nullyes (job rows)Whole days since createdAt, never negative
languagestring|nullnoThe language version asked for; empty when the board was read as published
foundbooleanyesfalse on the marker row of a slug without a career site
messagestring|nullnoWhy a row is a marker row
urlstringyesPublic job page with the application form (career site on summary and marker rows)
boardUrlstringyesThe career site the row came from
fetchedAtstringyesMoment of the request, ISO 8601 UTC

With includeDescription: true every job row also carries descriptionSections ([{ "name": "Your mission", "text": "…" }]), descriptionHtml and descriptionText. Some postings, and some language versions of a posting, carry no sections — those rows get empty values.

A summary row (same run family, stark grouped by department):

{
"companySlug": "stark",
"companyName": null,
"groupBy": "department",
"value": "R&D / Hardware",
"jobCount": 48,
"boardJobCount": 138,
"found": true,
"message": null,
"url": "https://stark.jobs.personio.com/",
"boardUrl": "https://stark.jobs.personio.com/",
"fetchedAt": "2026-09-29T20:17:00.228Z"
}

jobCount is the number of open positions in the group, boardJobCount the number of positions the groups were counted from. A position open in three offices counts once per office and a position with five keywords once per keyword, so those two groupings can add up to more than boardJobCount; positions with an empty field land in the group (none).

A slug that serves no career site produces one row with found: false, the same field names with empty values, and a message naming the slug — the run still succeeds.

Every run also writes a SUMMARY record into the run's key-value store: the career sites requested and found, positions per board, how many rows the filters cut, the filter corrections that were applied, the request count and — with onlyNew — the state store it used, how many positions it knew before the run and how many it knows now.

Dataset views: Open positions (slug, title, department, office, employment type, working time, seniority, created, days online, salary from, job page) · Level, pay and skills · Open jobs per group.

Use it from code / agents

curl -X POST "https://api.apify.com/v2/acts/yadroo~personio-jobs/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"companies":["carbmee","deskbird"],"titleKeywords":["engineer"],"maxItems":25}'
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/personio-jobs').call({
companies: ['carbmee', 'stark'],
postedWithinDays: 30,
fields: ['companySlug', 'title', 'office', 'createdAt', 'url'],
maxItems: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/personio-jobs").call(run_input={
"companies": ["stark", "carbmee"],
"mode": "summary",
"groupBy": "office",
"maxItems": 30,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items

MCP: add https://mcp.apify.com to Claude / Cursor / any MCP client and call the yadroo/personio-jobs tool with the same JSON input. fields keeps the rows small enough for a model context — ["title", "office", "employmentType", "url"] is usually all an agent needs.

Pricing

Pay per event: $0.001 per run start + $0.0012 per dataset row. Every run is charged the start event, also a run that returns a single row or only a found: false marker. A summary group and a marker row are billed like a job row.

RunRowsCost
One small career site (carbmee, whole board)12$0.001 + 12 × $0.0012 = $0.0154
The three prefilled career sites with the default maxItems: 5050$0.001 + 50 × $0.0012 = $0.061
Department summary of three employers20$0.001 + 20 × $0.0012 = $0.025
A watchlist sweep with postedWithinDays: 7 that finds nothing new0$0.001 (the start event only)

maxItems is a hard stop, so a run can never cost more than $0.001 + maxItems × $0.0012. Filters are applied before rows are written, so you pay only for the postings you asked for — a postedWithinDays sweep over 20 employers that finds three new roles costs $0.0046.

Limits & FAQ

Which companies are covered? Any employer whose career site is hosted on this platform, one slug at a time. There is no search across companies: you bring the list, the actor reads each board live. Career sites on the older .jobs.personio.de host are out of scope — the actor reads only <slug>.jobs.personio.com.

How fresh is the data? It is the employer's own board at the moment of the run. The runs behind this README took 5–8 seconds for one to six career sites.

What is not in it? Only what the employer publishes: closed, draft and internal-only postings are absent, and a company between hiring rounds serves an empty board (the run then writes a marker row explaining that, instead of failing). Applicant data, contact persons and anything behind the application form are not read.

office is not a city. It is the label the employer typed — Munich HQ (Karlsfeld), Hybrid - Berlin, Germany, Remote - Germany. Filter with a substring (munich) rather than an exact name, and expect no normalised country or coordinates.

Sparse fields. salaryMin, keywords, yearsOfExperience, department and companyName depend on how carefully the employer filled its posting form; across the 178 positions read from eight career sites while this actor was built, five named a salary. Filters on a field always drop the postings that leave it empty.

Descriptions. includeDescription costs no extra request — the texts arrive in the same response — but it makes rows much larger, which is why it is off by default. Some postings carry no sections at all.

Rate limits. One request per career site per run, with polite retries and exponential backoff on 429 and 5xx answers, and a normal identifying User-Agent. No proxy, no browser, no login. If you watch many employers, prefer one scheduled run with a long companies list over many parallel runs.

Empty result or error? A wrong slug, an empty board and a source that is throttling are three different answers: the first two become a found: false row with a message and a successful run, the third an error naming the career site. A run fails only when no career site at all could be read.

Can I get only new postings? Yes — onlyNew remembers career site and position id in a named key-value store in your account (personio-jobs-state, or personio-jobs-state-<task id> for a task) and writes only what was not there before. The first run returns everything it finds, later runs only the additions; parallel runs started in the same second share one memory snapshot and can repeat a posting. Watching for new postings has the details.


Made by Yadroo. Sibling actors: greenhouse-jobs, workable-jobs, recruitee-jobs, ashby-job-postings, hh-kz-vacancies.