Dieses Dokument ist als historisches Spike-Artefakt erhalten.
Der hier skizzierte authentifizierte REST-Refactor wurde am 2026-05-15 nach Live-Spike verworfen (REST liefert nur 8 Header-Felder, alle Content-Felder leben ausschließlich im XML-Feed). Kanonische Story:
F-280-personio-xml-feed.html. Architektur-Begründung:
ADR-0035. Wertvoll bleiben hier die Spike-Findings (Section
Spike-Findings) als Re-Litigation-Anker für die Frage „warum nicht authentifiziert?".
User Story
Als HR-Verantwortliche:r von SOLCOM möchte ich, dass alle in Personio gepflegten offenen Stellenanzeigen automatisch in den Karriere-Bereich von solcom.de gespiegelt werden — über eine authentifizierte Service-Identity, die compliance- und IT-Security-konform ist — damit die Website tagesaktuell den HR-Bestand widerspiegelt, ohne manuelle Doppelpflege in Drupal und ohne dass externe Datenquellen anonym angesprochen werden.
Technische Beschreibung
Drupal-Modul solcom_vacancy wird auf einen authentifizierten REST-API-basierten Sync gegen Personio REST API v1 umgebaut. Auth via JWT-Bearer-Token (Client-ID + Client-Secret aus dem drupal/key-Modul mit Env-Provider). Service-Layer PersonioApiClient + PersonioPositionMapper + PersonioVacancySyncer, getriggert von hook_cron, ausgeführt über Drupal Queue-API. Synchroner Hard-Delete-Sweep vor Queue-Befüllung. Datenmodell, Vocabularies und Canvas-Templates bleiben unverändert.
Narrativ-Hinweis
Diese Story ist als neuer authentifizierter Personio-Import formuliert (Outcome-orientiert). Der bisherige Backlog-Eintrag F-280 referenzierte einen anderen Datenkanal; mit dieser Story wird der authentifizierte REST-Pull gebaut. Begründung: Compliance- und IT-Security-seitige Anforderung, dass externe Datenquellen mit Service-Identity angesprochen werden.
Spike-Findings (2026-05-15) — Plan-Annahme widerlegt
Stand: Plan/PRD basieren auf der Annahme „authentifizierte REST API liefert vollständigen Job-Content". Live-Spike gegen Personio (Bearer-Token papi-…, 24h gültig) hat diese Annahme erschöpfend widerlegt. Vor Implementation muss die Architektur-Richtung neu entschieden werden.
Quellen-Vergleich
| Quelle | Auth | Job-Set | Felder pro Job |
POST api.personio.de/v1/auth +
GET api.personio.de/v2/recruiting/jobs (Beta-Header) |
Bearer papi-…, Client-ID + Client-Secret via /v1/auth, opaque Token (kein JWT), expires_in: 86400s |
429 distinct (alle Org-Jobs inkl. archiviert + Drafts) |
10 Header-Felder: id, name, category{id,name}, company{id}, department{id,name}, hiring_team[], created_at, updated_at, requirements_parsed_data, total_requirements — die letzten zwei sind in 0/2345 Antworten je non-null |
GET solcom-gmbh.jobs.personio.de/xml |
keine (public) |
32 (nur Career-Site-live) |
15 Content-Felder: id, subcompany, office, department, recruitingCategory, name, jobDescriptions, employmentType, seniority, schedule, yearsOfExperience, keywords, occupation, occupationCategory, salaryInformation |
Beispiel: REST-Response für Job 2030483
{
"id": "2030483",
"name": " Sales Manager als Teamleiter:in (m/w/d) Vertrieb Karlsruhe",
"category": { "id": "524768", "name": "Vertrieb" },
"company": { "id": "78231" },
"department": { "id": "2277747", "name": "Vertrieb" },
"hiring_team": [
{ "person": { "id": "25230789", "_meta": { "links": { "self": { "href": "https://api.personio.de/v2/persons/25230789" } } } },
"role": { "name": "Stellenverwalter" } },
/* … 5 weitere hiring_team-Einträge … */
],
"created_at": { "date-time": "2025-03-25T13:33:25Z", "timezone": "UTC" },
"updated_at": { "date-time": "2025-07-31T10:10:26Z", "timezone": "UTC" },
"requirements_parsed_data": null,
"total_requirements": null,
"_meta": { "links": { "self": { "href": "https://api.personio.de/v2/recruiting/jobs/2030483" } } }
}
Beispiel: XML-Response für Position 2519922 (verkürzt)
<position>
<id>2519922</id>
<subcompany>SOLCOM GmbH</subcompany>
<office>Reutlingen</office>
<department>Vertrieb</department>
<recruitingCategory>Recruiting</recruitingCategory>
<name>(Junior) Recruiter (m/w/d) – Vertrieb & Talent Management</name>
<jobDescriptions>
<jobDescription>
<name>Das erwartet Dich</name>
<value><![CDATA[<span style="font-size:18px;">Seit 1994 … </span>…]]></value>
</jobDescription>
<jobDescription><name>Das bringst Du mit</name><value><![CDATA[<ul><li>…</li></ul>]]></value></jobDescription>
<jobDescription><name>Deine Benefits</name><value><![CDATA[…]]></value></jobDescription>
<jobDescription><name>Standort</name><value><![CDATA[…]]></value></jobDescription>
<jobDescription><name>Team</name><value><![CDATA[…]]></value></jobDescription>
<jobDescription><name>Startdatum</name><value>…</value></jobDescription>
<jobDescription><name>Ansprechpartner/in</name><value>…</value></jobDescription>
</jobDescriptions>
<employmentType>permanent</employmentType>
<seniority>entry-level</seniority>
<schedule>full-time</schedule>
<yearsOfExperience>lt-1</yearsOfExperience>
<keywords>…</keywords>
<occupation>…</occupation>
<occupationCategory>…</occupationCategory>
<salaryInformation>
<min>45000</min><max>50000</max>
<currencyCode>EUR</currencyCode><currencySymbol>€</currencySymbol><type>year</type>
</salaryInformation>
</position>
Weitere verifizierte Befunde
- Token-Format: opaque
papi-…, kein JWT. Plan-AK „JWT-Format dokumentiert" ist nicht haltbar. TTL kommt aus expires_in-Feld der Auth-Response (24h).
- Auth-Endpoint:
POST /v1/auth mit application/x-www-form-urlencoded-Body (client_id + client_secret). Response: {success: true, data: {token, expires_in, scope}}.
- Job-Endpoint:
/v1/recruiting/positions (im Plan) existiert nicht. Korrekt ist GET /v2/recruiting/jobs mit Beta: true-Header.
- Pagination: cursor-basiert (
?limit=200&cursor=…). Termination muss über _data === [] laufen, nicht über fehlenden next-Link — Personio gibt auch nach erschöpfter Page-Folge weiter einen Cursor zurück (Loop). Über 21 Pages und 2345 Antworten gesammelt, 429 distinct IDs.
- Schema-Audit über alle 2345 Antworten: 10 Top-Level-Keys, immer dieselben. Kein einziges Sample hatte je zusätzliche Felder.
requirements_parsed_data und total_requirements sind 0/2345 non-null — die API setzt diese Felder im Recruiting-Resource derzeit nicht.
- Set-Differenz: REST liefert 429 Jobs, XML 32 — Faktor 13,4×. Beispiel-Job
2030483 existiert in REST, aber NICHT im XML — REST enthält archivierte und Drafts, XML nur die live-veröffentlichten Career-Site-Positions.
Architektur-Optionen zur Stakeholder-Klärung
- Hybrid — REST für Lifecycle/Set-Wahrheit (welche Jobs aktuell, updated_at-Filter, archiviert-Erkennung), XML für Content (15 Felder). Service+Queue+key-Modul-Pattern aus Plan bleibt, aber Mapper liest XML, REST steuert die Sweep-Liste. PRD und Plan müssen umgeschrieben werden, Wording „authentifizierte Spiegelung" wird „authentifizierte Lifecycle-Steuerung + XML-Content". Brauchbarkeit des key-Moduls bleibt (audit, identity), wird aber kleiner.
- XML-only — Plan radikal vereinfachen: kein
/v1/auth, kein drupal/key, kein REST. Service+Queue+hook_cron direkt auf dem public XML-Feed. Schnellster Weg zu lauffähigem MVP (2026-07-08). Kosten: der ursprüngliche Treiber „Compliance/Service-Identity" entfällt — externe Datenquelle bleibt anonym angesprochen, was IT-Security ggf. reframen muss.
- Pausieren / GA-warten — Personio-Support fragen, ob das v2-Job-Schema im GA-Release Content-Felder bekommt. Bis dahin Status quo (alter Migrate+XML) belassen. Risiko: MVP-Termin 2026-07-08 verfehlt.
Goal-Mode pausiert, bis Entscheidung gefallen ist.