ORP (job-room.ch) Internal API - Reverse Engineered

Source: Angular bundle analysis via Wayback Machine (chunk-OJFRFBH6.js, chunk-SHQXGEIR.js) Date: 2026-03-03 Updated: 2026-03-04 (confirmed working endpoints, browser-context fetch requirement)

Base URL

All relative to https://www.job-room.ch - Base: /onlineform-service/api/npa - Search: /onlineform-service/api/npa/_search - Actions: /onlineform-service/api/npa/_action

Endpoints

| Method | URL | Purpose | |--------|-----|---------| | POST | /_action/add-work-effort?userId={userId} | CREATE new work effort | | POST | /_action/update-work-effort?userId={userId} | UPDATE work effort (full replace) | | PATCH | /{proofId}/work-efforts?userId={userId} | PATCH work effort | | DELETE | /{proofId}/work-efforts/{workEffortId} | DELETE a work effort | | GET | /_search/by-owner-user-id?userId={userId}&page={page} | List user's records | | GET | /{proofId} | Get single proof record | | GET | /{proofId}/submit | Submit proof to RAV | | GET | /{proofId}/pdf-document | Download PDF | | PATCH | /{proofId}/work-efforts/{workEffortId}/note | Update personal note | | DELETE | /{proofId}/work-efforts/{workEffortId}/note | Delete personal note |

CRITICAL: curl Does NOT Work — Browser-Context fetch() Required

curl to ORP API returns 401 even with a valid JWT and all cookies. The session is bound to browser internals (eIAM SSO cookies, session storage). External HTTP clients cannot replicate this.

The only working approach: Run fetch() inside the authenticated Playwright browser context via page.evaluate(). This executes within the live browser session and inherits all cookies and session state automatically.

Authentication

Every request needs: 1. Authorization: <jwt> header — JWT from sessionStorage.getItem('authenticationToken') (no "Bearer" prefix — the stored value already includes it) 2. ?_ng=ZGU= query parameter — this is NOT a CSRF token, it is simply base64("de") = the language key for German. Always use ZGU= for German interface. 3. X-Requested-With: XMLHttpRequest header

Getting the token

_ng parameter (NOT CSRF)

Data Model

Two levels: - ProofOfWorkEfforts (proofId) - monthly container, one per control period - WorkEffort (workEffortId) - individual application entries within a proof

Proof statuses: RE_OPENED, SUBMITTED, OPEN, CLOSED

Create Work Effort Payload

```json { "id": null, "applyDate": "YYYY-MM-DD", "ravAssigned": false, "applyChannel": { "contactPerson": "Name", "email": "hr@company.com", "formUrl": "https://company.com/apply", "phone": "+41...", "types": ["ELECTRONIC"], "address": { "name": "Company Name", "street": "Strasse", "houseNumber": "1", "postalCode": "1000", "country": "CH", "city": "Lausanne", "poBox": null } }, "applyStatus": ["PENDING"], "occupation": "Position Title", "fullTimeJob": true, "rejectionReason": null, "jobAdvertisementId": null } ```

Field Values

applyChannel.types (at least one): - ELECTRONIC - online/email - MAIL - postal - PERSONAL - in-person - PHONE - telephone

applyStatus (array, at least one): - PENDING - Noch offen - EMPLOYED - Anstellung - REJECTED - Absage - INTERVIEW - Vorstellungsgesprach (can combine: ["REJECTED", "INTERVIEW"])

fullTimeJob: true = Vollzeit, false = Teilzeit

Working browser-context fetch() Pattern

```javascript // MUST run via Playwright page.evaluate() — curl does NOT work (401) await page.evaluate(async (payload) => { const token = sessionStorage.getItem('authenticationToken'); const userId = 'USERIDHERE'; const resp = await fetch( /onlineform-service/api/npa/_action/add-work-effort?userId=${userId}&_ng=ZGU=, { method: 'POST', headers: { 'Authorization': token, 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) } ); return { status: resp.status }; }, payload); // Returns { status: 201 } on success ```

Search (list user's work efforts)

Response structure: { content: [ { id: proofId, workEfforts: [ { id, applyDate, applyChannel, applyStatus, occupation, ... } ] } ] } - It's NOT an array - it's data.content[0].workEfforts to get the work efforts list

```javascript await page.evaluate(async () => { const token = sessionStorage.getItem('authenticationToken'); const userId = 'USERIDHERE'; const resp = await fetch( /onlineform-service/api/npa/_search/by-owner-user-id?userId=${userId}&_ng=ZGU=, { headers: { 'Authorization': token, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } } ); return await resp.json(); }); ```

Update Work Effort Status (Confirmed Working 2026-03-17)

To update status (e.g. PENDING -> REJECTED), use /_action/update-work-effort. This is a full replace - send the complete existing payload with updated fields.

```javascript // Step 1: Search to find the work effort const data = await page.evaluate(async () => { const token = sessionStorage.getItem('authenticationToken'); const userId = 'USERIDHERE'; const resp = await fetch( /onlineform-service/api/npa/_search/by-owner-user-id?userId=${userId}&_ng=ZGU=, { headers: { 'Authorization': token, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } } ); const data = await resp.json(); const proof = data.content[0]; return proof.workEfforts.find(we => we.applyChannel.address.name.includes('COMPANY_NAME')); });

// Step 2: Update with modified applyStatus and rejectionReason await page.evaluate(async (we) => { const token = sessionStorage.getItem('authenticationToken'); const userId = 'USERIDHERE'; we.applyStatus = ['REJECTED']; we.rejectionReason = 'Absage erhalten am DD.MM.YYYY'; const resp = await fetch( /onlineform-service/api/npa/_action/update-work-effort?userId=${userId}&_ng=ZGU=, { method: 'POST', headers: { 'Authorization': token, 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(we) } ); return { status: resp.status }; // 201 = success }, data); ```

Known User IDs

| User | userId | Playwright profile | |------|--------|--------------------| | Matthias | 591489d5-0039-11f1-a99a-462689f09ec3 | ~/.claude/playwright-profiles/matthias/ | | Gina | 703d651c-0412-11f1-a99a-462689f09ec3 | ~/.claude/playwright-profiles/grazyna/ |

Gina's additional IDs

CRITICAL: Always Use API, NEVER Click Through UI

NEVER use Playwright browser_click/browser_type to interact with job-room.ch UI elements. Always use the internal API via page.evaluate(fetch(...)). The UI approach is slow, fragile, and unnecessary.

Integration Pattern (Confirmed Working)

  1. Open job-room.ch in Playwright (just needs to be logged in — any page works)
  2. Extract token: sessionStorage.getItem('authenticationToken')
  3. Get userId from /user-service/api/current-user?_ng=ZGU= (or use known userId from table above)
  4. Get proofId from /_search/by-owner-user-id call (or use known proofId for current month)
  5. POST to /_action/add-work-effort or /_action/update-work-effort via page.evaluate() with JSON payload
  6. Returns HTTP 201 on success
  7. Do NOT use curl — it returns 401 regardless of credentials
  8. Do NOT click UI elements — always use the API via fetch()