gnubok

Employees

Payroll roster — CRUD with personnummer masking on list endpoints.

Endpoints


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_active if 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 review or 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
}