Employees
Payroll roster — CRUD with personnummer masking on list endpoints.
Endpoints
GET/api/v1/companies/:companyId/employees— List employees for a company.GET/api/v1/companies/:companyId/employees/:id— Get a single employee.POST/api/v1/companies/:companyId/employees— Create an employee.PATCH/api/v1/companies/:companyId/employees/:id— Update an employee.DELETE/api/v1/companies/:companyId/employees/:id— Soft-delete an employee.
GET /api/v1/companies/:companyId/employees {#get-employees-list}
employees.list · scope payroll:read
List employees for a company.
Returns active employees in created-first order. Pass ?include_inactive=true to include soft-deleted (is_active=false) rows. Use ?search to match against first or last name. Personnummer is masked (birthdate visible, last-4 hidden); use GET /employees/{id} for the full value.
Use when: You need a roster — for building a UI picker, resolving employee_id before adding to a salary run, or syncing an external HR system.
Don't use for: Fetching a single employee you already know the id of — use GET /api/v1/companies/{companyId}/employees/{id}. Salary calculations live on /salary-runs/{id}.
Pitfalls
- Inactive employees are hidden by default; soft-delete via DELETE sets is_active=false (BFL 7 kap retention).
- personnummer is masked in the list response (GDPR Art.5(1)(c) data minimisation). The detail endpoint returns the full value.
- salary_type drives which field is meaningful: monthly_salary for monthly, hourly_rate for hourly. The other is null.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": [
{
"id": "a8f1…",
"first_name": "Anna",
"last_name": "Andersson",
"personnummer_masked": "YYYYMMDDXXXX",
"employment_type": "employee",
"employment_start": "2024-01-15",
"employment_end": null,
"salary_type": "monthly",
"monthly_salary": 35000,
"hourly_rate": null,
"f_skatt_status": "a_skatt",
"is_active": true,
"created_at": "2024-01-15T08:00:00Z"
}
],
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12",
"next_cursor": null
}
}
GET /api/v1/companies/:companyId/employees/:id {#get-employees-get}
employees.get · scope payroll:read
Get a single employee.
Returns the full employee record including the 12-digit personnummer, bank details, tax configuration, and contact info. This is the deliberate drill-in for an id you already know — list calls mask personnummer.
Use when: You have an employee id and need every field (tax table, bank account, vacation rule) — typically to render an edit form or to construct a payroll calculation input.
Don't use for: Rosters or pickers (use the list endpoint — personnummer is masked there).
Pitfalls
- The response includes the full personnummer. Treat it as a national identifier (GDPR Art.5(1)(c)) — do not propagate it to logs or external systems beyond what your integration strictly requires.
- Inactive (soft-deleted) employees are returned by the detail endpoint; check
is_activeif your flow should skip them.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": {
"id": "a8f1…",
"first_name": "Anna",
"last_name": "Andersson",
"personnummer": "YYYYMMDDNNNN",
"employment_type": "employee",
"employment_start": "2024-01-15",
"employment_end": null,
"salary_type": "monthly",
"monthly_salary": 35000,
"f_skatt_status": "a_skatt",
"is_active": true
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/employees {#post-employees-create}
employees.create · scope payroll:write
Create an employee.
Creates a new employee for the company. Requires Idempotency-Key (UUID). Supports ?dry_run=true for input validation without committing. The personnummer in the request body must be 12 digits (ÅÅÅÅMMDDNNNN); the response echoes a masked form (birthdate + XXXX) — GDPR Art.5(1)(c).
Use when: You need to register a new employee before adding them to a salary run. Use dry-run first to catch validation errors (missing tax table, salary amount, F-skatt mismatch) before committing.
Don't use for: Updating an existing employee (PATCH instead). Soft-deactivating (DELETE — sets is_active=false). Hard-deleting (the API does not expose hard delete; BFL 7 kap retention).
Pitfalls
- Idempotency-Key is mandatory — calls without it return 400 VALIDATION_ERROR.
- personnummer must be exactly 12 digits with the YYYYMMDD prefix (not the short 10-digit form).
- Duplicate personnummer within a company returns 409 EMPLOYEE_DUPLICATE_PERSONNUMMER. Personnummer is unique per (company_id, personnummer).
- For A-skatt employees who are not sidoinkomst, tax_table_number is required (29–42).
- salary_type drives which salary field is required: monthly_salary for monthly, hourly_rate for hourly.
- The response masks personnummer; never echo back the supplied value. Detail endpoint (deliberate drill-in) returns the full value.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"first_name": "Anna",
"last_name": "Andersson",
"personnummer": "YYYYMMDDNNNN",
"employment_type": "employee",
"employment_start": "2024-01-15",
"salary_type": "monthly",
"monthly_salary": 35000,
"tax_table_number": 33,
"tax_column": 1,
"tax_municipality": "Stockholm"
}
Example response
{
"data": {
"id": "a8f1…",
"first_name": "Anna",
"last_name": "Andersson",
"personnummer_masked": "YYYYMMDDXXXX",
"employment_type": "employee",
"employment_start": "2024-01-15",
"employment_end": null,
"employment_degree": 100,
"salary_type": "monthly",
"monthly_salary": 35000,
"hourly_rate": null,
"tax_table_number": 33,
"tax_column": 1,
"tax_municipality": "Stockholm",
"is_sidoinkomst": false,
"f_skatt_status": "a_skatt",
"vacation_rule": "procentregeln",
"vacation_days_per_year": 25,
"is_active": true,
"created_at": "2024-01-15T08:00:00Z"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
PATCH /api/v1/companies/:companyId/employees/:id {#patch-employees-update}
employees.update · scope payroll:write
Update an employee.
Partial update of an employee. Only the fields supplied in the body are changed. Supports ?dry_run=true to validate the merged record without committing. Personnummer changes are NOT permitted via this endpoint — the natural-person identity is immutable post-creation.
Use when: You need to change tax configuration, bank details, salary amount, or contact info on an existing employee.
Don't use for: Changing personnummer (not supported — create a new employee if the natural-person identity changes, which is a rare edge case). Soft-deleting (use DELETE).
Pitfalls
- personnummer in the body is ignored by this endpoint. To change it you must DELETE and recreate.
- salary_type changes require the matching salary field in the same request — switching to monthly without monthly_salary returns 400.
- tax_table_number changes only take effect on future salary runs; runs already in
reviewor beyond use a frozen snapshot.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: yes
Example request
{
"monthly_salary": 38000,
"tax_municipality": "Göteborg"
}
Example response
{
"data": {
"id": "a8f1…",
"monthly_salary": 38000
}
}
DELETE /api/v1/companies/:companyId/employees/:id {#delete-employees-delete}
employees.delete · scope payroll:write
Soft-delete an employee.
Sets is_active=false. The row is preserved because past salary runs reference it via salary_run_employees and those verifikationer are räkenskapsinformation under BFL 7 kap (BFL retention attaches to the verifikationer themselves, not strictly to the personnummer attribute on the master row). Hard delete is never exposed.
Use when: An employee has left the company and should no longer appear in active rosters or default to new salary runs.
Don't use for: Reactivating later (PATCH is_active=true instead). Hard-deleting (not supported — retention).
Pitfalls
- Idempotent: deleting an already-inactive employee returns 204 No Content (the same as the first call).
- The row is NOT removed from the database — re-creating with the same personnummer returns 409 EMPLOYEE_DUPLICATE_PERSONNUMMER even after soft-delete.
- Past salary runs still reference this employee; their data continues to surface in GET /salary-runs/{id} and SIE exports.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example response
{
"data": null
}