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)
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
| 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 |
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.
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
_ng=ZGU= is simply base64("de") — the language keyZGU= (German) regardless of sessionTwo levels: - ProofOfWorkEfforts (proofId) - monthly container, one per control period - WorkEffort (workEffortId) - individual application entries within a proof
Proof statuses: RE_OPENED, SUBMITTED, OPEN, CLOSED
```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 } ```
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
```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
```
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();
});
```
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);
```
| User | userId | Playwright profile |
|------|--------|--------------------|
| Matthias | 591489d5-0039-11f1-a99a-462689f09ec3 | ~/.claude/playwright-profiles/matthias/ |
| Gina | 703d651c-0412-11f1-a99a-462689f09ec3 | ~/.claude/playwright-profiles/grazyna/ |
1666047927af9f1f-06ee-11f1-8c9b-967beb1d3317NEVER 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.
sessionStorage.getItem('authenticationToken')/user-service/api/current-user?_ng=ZGU= (or use known userId from table above)/_search/by-owner-user-id call (or use known proofId for current month)/_action/add-work-effort or /_action/update-work-effort via page.evaluate() with JSON payload