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.GET/api/v1/companies/:companyId/employees/:id/absence: List absence days for an employee in a date range.GET/api/v1/companies/:companyId/employees/:id/opening-balances: Get an employee's payroll cutover opening balances.GET/api/v1/companies/:companyId/employees/:id/vacation-balance: Get an employee's current vacation balance.GET/api/v1/companies/:companyId/salary-runs/:id/employees: List per-employee results of a salary run.GET/api/v1/companies/:companyId/salary-runs/:id/employees/:employeeId: Get one employee's payslip in a salary run.POST/api/v1/companies/:companyId/employees: Create an employee.POST/api/v1/companies/:companyId/salary-runs/:id/employees: Add an employee to a draft salary run.POST/api/v1/companies/:companyId/salary-runs/:id/employees/:employeeId/lines: Add a payslip line to an employee in a draft salary run.PATCH/api/v1/companies/:companyId/employees/:id: Update an employee.PUT/api/v1/companies/:companyId/employees/:id/absence: Register absence for an employee over a date range.PUT/api/v1/companies/:companyId/employees/:id/opening-balances: Set an employee's payroll cutover opening balances.PUT/api/v1/companies/:companyId/employees/opening-balances: Bulk-set payroll cutover opening balances (atomic).DELETE/api/v1/companies/:companyId/employees/:id: Soft-delete an employee.DELETE/api/v1/companies/:companyId/employees/:id/absence: Delete absence days for an employee in a date range.DELETE/api/v1/companies/:companyId/salary-runs/:id/employees/:employeeId: Remove an employee from a draft salary run.
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"
}
}
GET /api/v1/companies/:companyId/employees/:id/absence {#get-employees-absence-list}
employees.absence.list · scope payroll:read
List absence days for an employee in a date range.
Returns per-day absence rows (sick, vab, parental, ...) between ?from and ?to (inclusive, max 92 days). No cursor pagination: the bounded range is the page. Optional ?type filter.
Use when: You need an employee's registered absence: to reconcile with an external time-tracking system, to verify what the salary engine will derive, or to display a calendar.
Don't use for: The derived pay impact (karensavdrag, sjuklön lines): that lives on the payslip detail after :calculate. Worked hours for hourly staff: separate register, not on v1 yet.
Pitfalls
- Ranges over 92 days return 400 ABSENCE_RANGE_TOO_LARGE: iterate quarters instead.
- A day can carry multiple rows with different absence_type values (e.g. half-day sick + half-day vab).
- Rows may reference the salary run that consumed them via salary_run_employee_id.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": [
{
"salary_absence_day_id": "abs_91d2…",
"absence_date": "2026-03-03",
"absence_type": "sick",
"hours": 8,
"notes": null
}
],
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
GET /api/v1/companies/:companyId/employees/:id/opening-balances {#get-employees-opening-balances-get}
employees.opening-balances.get · scope payroll:read
Get an employee's payroll cutover opening balances.
Returns the opening balances set for a mid-year migration (YTD gross/tax/net, vacation balances, opening semesterlöneskuld, karens adjustment) plus the lock state: locked=true once the employee has a booked salary run.
Use when: Verifying cutover state before the first calculated run, or checking whether balances can still be edited (locked=false).
Don't use for: The live vacation liability (GET /reports/vacation-liability includes the opening terms). Pre-cutover absence history: GET /employees/{id}/absence.
Pitfalls
- 404 NOT_FOUND when no opening balances have been set: distinct from an all-zeros row.
- locked_by_run_id names the booked run that froze the row; correcting that run unlocks it.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": {
"employee_id": "emp_77b2…",
"cutover_date": "2026-07-01",
"ytd_gross": 210000,
"vacation_paid_days_remaining": 12.5,
"locked": false
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
GET /api/v1/companies/:companyId/employees/:id/vacation-balance {#get-employees-vacation-balance-get}
employees.vacation-balance.get · scope payroll:read
Get an employee's current vacation balance.
Returns the open vacation-ledger row (recomputed on every booking): entitled/taken/remaining days, sparade dagar keyed by origin year (Semesterlagen 5-year rule), forced-payout days from expired savings, and a computed SEK estimate of the individual semesterlöneskuld.
Use when: Answering "how many vacation days does Anna have left", pre-payroll review, or preparing the year-close.
Don't use for: The company-wide liability report: GET /reports/vacation-liability. Closing the year: POST /salary/vacation-year-close.
Pitfalls
- 404 VACATION_BALANCE_NOT_FOUND until the first booking (or year-close) touches the employee: the ledger seeds lazily.
- remaining_days can go negative if more days were taken than entitled: surface it, do not clamp.
- The SEK estimate uses the year-close day valuation (simplified BFNAR 2016:10); the booked 2920 is reconciled only at year-close.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": {
"employee_id": "emp_77b2…",
"vacation_year_start": "2026-01-01",
"entitled_days": 25,
"taken_days": 10,
"remaining_days": 15,
"saved_days": {
"2025": 5
},
"saved_days_total": 5,
"estimated_liability_sek": 31151.4
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
GET /api/v1/companies/:companyId/salary-runs/:id/employees {#get-salary-runs-employees-list}
salary-runs.employees.list · scope payroll:read
List per-employee results of a salary run.
Returns one row per employee in the run with the calculated aggregates: gross salary, tax withheld, net pay, arbetsgivaravgifter, vacation accrual, and absence day counts. All aggregate fields are 0 until POST /calculate has run. Cursor pagination on (created_at, id).
Use when: You need the per-employee outcome of a run: to review before approval, to reconcile against an external system, or to pick an employee_id for the payslip drill-in.
Don't use for: Payslip line items or the step-by-step calculation breakdown: use GET /salary-runs/{id}/employees/{employeeId}. The employee master record: use GET /employees/{id}.
Pitfalls
- Aggregates are 0 until POST /calculate has advanced the run to review.
- tax_withheld_override / avgifter_amount_override are review-stage manual adjustments; the effective value is COALESCE(override, calculated).
- personnummer is masked on all payslip-shaped responses (GDPR Art.5(1)(c)); the employee detail endpoint returns the full value.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": [
{
"salary_run_employee_id": "sre_a8f1…",
"employee_id": "emp_77b2…",
"first_name": "Anna",
"last_name": "Andersson",
"personnummer_masked": "YYYYMMDDXXXX",
"salary_type": "monthly",
"gross_salary": 35000,
"tax_withheld": -8200,
"net_salary": 26800,
"avgifter_amount": 10997
}
],
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12",
"next_cursor": null
}
}
GET /api/v1/companies/:companyId/salary-runs/:id/employees/:employeeId {#get-salary-runs-employees-get}
salary-runs.employees.get · scope payroll:read
Get one employee's payslip in a salary run.
Returns the full payslip for one employee in a run: gross/tax/net aggregates, arbetsgivaravgifter with category, vacation accrual, YTD accumulators, every payslip line item (grundlön, tillägg, avdrag, förmåner), and the step-by-step calculation_breakdown recorded by the engine.
Use when: You need to verify how a specific employee's pay was computed: reviewing a run before approval, answering "why is the tax this amount", or rendering a payslip in an external system.
Don't use for: The rendered PDF payslip: use GET /salary-runs/{id}/payslips/{employeeId}/pdf. Editing line items: POST/PATCH/DELETE on the lines endpoints.
Pitfalls
- calculation_breakdown is null and aggregates are 0 until POST /calculate has run.
- line_items include engine-derived rows (absence, benefits) that are regenerated on every :calculate; manual rows survive recalculation.
- The effective tax is COALESCE(tax_withheld_override, tax_withheld); same for avgifter overrides.
- personnummer is masked here (GDPR Art.5(1)(c)); GET /employees/{id} is the identity drill-in.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": {
"salary_run_employee_id": "sre_a8f1…",
"employee_id": "emp_77b2…",
"first_name": "Anna",
"last_name": "Andersson",
"personnummer_masked": "YYYYMMDDXXXX",
"gross_salary": 35000,
"tax_withheld": -8200,
"net_salary": 26800,
"line_items": [
{
"salary_line_item_id": "sli_31c9…",
"item_type": "monthly_salary",
"description": "Grundlön",
"amount": 35000
}
]
},
"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"
}
}
POST /api/v1/companies/:companyId/salary-runs/:id/employees {#post-salary-runs-employees-add}
salary-runs.employees.add · scope payroll:write
Add an employee to a draft salary run.
Attaches an active employee to a draft run: snapshots their pay configuration (salary, degree, tax table) onto the run and seeds the base salary line (Grundlön/Timlön). For hourly employees, pass hours_worked.
Use when: The run was created without this employee (e.g. hired after the run was drafted), or you create runs empty and attach employees one by one from an external system.
Don't use for: Changing an attached employee's pay for this month (internal per-run PATCH; not on v1). Re-attaching after removal is fine: the snapshot is retaken.
Pitfalls
- Draft-only: 400 SALARY_RUN_EMPLOYEES_NOT_DRAFT once the run has advanced.
- Attaching twice returns 409 SALARY_RUN_EMPLOYEE_DUPLICATE.
- The snapshot freezes salary/degree/tax-table at attach time: later employee edits do not flow into this run.
- Inactive (soft-deleted) employees cannot be attached: 404 EMPLOYEE_NOT_FOUND.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"employee_id": "emp_77b2…"
}
Example response
{
"data": {
"salary_run_employee_id": "sre_a8f1…",
"employee_id": "emp_77b2…",
"salary_type": "monthly",
"monthly_salary": 35000
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/salary-runs/:id/employees/:employeeId/lines {#post-salary-runs-lines-create}
salary-runs.lines.create · scope payroll:write
Add a payslip line to an employee in a draft salary run.
Creates a salary_line_items row (bonus, overtime, gross/net deduction, benefit, traktamente, ...) for one employee in a draft run. account_number auto-resolves from item_type when omitted. Amounts are rounded to whole öre.
Use when: You need to add a one-off pay component before calculating: a bonus, an expense reimbursement, a union fee, or a manual correction line.
Don't use for: Editing the base monthly salary (PATCH the run-employee via the internal surface; not on v1 yet). Absence: register absence days instead (PUT /employees/{id}/absence); the engine derives sick/VAB lines itself.
Pitfalls
- Draft-only: returns 400 SALARY_RUN_LINE_NOT_DRAFT once the run has advanced.
- Line edits do not recompute tax or totals: call POST /salary-runs/{id}/calculate afterwards.
- Engine-derived lines (absence, benefits) are regenerated on every :calculate; manual lines survive.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"item_type": "bonus",
"description": "Kvartalsbonus Q2",
"amount": 5000
}
Example response
{
"data": {
"salary_line_item_id": "sli_31c9…",
"item_type": "bonus",
"description": "Kvartalsbonus Q2",
"amount": 5000,
"account_number": "7210"
},
"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
}
}
PUT /api/v1/companies/:companyId/employees/:id/absence {#put-employees-absence-upsert}
employees.absence.upsert · scope payroll:write
Register absence for an employee over a date range.
Expands [from, to] (max 92 days) to per-day rows and upserts them on the natural key (employee, date, type). Weekends are skipped unless include_weekends=true. Single day = from == to. Idempotent by construction: replaying the same PUT converges on the same rows.
Use when: "Anna was sick 3-7 March": one call registers the whole event. Also for pre-cutover history backfill when migrating from another payroll system (any past date is legal; imported sick days feed the karensavdrag lookback).
Don't use for: Vacation day REQUESTS/approval workflows (out of scope). Editing hours on one existing day inside a range: PUT the single day (from == to) with the new hours.
Pitfalls
- Weekends are skipped by default: pass include_weekends=true for schedules that span them.
- Upsert REPLACES the (date, type) rows in the range: hours/notes are overwritten, not merged.
- A day whose combined absence + worked hours exceed 24h returns 409 ABSENCE_HOURS_CONFLICT and the whole range is rejected (atomic).
- Registering absence does not recompute an open salary run: call POST /salary-runs/{id}/calculate afterwards.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"from": "2026-03-03",
"to": "2026-03-07",
"absence_type": "sick"
}
Example response
{
"data": {
"count": 5,
"days": [
{
"absence_date": "2026-03-03",
"absence_type": "sick",
"hours": 8
}
]
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
PUT /api/v1/companies/:companyId/employees/:id/opening-balances {#put-employees-opening-balances-set}
employees.opening-balances.set · scope payroll:write
Set an employee's payroll cutover opening balances.
Full-replace upsert of the cutover state: YTD gross/tax/net for the cutover year, paid vacation days remaining, sparade dagar keyed by origin year (5-year rule), opening semesterlöneskuld SEK (+avgifter), and karens periods not covered by imported absence rows. cutover_date must be the first of a month in the current or previous year, on/after employment_start.
Use when: Onboarding one employee during a mid-year migration from Fortnox/Visma/etc. For whole-company onboarding, prefer the bulk PUT /employees/opening-balances.
Don't use for: SIE opening balances on the LEDGER (2920/2940 arrive via the SIE import). Ongoing sick cases: import pre-cutover days via PUT /employees/{id}/absence instead.
Pitfalls
- Full replace: omitted numeric fields reset to 0 (their defaults). Send the complete state every time.
- 409 OPENING_BALANCES_LOCKED once the employee has a booked run; correcting that run unlocks.
- The opening liability is NOT booked by Accounted: it only feeds the vacation-liability report.
- YTD affects payslip display and reports only; per-month tax and avgifter caps never read it.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"cutover_date": "2026-07-01",
"ytd_gross": 210000,
"ytd_tax": 48000,
"ytd_net": 162000,
"vacation_paid_days_remaining": 12.5,
"vacation_saved_days_by_year": {
"2025": 5
},
"opening_semester_liability": 42000,
"opening_semester_liability_avgifter": 13196.4,
"karens_periods_adjustment": 1
}
Example response
{
"data": {
"employee_id": "emp_77b2…",
"cutover_date": "2026-07-01",
"locked": false
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
PUT /api/v1/companies/:companyId/employees/opening-balances {#put-employees-opening-balances-bulk-set}
employees.opening-balances.bulk-set · scope payroll:write
Bulk-set payroll cutover opening balances (atomic).
Upserts opening balances for up to 200 employees in one call. Validation is all-or-nothing: any invalid item (unknown/inactive employee, cutover before employment_start, locked by a booked run) fails the WHOLE request with a per-item error list and zero writes.
Use when: Onboarding a whole company mid-year from another payroll system: one call per migration file instead of N sequential PUTs.
Don't use for: Single-employee corrections after go-live: PUT /employees/{id}/opening-balances. Ledger opening balances (SIE import).
Pitfalls
- Atomic: one bad item fails everything. The error details carry item_errors[{index, employee_id, code, message}]: fix and resubmit the full set.
- Full replace per employee: resubmitting with fewer fields resets the omitted ones to 0.
- Duplicate employee_id within items is rejected outright.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"items": [
{
"employee_id": "emp_77b2…",
"cutover_date": "2026-07-01",
"ytd_gross": 210000,
"ytd_tax": 48000,
"ytd_net": 162000
}
]
}
Example response
{
"data": {
"count": 1,
"rows": [
{
"employee_id": "emp_77b2…",
"cutover_date": "2026-07-01",
"locked": false
}
]
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
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
}
DELETE /api/v1/companies/:companyId/employees/:id/absence {#delete-employees-absence-delete}
employees.absence.delete · scope payroll:write
Delete absence days for an employee in a date range.
Deletes per-day absence rows between ?from and ?to (inclusive), optionally filtered by ?type. Returns deleted_count (200, not 204) so callers can verify how many rows went.
Use when: An absence event was registered by mistake or ended early: "Anna came back Thursday, delete Thu-Fri sick days".
Don't use for: Correcting hours on a day: PUT the day again instead. Rows already consumed by a BOOKED run: deleting them does not un-book the run; use the run correction flow.
Pitfalls
- Without ?type, ALL absence types in the range are deleted.
- deleted_count: 0 with a 200 means nothing matched: not an error.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: yes
Example response
{
"data": {
"deleted_count": 2
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
DELETE /api/v1/companies/:companyId/salary-runs/:id/employees/:employeeId {#delete-salary-runs-employees-remove}
salary-runs.employees.remove · scope payroll:write
Remove an employee from a draft salary run.
Detaches the employee from the run and cascades away their payslip line items. Draft-only. The employee master record is untouched: this only affects the run roster.
Use when: An employee should not be paid this period (unpaid leave the whole month, employment ended) but was auto-added when the run was created.
Don't use for: Deactivating the employee entirely: DELETE /employees/{id} (soft-delete). Zero-salary months: keep them in the run with a 0 base instead if you want a nollkörning on record.
Pitfalls
- Draft-only: 400 SALARY_RUN_EMPLOYEES_NOT_DRAFT once the run has advanced.
- Cascade-deletes the employee's line items in this run, including manual ones.
- Re-attaching later retakes the pay snapshot from the employee master.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example response
{
"data": null
}