Personio¶
Personio's career sites, observed on live postings, last on 2026-10-22. The fixture page is tests/pages/personio/apply.html, served as acme.jobs.personio.com in tests/test_personio.py: one example of each rendering below.
Where the form is¶
- Every employer is on
<account>.jobs.personio.com(.deserves the same pages). A job is/job/<id>, its form/job/<id>/apply, which the adapter opens from the job page. The double-send key is the job URL on.com. - The apply page is rendered on the server with the whole form in its HTML, in the career site's language (a
?language=in the URL picks another;Accept-Languagedoes not).request_readfetches that server-rendered HTML,browser_readtakes the page's form, and one parser lists the fields from both: parity holds by construction. - The page builds the form from
/api/v1/jobs/<id>/application-form, a public JSON API with English labels. It answers 429 for a while after some 180 requests in a burst from one address, while the pages keep answering, so the adapter does not use it. - A closed posting's apply page has no form.
- A document is posted to
/api/v1/documentsas soon as it is attached: it reaches Personio before the submit.
Reading the form¶
A field's id is its control's name: first_name, custom_attribute_<n>, and documents.<category> for a document category's file input. Its label is its label[for], else its group's label (role=group, aria-labelledby: the name pair, a document category), without the marker spans; a starred label, own or the group's, makes it required. A control outside the table below is unhandled_rendering.
Each rendering, and in how many of 441 postings' apply pages it shows (2026-10-22; the form API of 178 of them listed only the types text, option and date):
| Rendering | Field type | Postings |
|---|---|---|
text input: name, phone, available from, salary, location, current title and employer, profile, custom questions |
text, maxlength; phone for phone |
441; custom 217 |
email input: email |
text |
441 |
native select: custom questions, gender, years_of_experience; the standard fields' option texts in the page's language |
choice, the option's text |
200, 94, 25 |
native date input: birthday, custom dates; YYYY-MM-DD |
date |
53, 16 |
file input per document category (cv, other, cover-letter, employment-reference, certificate, work-sample), multiple; once uploaded, the input is cleared and the name shown as a pill in the category |
file |
441, 389, 227, 141, 51, 39 |
No checkbox, radio, textarea or consent box was seen: the privacy notice is text.
Filling¶
- A text, phone or date input is filled; a select is set to the value of the option whose text is the answer.
- A document is attached to its category's input and waited for until its name shows as a pill.
Read-back and submit¶
- The read-back is each control's value, a select's chosen option's text, and a document's last pill name.
- The apply page carries a Cloudflare Turnstile widget, and its submit button is disabled while the form is empty; what enables it is unobserved. Submit waits for it to enable, else it is
blocked,captcha; the evidence is the answer to the POST on/api/v1/jobs/<id>/application(JSON, with anIdempotency-Key). No send has been made through this adapter, so the answer and the page after a send are unobserved.
Known gaps¶
None.